Android内存泄漏定位、分析、解决全方案

为什么会发生内存泄漏

内存空间使用完毕之后未回收, 会导致内存泄漏。有人会问:Java不是有垃圾自动回收机制么?不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑(logical leak)。虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。

例如

忘记释放分配的内存的。(Cursor忘记关闭等)。

应用不再需要这个对象,未释放该对象的所有引用。

强引用持有的对象,垃圾回收器是无法在内存中回收这个对象。

持有对象生命周期过长,导致无法回收。

Java判断无效对象的原理

Android内存回收管理策略图:

图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots 存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。反过来,如果圆节点与 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 过程中将其回收掉。

从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源任与GC-Root保持可达路径,导致系统无法进行回收。

内存泄漏带来的危害

用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。

内存泄露是内存溢出OOM的重要原因之一,会导致Crash

Android中常见的可能发生内存泄漏的地方

1.在Android开发中,最容易引发的内存泄漏问题的是Context

比如Activity的Context,就包含大量的内存引用,一旦泄漏了Context,也意味泄漏它指向的所有对象。

造成Activity泄漏的常见原因:

Static Activities

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。

如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。

因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。

static Activity activity; //这种代码要避免

单例中保存Activity

在单例模式中,如果Activity经常被用到,那么在内存中保存一个Activity实例是很实用的。但是由于单例的生命周期是应用程序的生命周期,这样会强制延长Activity的生命周期,这是相当危险而且不必要的,无论如何都不能在单例子中保存类似Activity的对象。

举例:

publicclassSingleton{privatestaticSingleton instance;privateContext mContext;privateSingleton(Context context){this.mContext = context;    }publicstaticSingletongetInstance(Context context){if(instance ==null){synchronized(Singleton.class){if(instance ==null){                    instance =newSingleton(context);                }            }        }returninstance;    }}

在调用Singleton的getInstance()方法时传入了Activity。那么当instance没有释放时,这个Activity会一直存在。因此造成内存泄露。

解决方法:

可以将new Singleton(context)改为new Singleton(context.getApplicationContext())即可,这样便和传入的Activity没关系了。

Static Views

同理,静态的View也是不建议的

Inner Classes

内部类的优势可以提高可读性和封装性,而且可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。 例如在内部类中持有Activity对象

解决方法:

1.将内部类变成静态内部类;

2.如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;

3.在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

例如:

发生内存泄漏的代码:

public class LeakAct extends Activity{

@Override

protected void onCreate(Bundle savedInstanceState){

    super.onCreate(savedInstanceState);        

     setContentView(R.layout.aty_leak);        

     test();    }//这儿发生泄漏    

public void test(){

    newThread(newRunnable() {

        @Override public void run(){

            while(true) {

                        try{                        

                                 Thread.sleep(1000);                    }

                        catch(InterruptedException e) {

                                 e.printStackTrace();                    

                             }               

                         }            

             }        }).start();    } }

解决方法:

public class LeakAct extends Activity{

    @Override

    protected void onCreate(Bundle savedInstanceState){

        super.onCreate(savedInstanceState);        

         setContentView(R.layout.aty_leak);        

         test();    }//加上static,变成静态匿名内部类

public static void test(){

       newThread(newRunnable() {

            @Override public void run(){

                    while(true) {

                            try{                        

                                     Thread.sleep(1000);                    

                             }catch(InterruptedException e) {                       

                                     e.printStackTrace();                    }                }            }        }).start();    } }

Anonymous Classes

匿名类也维护了外部类的引用。当你在匿名类中执行耗时任务,如果用户退出,会导致匿名类持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。

原文链接

更多教程

Handler

handler中,Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。

解决方法:

1.可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露;

2.如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的.

3.在界面销毁是,释放handler资源

@OverrideprotectedvoiddoOnDestroy(){super.doOnDestroy();if(mHandler !=null) {        mHandler.removeCallbacksAndMessages(null);    }    mHandler =null;    mRenderCallback =null; }

同样还有其他匿名类实例,如TimerTask、Threads等,执行耗时任务持有Activity的引用,都可能导致内存泄漏。

线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

要解决Activity的长期持有造成的内存泄漏,可以通过以下方法:

传入Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,主动结束执行的任务,并释放Activity资源。

将线程的内部类,改为静态内部类。

因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。

如果需要引用Acitivity,使用弱引用。

谨慎对context使用static关键字。

2.Bitmap没调用recycle()

Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才设置为null.

3.集合中对象没清理造成的内存泄露

我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

解决方案:

在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。

4.注册没取消造成的内存泄露

这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。

解决方案:

1.使用ApplicationContext代替ActivityContext;

2.在Activity执行onDestory时,调用反注册;

5.资源对象没关闭造成的内存泄露

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。

6.占用内存较多的对象(图片过大)造成内存溢出

因为Bitmap占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android分配给Bitmap的大小只有8M.

解决方法:

1.等比例缩小图片

BitmapFactory.Options options =newBitmapFactory.Options(); options.inSampleSize =2;//图片宽高都为原来的二分之一,即图片为原来的四分之一

2.对图片采用软引用,及时地进行recycle()操作

//软引用SoftReference bitmap =newSoftReference(pBitmap);//回收操作if(bitmap !=null) {if(bitmap.get() !=null&& !bitmap.get().isRecycled()){            bitmap.get().recycle();            bitmap =null;        } }

7.WebView内存泄露(影响较大)

解决方案:

用新的进程起含有WebView的Activity,并且在该Activity 的onDestory() 最后加上 System.exit(0); 杀死当前进程。

检测内存泄漏的方法

1.使用 静态代码分析工具-Lint 检查内存泄漏

Lint 是 Android Studio 自带的工具,使用姿势很简单 Analyze -> Inspect Code 然后选择想要扫面的区域即可

对可能引起泄漏的编码,Lint 都会进行温馨提示:

2.LeakCanary 工具

Square 公司出品的内存分析工具,官方地址如下:https://github.com/square/lea...

LeakCanary 需要在项目代码中集成,不过代码也非常简单,如下的官方示例:

在你的 build.gradle:

dependencies{ debugImplementation'com.squareup.leakcanary:leakcanary-android:1.6.3'releaseImplementation'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'// Optional, if you use support library fragments:debugImplementation'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'}

在 Application 类:

publicclassExampleApplicationextendsApplication{@OverridepublicvoidonCreate(){super.onCreate();if(LeakCanary.isInAnalyzerProcess(this)) {// This process is dedicated to LeakCanary for heap analysis.// You should not init your app in this process.return;    }    LeakCanary.install(this);// Normal app init code...}}

当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录

-3.Android Monitor

开Android Studio,编译代码,在模拟器或者真机上运行App,然后点击

,在Android Monitor下点击Monitor对应的Tab,进入如下界面

在Memory一栏中,可以观察不同时间App内存的动态使用情况,点击

可以手动触发GC,点击

可以进入HPROF Viewer界面,查看Java的Heap,如下图

Reference Tree代表指向该实例的引用,可以从这里面查看内存泄漏的原因,Shallow Size指的是该对象本身占用内存的大小,Retained Size代表该对象被释放后,垃圾回收器能回收的内存总和。

扩展知识

四种引用类型的介绍

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;

软引用(SoftReference):只有在内存空间不足时,才会被回的对象;

弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

(注:转载自 https://segmentfault.com/a/1190000018118285

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352