Android内存优化(二):一分钟发现内存泄漏

在上一篇文章Android内存优化(一):Java内存区域中已经大体上介绍了Java中的内存分布情况,这一篇主要讲一下内存泄漏的产生原因、内存泄漏的危害、内存泄漏一键分析与定位、以及代码中常见的内存泄漏。

1内存泄漏的产生原因

前方高能,18岁以下请避让!!!
惊天大咪咪:内存泄漏产生的原因是对象占着茅坑不拉屎!!!
有必要讲一下Android中的垃圾收集是怎么进行的,Android中使用标记-清除(Mark-Sweep)算法进行垃圾回收(garbage collection,简称GC),就是按照正常套路来说,在坑位(内存)不够的情况下,垃圾收集器会遍历全部对象,看哪些对象是可以被回收掉腾出内存的,这个过程称为Mark(标记),Mark的时候要求除了垃圾收集线程之外,其它的线程都停止,这种吊炸天的现象在垃圾收集算法中称为Stop The World,世界围着他转,这就造成了我们的程序会卡顿,但是一般情况下这个时间就几十毫秒,我根本就感受不到好吗。Mark完之后,就是释放内存空间啦,这个过程称为Sweep(清除)。

这一切看起来很美好,但是就是有内存泄漏发生,所以得提一下,不是所有的对象都是特仑苏,阿呸,不是所有的对象都能被回收的,比如下面的傲娇贱货。

  • 垃圾回收的原则:被全局变量(static)、栈变量和寄存器等直接引用和间接引用的对象不能被回收。

    所以说,对象即使已经使用完,但却一直被其它对象引用,就会导致这个对象无法被回收,造成内存的浪费,让别的对象无屎可拉。对象无法被GC回收就是造成内存泄露的原因!

2内存泄漏的可能会造成的创伤

如果不是利用工具去找的话,一般情况下内存泄漏是比较难发现的,因为Java中不会报内存泄漏这种异常,所以在轻微的内存泄漏表面上看是跟正常情况下没有区别的。

  • 2.1 内存泄漏跟内存溢出(OOM)的区别就是:量变和质变。一个两个内存泄漏表面看起来没毛病,但是量变可以导致质变,内存泄漏多了会炸的,就是报OOM异常,应用直接崩溃,连解释的机会都没有。
  • 2.2 堆得内存大小是确定的,出现内存泄漏后可用的内存会减小,这又会造成垃圾回收的频率加剧,上面提到过,垃圾回收的Mark阶段会有一种吊炸天的现象,就是Stop The World,除了垃圾回收线程之外的线程会停止,频繁的垃圾回收卡顿明显的感受到。
  • 2.3 应用后台运行的时候,内存占用大,进程被系统杀死的概率就会大咯。

3内存泄漏的发现

内存泄漏的分析的话,必须使用工具才行,庆幸的是,各路大神已经给我们提供了很多强大的内存分析工具,我这里只会讲最方便的。这里提供几个套餐供选择

3.1 套餐一:Studio自带Heap Viewer

想不想知道你的应用到底有没有内存泄漏呢?说真,就一分钟的事。

  • 3.1.1打开Studio,连上你的应用,然后Android Monitor (1)->Monitors(2)->Memory,上面有四个图标,暂停图标是开启内存使用状态追踪的开关,默认是开启的,小车图标就是手动GC(3),向下箭头图标(4)是查看堆的分配情况,最后的图标allocation tracker用来跟踪内存分配情况。

  • 3.1.2我讲一下我的使用方式,在应用中操作,从activity1跳转到activity2,然后跳回到activity1界面,这样是为了分析activity2是否会产生内存泄漏。接下来就是真刀真枪的干了。

  • 3.1.3点击小车图标(3),手动GC进行垃圾回收,这样才能更准确的判断activity2是否有内存泄漏发生,最后点击向下箭头图标(4),Studio会自动生成hprof文件并自动展示在Studio界面中。


  • 3.1.4这个就是内存的分析文件了,点击Analyzer Tasks(5),这是让Studio帮我们自动分析是否出现内存泄漏。


  • 3.1.5勾上Detect Leaked Activities(6),最后运行(7)就出现分析结果了


  • 3.1.6看到没,activity2出现内存泄漏了(8),左下角是引用树(9),通过引用树就可以定位到内存泄漏的具体信息了。


3.2套餐二:Heap Viewer + MAT

是啊,发现有内存泄漏了,然而还有其它的选择,这里就必须使用到其它的工具进行辅助了。


MAT(Memory Analyzer)内存分析工具,这个工具的使用我只简单讲一下,因为我一般不用,不要问为什么,因为用起来比较麻烦一些。

  • 3.2.1MAT下载,进入下载的官网,我电脑是64位的,所以选择Windows(x86_64),整个下载安装流程跟一般软件没啥区别,进入新页面然后点击DOWNLOAD

    点击click here就可以下载使用了
  • 3.2.2 hprof文件导入,这个文件的获取流程跟内存泄漏的发现流程基本一样,按上面说的通过Studio的Heap工具获取的,但是文件导入前需要进行一下转换,因为MAT工具不能直接使用,转换也

    不麻烦,Studio已经帮你简化这个过程,一键导出转换文件,请看过来
  • 3.2.3 用MAT打开hprof的转换文件,其中Histogram和Dominator Tree比较常用,分析内存泄漏特别需要用到Histogram的两份文件对比分析,就是获取两份内存泄漏前后的hprof转换文件
  • 3.2.3 标题栏Window->Navigator History,打开 Navigator History面板,然后点击打开Histogram


  • 3.2.4 右键histogram,将两份分析文件的

    Histogram结果都添加到 Compare Basket中,点击右上角的!图标就会生成对比文件
  • 3.2.5 这就是最后生成的对比文件,你还可以自己选择对比的方式,红圈里面提供不同的对比方式,这样就可以很直观的看出差异,因为我对比的是同一份文件,所以对象间木有差异。


3.3套餐三:Leakcanary

square的开源内存泄漏分析框架,好用得不得了,配置很简单

  • 3.3.1建议在app的build.gradle文件下添加下面的依赖
dependencies {
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
        testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }
  • 3.3.2在你的Application中的onCreate()方法中进行初始化
public class ExampleApplication extends Application {

  @Override public void onCreate() {
    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...
  }
}
  • 3.3.3然后,就没有然后了,编译完后运行你的项目,会在项目安装成功后出现附加的组件,里面会展示具体的内存泄漏路径。


  • 3.3.4通过这个泄漏路径,就对应进行内存泄漏的原因进行分析了,你也可以通过输出的日志进行内存泄漏的定位。


注:到这里3个套餐已经讲完了,关于MAT这个套餐我只是讲一下基本的使用,其实已经够用了,怎么说呢,用起来比较麻烦,所以我自己本身也很少用,我就按自己的使用对比一下三者。
套餐三>套餐一>套餐二
1.套餐三使用最方便,一劳永逸,解析hprof的速度有点慢,但是因为后台自动解析,所以基本上没多大关系;
2.套餐一使用最快,切换一下页面分分钟就知道有没有内存泄漏,但是需要你每一次都要手动操作;
3.套餐三最麻烦,耗时耗力,但是自动分析工具并不能保证找出所有的内存泄漏,这个时候就需要通过MAT辅助分析了。

4代码里头内存泄漏的常见原因

代码中内存泄漏大多数产生的原因是不遵循activity的生命周期。

  • 4.1单例模式(静态activity):在你的Activity中定义了一个 static 变量引用了activity,因为static变量的生命周期和app一样长,就算activity被销毁,activity对象还是会被static变量持有,一直到app被销毁,这也是单例模式最容易造成泄漏的原因,如果静态的单例对象持有activity对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。解决办法是使用Application的Context代替activity的context;
/**
  * 单例模式
 */
public class SingletonClass{
    private static SingletonClass instance;
    private Context context;
    public static SingletonClass getInstance(Context context){
            synchronized(SingletonClass.class){
                if(instance==null){
                    instance=new SingletonClass(Context context);
                }
            }
        return instance;
    }
    private SingletonClass(Context context){
       this.context = context; //传入activity的context就会造成内存泄露咯
    }
}
  • 4.2静态View:当一个view 被加入到界面中时,它就会持有 context 的强引用,也就是我们的 activity。如果我们通过一个static成员变量引用了这个 view,相当于直接引用了 activity,然后就泄漏了;
private static View view;
view = findViewById(R.id.sv_button);
  • 4.3非静态内部类:我们都知道,内部类能够引用外部类的成员,这正是内部类的好处所在,但是恰恰是这个优势会导致activity内存泄漏,因为非静态内部类默认持有外部类的引用。如果我们创建了一个内部类的对象,并且通过静态变量持有这个对象,就会导致内存泄漏;
        private static InnerClass inner = new InnerClass();
        class InnerClass {
        }
  • 4.4匿名内部类:匿名类同样会持有定义它们的对象的引用,如果在 activity 内定义了一个匿名的 AsyncTask 对象,就有可能发生内存泄漏了。因为在activity被销毁之后AsyncTask可能仍然在运行,这样只能等到AsyncTask执行结束才能回收activity;
new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
  • 4.5Handler+Runnable:定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 activity 泄漏。Runnable对象引用了定义它的 activity 对象,而它会被提交到 Handler 的 MessageQueue 中,如果它在 activity 销毁时还没有被处理,那就会导致内存泄漏了。
new Handler() {
         @Override
         public void handleMessage(Message message) {
                super.handleMessage(message);
            }
        }.postDelayed(new Runnable() {
            @Override public void run() {
                while(true);
            }
        }, 1000);
  • 4.6Thread:原因类似4.5,尽管是在单独的线程执行任务,但是线程还是会默认持有外部对象,任务没有执行完成就不会释放持有的引用;
new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
  • 4.7资源未关闭:如果使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
  • 4.8集合容器:在我们做缓存的时候会用一些数据结构来存储一些数据,当我们不需要它时要及时清理,不然就会像滚雪球一样会越来越大,想不泄露都难。

可以了,造成内存泄露还有很多原因,这就靠慢慢跳坑了,生活太艰难。再话痨一下,“千丈之堤,以蝼蚁之穴溃;百尺之室,以突隙之烟焚。”,所以我推荐套餐三Leakcanary,让你的整个开发过程伴随着内存泄露的监控。

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

推荐阅读更多精彩内容