
貌似很久很久没写博客了,996的生活过了太久,自己也有点颓废了,感觉这样子的状态不行,有一种莫名的负罪感。虽然中间都有断断续续的记录一些东西,学习一些东西,但都感觉还不成气候,就一直没有发到博客上来,但每次想到我的博客都难免有些难受。今天,我选择了面对,而不是逃避,刚好,最近在忙新项目的事情,也涉及到了性能优化方面的事情,就来记录一下最近又重新学习了的,和之前用过的一些经验,记录一下,整理一下。
1.内存泄露的定义
所谓内存泄露:就是一些本该被回收的资源,被一些对象错误的引用着,而导致GC没有办法对这些资源进行回收,而造成的一种内存资源浪费的现象。直白的说,就是GC失去了对这些资源的掌控。
为什么说被引用的对象无法被回收呢?
这就要说起jvm的垃圾回收机制了。早前,jvm使用的是引用计数器的方法来判断当前对象是否需要被回收的,而这个方法会引发一个问题,就是孤岛问题,就是说,一些应该被回收的对象,相互引用着,造成对象的计数器,不为0,也就表示该对象还是被其它对象引用着,实际上这一系列对象都应该被回收。
而为了解决这个问题,后面引入了根搜索法(GC ROOT)的方法来判断一个对象是否应该被回收,大概意思就是说,从GC ROOT开始将所有还用到的对象,以树的形式连接到一起,而不在这个树上面的对象,就表示为需要被gc回收的无用对象,从而解决了孤岛问题。
2.常见的内存泄露的情况
1.错误的Context引用(不同生命周期的对象相互引用)
/**
 * Created by allen on 17/3/26.
 */
public class CommonUtils {
    private static CommonUtils mCommonUtils;
    private Context mContext;
    private CommonUtils(Context context) {
        this.mContext = context;
    }
    public static CommonUtils getInstance(Context context) {
        if (mCommonUtils == null) {
            synchronized (CommonUtils.class) {
                if (mCommonUtils == null) {
                    mCommonUtils = new CommonUtils(context);
                }
            }
        }
        return mCommonUtils;
    }
}
在activity中通过下列方式进行应用:
CommonUtils.getInstance(this);
上面的这种情况是典型的使用场景,而上面的这种用法,就造成了内存泄漏。每一次调用getInstance方法的activity会被CommonUtils中的Context一直引用着,而当其被调用onDestory后,依然,无法被回收。
解决方法:
使用getApplicationContext代替Context,主要是单例对象是静态对象,其生命周期和Application一致。
2.handle造成的内存泄露
public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Message message = Message.obtain();
        message.arg1 = 1;
        mHandler.sendMessage(message);
    }
}
上面的代码就是Handler应用的典型场景。  
handler是通过android消息机制来进行运转的,MessageQueue中的每个消息都持有一个Handler对象的引用,而非静态内部类持有外部类的一个引用。当MessageQueue还有没有发送的消息时,该Message就持有该Handler的引用,而该Handler对外部类Activity也持有其引用,则此时,如果Activity退出时,由于被引用,而无法被回收,从而造成了内存泄露。
解决方法:
使用static来修辞Handler来解决内部类持有对外部类引用的问题。
3.容器的不当使用,造成的内存泄露
上面的MessageQueue中Message持有对Handler的引用,也是因为容器不当持有需要被销毁对象的引用,而造成的内存泄露。
多见于观察者模式下,没有及时移除引用的情况下,导致的内存泄露。
解决方法:
在相应的生命周期内移除容器内的资源。比如:在onDestory中,移除容器内部对该Activity的引用。
4.内部类的内存泄露
还是同样的意思,当外部类需要被销毁时,内部类还有任务在执行,而无法被销毁时,任务引用了内部类,而内部类又引用了外部类,导致外部类无法被回收,而造成的内存泄露。
解决方法
将内部类使用static修辞。
5.资源未正常关闭造成的内存泄露
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
解决方法
正常关闭相应资源。
……
3.常用的内存泄露分析工具
- Android Monitor
- MAT
- LeakCanary
- Allocation tracking
Android Monitor
Android Monitor是Android Studio中默认的性能优化工具。它提供了对Memory,CPU,Network,GPU的监视。我们今天主要是用来对Memory的监控。
以单例模式下Context泄漏为例子来说下其使用.
多次翻转屏幕后,造成MainActivity的多次回收和创建,然后,按下Initiate GC按钮后,再按dump java heap按钮来观察当前heap中MainActivity的情况。
如图所示,上面出现了两个MainActivity的实例,我们都知道,如果在翻转屏幕的时候,如果没有在Androidmainfest.xml中配置横竖屏不敏感的话,当前Activity会被销毁,而新的Activity会被创建,也就是说,正常情况是不会出现两个Activity的实例的,所以,说明是出现了内存泄漏。
这里可以通过包结构的方式来看相关的泄露情况,更加的直观方便。
图上的信息是通过多次GC之后,再dump出来的。从图上可以看到存在两个MainActivity的对象,说明MainActivity存在泄露,而为什么下面会多了一个MainActivity$1,而且个数正好也是两个呢?
其实,这里的MainActivity$1只是MainActivity对象的引用,这一个可以从MainActivity$1中的this所指向的内存地址正好是MainActivity的两个地址。
MAT
MAT(memory Analyzer tool):内存分析工具,这个是曾经在Eclipse时代,非常好用的一款内存分析工具,而且有单独版本的。我们这里就使用单独版本的来说明一下其使用方法。
同样的,我们使用Android studio中dump出.hprof格式的文件,然后使用hprof-conv 源文件   目标文件如:hprof-conv 1.hropf  2.hprof来生成MAT可以使用的目标文件2.hprof。
文件转换完成后,我们导入文件,导入的时候选择内存泄露分析。
这里我主要说两个我们常用的功能,一个是histogram功能,还有一个就是内存对比。还是以上面MainActivity泄露的问题来分析。
打开histogram功能后,就看到了下面的结果:
像上面的东西,我们基本上是看不到任何有价值的东西的,这时,需要我们自己去搜索感兴趣的对象,看其存在的情况了。比如,我们可以搜索MainActivity大小写区分,来看看MainActitivy在内存中的存在情况。

图上可以看出,存在两个MainActivity对象,而其占用的内存大小为528B.接下来我们要看看是谁引用了我们的引用。
查看列表,我们发现MainActivity被CommonUtils中的mContext引用着。
我们使用merge shortest path(最短路径)也可以看出,其被CommonUtils引用着。

同样的,下面这种方式也能找到MainActivity被谁引用着。

下面,我们来说说,通过内存映射对比来分析其内存泄露情况。
分别打开new27和new28两个.hprof文件,然后,分别打开histogram功能。
分别将histogram添加到basket中。
添加完成后,要点击一下执行结果,才会出现对比的结果。
看到下面的内存对比的情况,这里new27和new28分别表示无内存泄露和内存泄露。
通过搜索MainActivity,我们可以看出#0出现了内存泄露的情况。
Allocation Tracking(分配内存追踪)
这个工具主要是用来分析内存的变化情况,比如:你打开了一个新的页面,此时,新的页面创建了多少对象,哪些对象被创建了多少次都会被追踪到,但如果调用gc,被回收的对象,也会显示在内存变化中,也就是它关心的是这段时间内,内存的变化,只做加法,也就是只记录最大的变化数,而被回收的对象,也会显示在列表中。这个工具不能直接发现内存泄露,而是作用辅助工具使用,关注内存的变化,可以和hprof配合使用。
这里我们还是以MainActivity泄露为例,先点击start allocation tracking,然后将手机屏幕翻转三次,再调用gc后的内存变化。

从图上可以看出,在翻转了三次屏幕后,MainActivity被创建了三次,而当我们调用了GC后,也是如此,也就说明了,allocation tracking只关注内存的一个最大变化。此时,我们就可以结合MAT来关注MainActivity的内存泄露情况了。
在Android Device Monitor中的Allocation Tracker也是同样的效果,不同的只是操作上,以及展示的信息。
LeakCanary
leakCanary是square开源的一款用于集成手机端来提醒内存泄漏的框架,只能用于观察Activity的泄露情况,会自动在消息通知栏,给出提示。
集成进app非常简单,先添加依赖:
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'
这里的no-op版本,说的是在测试编译或者正式版本编译不会被引入项目。
然后在Application中添加初始化,就开始正常工作了。
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
    }
}
当我翻转了两次屏幕后,就接收到了内存泄露的提示信息了,非常的好用。
