循序渐进学用MAT排查Android Activity内存泄露

一、先磨刀再砍柴,内存泄露相关介绍

  我们先来简单重温一下Java GC 的概念:GC即为Garbage Collection,垃圾回收机制。Java GC实质上也就是一个运行在Java虚拟机(JVM)上的一个程序,它自动地管理着内存的使用,在适当的时机释放并回收无用的内存分配。使得我们不用像写C++那样手动释放内存,从而帮助我们释放双手。那它是如何知道哪些内存分配是无用的,而哪些是有用的呢?借用一下2011年Google I/O的一张图:

GC

  垃圾回收机制设置了一个根节点GC Roots,然后从根节点开始往下进行遍历,凡是能被直接或间接访问到的子节点,都认为其是仍然有用的,不应该被回收的,也就是图中的黄色节点。而无法被遍历到的蓝色节点,即被认为是无用的,应该被回收的内存区域。
  那什么是内存泄露呢?简单来说就是:不应该被GC Roots访问到到的内存,仍能被访问到,GC Roots误以为这块内存区域并不是垃圾,导致该回收的内存没被回收。久而久之,内存泄露越来越严重,旧的垃圾内存得不到回收,新的垃圾内存不断增加,可用的内存也就越来越少。JVM为了申请新的内存空间,频繁触发GC,程序执行效率将会受到影响,程序甚至直接抛出Out Of Memory Exception异常退出。

  下面我们来熟悉一下Android开发中容易出现的内存泄漏场景:

参考:http://www.jianshu.com/p/a50ea6333677

1、静态变量引用Activity

  静态变量是驻扎在JVM的方法区,因此,静态变量引用的对象是不会被GC回收的,因为它们所引用的对象本身就是GC ROOT。如果实在要用这样别扭的写法,记得在onDestroy()里把mActivity置为null。

public class TopicDetailActivity extends AppCompatActivity {
    private static Activity mActivity;

    public static void enterActivity(Activity activity) {
        mActivity = activity;
        Intent intent = new Intent(activity, TopicDetailActivity.class);
        activity.startActivity(intent);
    }
}

2、非静态内部类

  非静态内部类(包括匿名内部类)默认会持有外部类实例,如果内部类的生命周期长于外部类,则可能导致内存泄漏。可以考虑改成静态内部类,或者新创建一个文件,把内部类挪过去。

public class MainActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        MyThread myThread = new MyThread();  
        myThread.start();  
    }  

    class MyThread extends Thread {  
        @Override  
        public void run() {  
            while (true) {  
                try {  
                    Thread.sleep(65536);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
}  

3、Activity内View不被释放

  一旦View attach到我们的Window上,就会持有一个Context(即Activity)的引用。如果View无法被释放,Activity也无法被释放。

public class MainActivity extends AppCompatActivity {

    private static TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = (TextView) findViewById(R.id.tv);
        tv.setText("Hello world");
    }
}

4、Handler

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {

            }
        };
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, Integer.MAX_VALUE);
    }
}

  同第二点,代码中new了一个Handler的匿名内部类,默认持有Activity。我们知道,主线程的Looper对象不断从消息队列中取出消息,然后再交给Handler处理。而每个Message对象是持有Handler的引用的(Message对象的target属性持有Handler引用),从而导致Message间接引用到了Activity。如果在Activty destroy之后,消息队列中还有Message对象,Activty是不会被回收的。同样是,可以把匿名内部类改成静态内部类,或挪到另一个文件中去。还可以在onDestroy()时,removeCallbacks或removeCallBacks来清除MessageQueue残留的Message,已保证Activity不会被Message hold住。

5、监听器

  在Activity里注册BroadcastReceiver或EventBus等忘记反注册。

….更多详情看http://www.jianshu.com/p/a50ea6333677

二、步入正题,学用MAT

MAT是一种快速,功能丰富的Java堆分析工具,用于分析hprof文件,帮助你查找内存泄漏和减少内存消耗。

1、获取hprof文件

  首先,我们打开Android Studio,调试好ADB相关,并run起我们的工程,这里确保一下我们的工程存在内存泄漏的Activity(- -因为如果没有内存泄漏存在,那就没得分析啦,巧妇难为无米之炊啊)。我们不停地在页面间进行跳转,最后回到起始的MainActivity。然后,在AS的左下角可以找到一个名为”Android Monitor”的栏目,点开之后再从我们常用的”logcat”切换到”Monitors”下,此时可以看见已连接手机的内存、CPU、GPU等使用情况。点击”Memory”项里红框选中的按钮”Dump Java Heap”。

dump

  稍等片刻,我们就会得到一个“不完整”的hprof文件,AS会自动帮我们打开这个文件。(注意,这个hprof还不能用MAT打开,强行用MAT打开会报错!)

before analyze

  点击右边的Analyzer Tasks,会有意外的惊喜哟~没错!AS居然自带了方便我们排查Activity内存泄露的工具。

2、意外的收获,AS自带的分析工具

  勾选”Detect Leaked Activities”,然后点击绿色的”>”按钮,分析结果就出来了。展开分析结果里的”Leaked Activities”就可以看到哪些Activity存在内存泄漏问题。这里可以看出TopicDetailActivity明显就泄漏了,因为我dump hprof文件时,已经回到起始的MainActivity了且Activity栈不存在TopicDetailActivity的实例了,所以内存中不应该有那么多TopicDetailActivity的实例:@601142272,@603487232等。这些实例都应该被GC回收,而没有被回收!查找内存泄漏是不是So easy啊~。嗯(⊙_⊙),虽然每天用AS都卡得飞起,但是这个分析工具还是值得肯定的,方便,准确!

analyze

  好了,我们现在知道,哪些Activity被泄漏了,但是我们仍然不知道它们为什么会被泄漏。刚开篇的时候,我们说到,能被GC Roots直接或间接访问到的实例是不会被回收的。Activity泄漏和其他内存泄漏一样,都是因为被某个长时间不会被销毁的家伙给引用了,而这个家伙能被GC Roots访问到。接着,我们就来看看是谁hold住了Activity又不肯放手。

reference tree

  选中某个已知泄漏的Activity,可以查看其Reference Tree。这里列出了所有引用TopicDetailActivity的对象,一眼望过去,图中引用TopicDetailActivity实例的对象有View类的,比如TextView,也有非View类,比如ContextImpl。这里注意一下,AS会把重点怀疑对象用蓝色字体标示出来,这里标出来的是CursorWatcherEditTextView(这里声明一下,它是EditText的子类),泄漏的原因八九不离十就是这个自定义View。那又是什么导致CursorWatcherEditTextView Hold住了Activity呢。这里再继续展开Reference Tree也不容易看出个所以然来。要想进一步深入还得使用MAT。
  我们来导出一个”完整版”的hprof。点击AS左上角的歪脖”Captures”,然后按图示”Export to standard.hprof”,选好导出位置再导出就好了

standard-hprof

3、主角登场,MAT使用简介

  接下来我们用MAT打开hprof文件:

overview

  注意红框里圈出的两个Action,分别是用两种不同角度查看内存的使用情况。
  Histogram根据以class为基本单位,来列举每个类各有多少个实例(Objects),Shallow Heap代表这个类的所有实例占有的内存大小。然后也没啥好看的了,我们再去看看Dominator Tree。

Histogram

  Dominator Tree会从大到小列举出一些当前内存中占用最大内存的对象。Shallow Heap的概念与Histogram里的一样。Retained Heap代表如果这个对象被回收,有多少内存能被真正释放,其中包括了Shallow Heap(自身占有的内存) + 只被这个对象Hold住的其他对象。Percentage则表示占比,这很简单。

dominator-tree

  刚才我们已经通过AS分析工具,得知TopicDetailActivity这个页面存在内存泄漏,故在搜索框里输入TopicDetailActivity。在以上两种Tab下,选中某一栏右键都能看到一系列的操作。

right-click

  这里我们只介绍一下”List Object”和”Merge Shortest Paths to GC Roots”,其他的大家自己去摸索哈。
  List Object也有两个可选的“with outgoing reference”:查看这个对象它引用了谁,”with incoming reference”:查看是谁引用了这个对象,又是两种不同的角度。排查内存泄漏,我们肯定是看incoming,因为只有被引用才可能被hold住,才可能内存泄漏。放上incoming的图,大家自行感受一下。

incoming

  由上图可见,总共有306个实例引用了TopicDetailActivity,一个个排查过去不得爆炸..所以我们化繁为简,用”Merge Shortest Paths to GC Roots”->”exclude weak/soft references”(排除掉弱引用和软引用,因为他们不会造成内存泄漏):从GC Roots开始找一条能到达Activity的最短的路径,这往往就是内存泄漏的真正原因。

shortest-gc-path

  然后逐级展开到底,我们能看到TopicDetailActivity实例。然后,简单介绍一下如何看这个图。我们从下往上看,Activity被CursorWatcherEditText hold住了,而Activity的引用被赋值在CursorWatcherEditText的mContext变量域里,同理,CursorWatcherEditText被Editor hold住了,CursorWatcherEditText的引用被赋值在Editor的mTextView变量域里,以此类推。那这个内存泄漏到底是为什么呢?这个问题可能比较复杂,我们慢慢理清逻辑。

  CursorWatcherEditText持有TopicDetailActivity这很正常,因为我们之前说过,一旦View attach到我们的Window上,就会持有其Activity的引用。我们再往上找原因。因为CursorWatcherEditText继承自EditText,EditText继承自TextView,通过看TextView.java的源码可知,在new 成员变量mEditor的时候,会把TextView实例自身传给mEditor,然后被保存在Editor实例的mTextView成员变量域内,这依旧没啥问题。

textview

Editor

  然后我们再往上看,噫,Editor被他自己的内部类Blink hold住了,找到Blink类,这货居然不是静态的内部类,还是个Handler,还实现了Runnable,极有可能就是这个导致的内存泄露。

Blink

  我们最后往上看一次- -,一眼看完,Path上端还真的有Message,MessageQueue,就是我们之前说的因为仍有残留的Message hold住了Handler导致的内存泄漏。好了,这条Path总算是摸清了,但是新的问题又出现了,为什么会有残留的Message,是不是我们用EditText的姿势不对或者说是Extends的时候出了什么问题。虽然不太明白,但是我们进一步阅读源码后发现,Blink与Cursor以及Focus息息相关,并且TextView#setEnabled(boolean enabled)里有这么一行,会尝试调用blink.removeCallbacks:
makeBlink,在onDestroy()里调用editReply.setEnabled(false);,内存泄漏得到解决

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值