Android 源码系列之从源码的角度深入理解LeakCanary的内存泄露检测机制(中)

       转载请注明出处:http://blog.csdn.net/llew2011/article/details/52958563

       在上篇文章Android 源码系列之<十二>从源码的角度深入理解LeakCanary的内存泄露检测机制(上)中主要介绍了Java内存分配相关的知识以及在Android开发中可能遇见的各种内存泄露情况并给出了相对应的解决方案,如果你还没有看过上篇文章,建议点击这里阅读一下,这篇文章我将要向大家介绍如何在我们的应用中使用square开源的LeakCanary库来检测应用中出现的内存泄露,如果你已经对LeakCanary的使用非常熟悉了请跳过本文(*^__^*) ……

       以介绍来自英文LeakCanary: Detect all memory leaks!的翻译,原文在这里

java.lang.OutOfMemoryError
        at android.graphics.Bitmap.nativeCreate(Bitmap.java:-2)
        at android.graphics.Bitmap.createBitmap(Bitmap.java:689)
        at com.squareup.ui.SignView.createSignatureBitmap(SignView.java:121)

没人喜欢OutOfMemoryError

       在Square Register中,在签名页面我们把客户的签名画在bitmap cache上,这个Bitmap的尺寸几乎和屏幕的尺寸一样大,在创建这个Bitmap对象时,经常会引发OutOfMemoryError,简称OOM。

        当时,我们尝试过一些解决方案,但是都没解决问题:

  • 使用Bitmap.Config.ALPHA_8,因为签名仅有黑色。
  • 捕捉OutOfMemoryError,尝试GC并重试(受GCUtils启发)。
  • 我们没想过在Java Heap内存之外创建Bitmap,苦逼的我们,那会Fresco等库还不存在。

路子走错了

       其实Bitmap的尺寸不是真正的问题,当内存吃紧的时候,到处都有可能引发OOM,在创建大对象,比如Bitmap的时候,则更有可能引发OOM,OOM只是一个表象,更深层次的问题可能是:内存泄露。

什么是内存泄露

       一些对象有着有限的声明周期,当这些对象所要做的事情完成了,我们希望它们会被垃圾回收器回收掉。但是如果有一系列对这个对象的引用存在,那么在我们期待这个对象生命周期结束时被垃圾回收器回收的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

       比如:当Activity的onDestroy()方法被调用后,Activity以及它涉及到的View和相关的Bitmap都应该被回收掉。但是,如果有一个后台线程持有这个Activity的引用,那么该Activity所占用的内存就不能被回收,这最终将会导致内存耗尽引发OOM而让应用crash掉。

对战内存泄露

       排查内存泄露是一个全手工的过程,这在Raizlabs的Wrang Dalvik系列文章中有详细描述。以下几个关键步骤:

  1. 通过BugsnagCrashlytics或者Developer Console等统计平台,了解OutOfMemoryError情况。
  2. 重现问题。为了重现问题,机型非常重要,因为一些问题只在特定的设备上出现。为了找到特定的机型,你需要想尽一切办法,你可能需要去买,去借,甚至去偷。当然,为了确定复现步骤,你需要一遍一遍地去尝试。一切都是非常原始和粗暴的。
  3. 在发生内存泄露的时候,把内存Dump出来。具体看这里
  4. 然后,你需要在MAT或者YourKit之类的内存分析工具中反复查看,找到那些原本该被回收掉的对象。
  5. 计算这个对象到GC Roots的最短强引用。
  6. 确定应用路径中的哪个应用是不该有的,然后修复。

       很复杂吧?如果有一个类库能在发生OOM之前把这些事情全部搞定,然后你只要修复这些问题就好了,岂不美哉!!!这就是LeakCanary的由来,接下来我们就要介绍一下LeakCanary的使用。

       由于Google已经不再对Eclipse上开发Android做支持,所以就不再Eclipse上演示如何使用LeakCanary了,LeakCanary的官方网址为:https://github.com/square/leakcanary,根据指导文档,使用LeakCanary首先要引入,在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'
}
       在build.gradle中添加了LeakCanary的依赖后,点击 按钮同步一下工程后就可以使用LeakCanary了。细心的小伙伴可能会注意到我们引入了三种模式的依赖,debugCompile表示在debug打包模式下引入的依赖库,releaseCompile表示在release打包模式下引入的依赖库,testCompile表示的是在test打包模式引入的依赖库。总的来说就是在不同模式下使用不同的库,在项目打包的时候不会把其他模式的库打包进去。它们之间的区别稍后会有讲解。

       引入了LeakCanary的依赖库后,接下来就是使用它了,根据GitHub上的指导文档,首先在我们的Application中初始化LeakCanary,如下所示:

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);
    }
}
       在初始化之前先是调用LeakCanary的静态方法isInAnalyserProcess()做过滤,如果该方法返回true就直接返回否则就执行LeakCanary的install()方法进行初始化工作。这样就完成了LeakCanary的初始化操作,是不是很简单?接下来我们测试一个例子,看看LeakCanary是如何检测内存泄露的,在上篇文章 Android 源码系列之<十二>从源码的角度深入理解LeakCanary的内存泄露检测机制(上)中讲解了Android开发中常见的内存泄露情形(如果你还没有看过请点击 这里)。我们根据上篇文章任意举一例子:从当前MainActivity页面跳转到LeakActivity页面,在LeakActivity页面中模拟长耗时的任务,然后点击返回键返回到MainActivity页面,现在编写LeakActivity,代码如下:
public class LeakActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.leak_activity);
    }

    public void start(View view) {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        Log.e(getPackageName(), "LeakCanary ----->>>>> " + System.currentTimeMillis());
                    } catch (Exception e) {
                    }
                }
            }
        }.start();
    }
}
       在LeakActivity中,当点击了按钮后就会启动一个新的匿名内部类线程并在线程中模拟了长耗时的操作,根据上篇文章的讲解,匿名内部类会默认持有当前MainActivity的引用,当点击返回按钮后LeakActivity本应该由系统进行回收,但是内部启动的线程还在继续执行操作就造成了LeakActivity所占用的内存资源得不到释放,就会造成内存泄露。然后运行一下程序,看看效果:


       根据运行效果来看,在LeakActivity页面在点击按钮启动了线程后返回到主页面后,大约5秒钟后会弹出一个提示框,提示说在进行内存泄露检测,稍后你会发现在状态栏上弹出了一个Notification,我们展开这个Notification看看里边是啥东东:


       弹出的Notification大致说了包名为com.example.leakcanary下的LeakActivity发生了内存泄露,泄露的内存大约是108KB,如果想要查看更为详细的内存泄露信息,可以点击查看,然后我们点击进去看看详细的内存泄露信息,如下所示:


       内存泄露引用链清晰详细的列举了发生内存泄露的原因,根据这种引用关系我们可以很容易的定位到内存泄露点,然后修复这些泄露问题。怎么样?是不是很简单?从此可以对身边的小伙伴说:用了LeakCanary以后再也不怕内存泄露了(*^__^*) ……检测到LeakActivity发生了内存泄露,根据上篇文章的修复方法修改一下LeakActivity之后再运行程序就不会有Notification弹出了。

       LeakCanary的使用就是这么简单,如果你还没使用过它强烈建议使用一次,相信你用过之后就会离不开它的(*^__^*) ……在我们引入LeakCanary的时候我们引入了三种不同模式的库,他们之间肯定是有区别的,下载完它的源码后,其结构图如下所示:


       通过阅读LeakCanary的代码发现leakcanary-android是真正内存泄露分析库,而leakcanary-android-no-op只是一个空壳子,里边啥也没做。也就是说在打release包的时候LeakCanary库根本就不会打进去,所以不不用担心引入额外的方法,只有在debug或者test模式下LeakCanary库才会打包进APK。

       首先看一下leakcanary-android下的AndroidManifest.xml文件,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary">

    <!-- To store the heap dumps and leak analysis results. -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application>
        <service
            android:name=".internal.HeapAnalyzerService"
            android:enabled="false"
            android:process=":leakcanary" />
        <service
            android:name=".DisplayLeakService"
            android:enabled="false" />

        <activity
            android:name=".internal.DisplayLeakActivity"
            android:enabled="false"
            android:icon="@drawable/leak_canary_icon"
            android:label="@string/leak_canary_display_activity_label"
            android:taskAffinity="com.squareup.leakcanary"
            android:theme="@style/leak_canary_LeakCanary.Base">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".internal.RequestStoragePermissionActivity"
            android:enabled="false"
            android:icon="@drawable/leak_canary_icon"
            android:label="@string/leak_canary_storage_permission_activity_label"
            android:taskAffinity="com.squareup.leakcanary"
            android:theme="@style/leak_canary_Theme.Transparent" />
    </application>
</manifest>
       在配置文件中首先申请读写权限是为了存储堆内存信息到文件中便于在后台分析是否发生了内存泄露。接着是声明了两个Service,他们分别是HeapAnalyzerService和DisplayLeakService,HeapAnalyzerService添加了 android:process = ":leakcanary"属性,那也就是说HeapAnalyzerService是运行在单独的进程中的,这样做的目的是不影响APP进程(比如给APP进程造成卡顿等影响),因此在初始化LeakCanary的时候首先是调用了LeakCanary的静态方法isInAnalyzerProcess()方法判断当前进程是否是分析进程,如果是分析进程就不需要在分析进程中做APP进程需要做的初始化操作,所以就直接返回。最后声明了两个Activity,DisplayLeakActivity主要是以列表的形式展示出做有发生过的内存泄露点,点击列表中的每一条信息时会进入内存泄露的详情页面,RequestStoragePermissionActivity根据名字就知道它是用来申请存储权限的,需要注意的是DisplayLeakActivity和RequestStoragePermissionActivity都声明了 android:taskAffinity = "com.squareup.leakcanary"属性,这既是说它们是运行在新的TaskStack中的,如果你不熟悉taskAffinity属性,请看我之前写的一篇文章: Android 源码系列之<九>从源码的角度深入理解Activity的launchModel特性

       了解了LeakCanary的相关配置后我们再看一下它的相关资源文件:


       资源文件就不详细说明了,对于这些资源我们能做的就是如果不喜欢这些资源文件,我们可以在项目中把它替换掉。

       由于篇幅原因,这里就不再过多的分析LeakCanary的源码了,在下篇文章Android 源码系列之<十四>从源码的角度深入理解LeakCanary的内存泄露检测机制(下)中我将带领小伙伴们从源码的角度出发深入分析一下LeakCanary的内存泄露分析机制,敬请期待!!!最后感谢收看(*^__^*) ……



       【参考文章:】

       https://medium.com/square-corner-blog/leakcanary-detect-all-memory-leaks-875ff8360745#.klentg7g4






发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/52958563