Android性能优化之内存泄漏优化

图1

貌似很久很久没写博客了,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.容器的不当使用,造成的内存泄露

上面的MessageQueueMessage持有对Handler的引用,也是因为容器不当持有需要被销毁对象的引用,而造成的内存泄露。

多见于观察者模式下,没有及时移除引用的情况下,导致的内存泄露。

解决方法:

在相应的生命周期内移除容器内的资源。比如:在onDestory中,移除容器内部对该Activity的引用。

4.内部类的内存泄露

还是同样的意思,当外部类需要被销毁时,内部类还有任务在执行,而无法被销毁时,任务引用了内部类,而内部类又引用了外部类,导致外部类无法被回收,而造成的内存泄露。

解决方法

将内部类使用static修辞。

5.资源未正常关闭造成的内存泄露

对于使用了BraodcastReceiverContentObserverFileCursorStreamBitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

解决方法
正常关闭相应资源。

……

3.常用的内存泄露分析工具

  1. Android Monitor
  2. MAT
  3. LeakCanary
  4. Allocation tracking

Android Monitor

Android MonitorAndroid Studio中默认的性能优化工具。它提供了对Memory,CPU,Network,GPU的监视。我们今天主要是用来对Memory的监控。

以单例模式下Context泄漏为例子来说下其使用.

多次翻转屏幕后,造成MainActivity的多次回收和创建,然后,按下Initiate GC按钮后,再按dump java heap按钮来观察当前heapMainActivity的情况。
img

如图所示,上面出现了两个MainActivity的实例,我们都知道,如果在翻转屏幕的时候,如果没有在Androidmainfest.xml中配置横竖屏不敏感的话,当前Activity会被销毁,而新的Activity会被创建,也就是说,正常情况是不会出现两个Activity的实例的,所以,说明是出现了内存泄漏。

这里可以通过包结构的方式来看相关的泄露情况,更加的直观方便。
img
图上的信息是通过多次GC之后,再dump出来的。从图上可以看到存在两个MainActivity的对象,说明MainActivity存在泄露,而为什么下面会多了一个MainActivity$1,而且个数正好也是两个呢?

其实,这里的MainActivity$1只是MainActivity对象的引用,这一个可以从MainActivity$1中的this所指向的内存地址正好是MainActivity的两个地址。

MAT

MAT(memory Analyzer tool):内存分析工具,这个是曾经在Eclipse时代,非常好用的一款内存分析工具,而且有单独版本的。我们这里就使用单独版本的来说明一下其使用方法。

同样的,我们使用Android studiodump.hprof格式的文件,然后使用hprof-conv 源文件 目标文件如:hprof-conv 1.hropf 2.hprof来生成MAT可以使用的目标文件2.hprof

文件转换完成后,我们导入文件,导入的时候选择内存泄露分析。
img

这里我主要说两个我们常用的功能,一个是histogram功能,还有一个就是内存对比。还是以上面MainActivity泄露的问题来分析。

打开histogram功能后,就看到了下面的结果:
img
像上面的东西,我们基本上是看不到任何有价值的东西的,这时,需要我们自己去搜索感兴趣的对象,看其存在的情况了。比如,我们可以搜索MainActivity大小写区分,来看看MainActitivy在内存中的存在情况。

img

图上可以看出,存在两个MainActivity对象,而其占用的内存大小为528B.接下来我们要看看是谁引用了我们的引用。
img

查看列表,我们发现MainActivityCommonUtils中的mContext引用着。
img

我们使用merge shortest path(最短路径)也可以看出,其被CommonUtils引用着。
imgimg

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

下面,我们来说说,通过内存映射对比来分析其内存泄露情况。
分别打开new27new28两个.hprof文件,然后,分别打开histogram功能。
img

分别将histogram添加到basket中。
img

添加完成后,要点击一下执行结果,才会出现对比的结果。img

看到下面的内存对比的情况,这里new27new28分别表示无内存泄露和内存泄露。img

通过搜索MainActivity,我们可以看出#0出现了内存泄露的情况。img

Allocation Tracking(分配内存追踪)

这个工具主要是用来分析内存的变化情况,比如:你打开了一个新的页面,此时,新的页面创建了多少对象,哪些对象被创建了多少次都会被追踪到,但如果调用gc,被回收的对象,也会显示在内存变化中,也就是它关心的是这段时间内,内存的变化,只做加法,也就是只记录最大的变化数,而被回收的对象,也会显示在列表中。这个工具不能直接发现内存泄露,而是作用辅助工具使用,关注内存的变化,可以和hprof配合使用。

这里我们还是以MainActivity泄露为例,先点击start allocation tracking,然后将手机屏幕翻转三次,再调用gc后的内存变化。imgimg

从图上可以看出,在翻转了三次屏幕后,MainActivity被创建了三次,而当我们调用了GC后,也是如此,也就说明了,allocation tracking只关注内存的一个最大变化。此时,我们就可以结合MAT来关注MainActivity的内存泄露情况了。

Android Device Monitor中的Allocation Tracker也是同样的效果,不同的只是操作上,以及展示的信息。img

LeakCanary

leakCanarysquare开源的一款用于集成手机端来提醒内存泄漏的框架,只能用于观察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);
    }
}

当我翻转了两次屏幕后,就接收到了内存泄露的提示信息了,非常的好用。imgimg