关闭

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

标签: android源码LeakCanary内存泄露
4259人阅读 评论(0) 收藏 举报
分类:

       转载请注明出处: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






0
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

LeakCanary核心原理源码浅析

网上大牛太多,不敢说分析,也不敢装成大大,所以只能是浅析… 那么今天这篇主要解决什么问题呢?其实就一个问题,LeakCanay.install(this)这个函数到底是怎么走的,用测试的话说就是数据流是怎么走的,用探索性测试的方法说就是:快递法。 LeakCanay项目的地址是:https:/...
  • Cloud_Huan
  • Cloud_Huan
  • 2016-11-08 13:00
  • 5764

LeakCanary源码分析

LeakCanary是一个监测内测泄露的工具,源码地址 https://github.com/square/leakcanary Android使用方法: 在build.gradle文件中添加 dependencies { debugCompile 'com.squareup.le...
  • yamingpeng
  • yamingpeng
  • 2016-07-08 21:26
  • 456

LeakCanary源码分析第二讲-RefWatcher详解

LeakCanary源码分析第二讲-RefWatcher详解 如果你已经阅读了LeakCanary源码分析第一讲,那么LeakCanary的基本架构应该已经掌握了。本文将详细分析RefWatcher的工作原理,当RefWatcher检查到引用路径不是弱通路的时候就会触发He...
  • xiaoqietingyu
  • xiaoqietingyu
  • 2016-07-03 17:23
  • 915

LeakCanary原理分析

导语: 提到Java语言的特点,无论是教科书还是程序员一般都会罗列出面向对象、可移植性及安全等特点。但如果你是一位刚从C/C++转到Java的程序员,对Java语言的特性除了面向对象之外,最外直接的应当是在Java虚拟机(JVM)在内存管理方面给我们变成带来的便利。JVM的这一大特性使Java...
  • aptentity
  • aptentity
  • 2017-05-07 10:42
  • 1043

利用 LeakCanary 来检查 Android 内存泄漏 6.0以上版本空指针解决

LeakCanary  是一个开源的在debug(Relese)版本中检测内存泄漏的java库,链接: https://github.com/square/leakcanary 你也许会遇到如下类似的如下错误 1.3.1 在6.0预览版报错 * FAILURE: java....
  • lzh7752
  • lzh7752
  • 2017-04-09 20:38
  • 1214

LeakCanary核心原理源码浅析

网上大牛太多,不敢说分析,也不敢装成大大,所以只能是浅析… 那么今天这篇主要解决什么问题呢?其实就一个问题,LeakCanay.install(this)这个函数到底是怎么走的,用测试的话说就是数据流是怎么走的,用探索性测试的方法说就是:快递法。 LeakCanay项目的地址是:https:/...
  • Cloud_Huan
  • Cloud_Huan
  • 2016-11-08 13:00
  • 5764

LeakCanary源码分析

LeakCanary是一个监测内测泄露的工具,源码地址 https://github.com/square/leakcanary Android使用方法: 在build.gradle文件中添加 dependencies { debugCompile 'com.squareup.le...
  • yamingpeng
  • yamingpeng
  • 2016-07-08 21:26
  • 456

工作中遇到的Android内存优化问题(3)-leakcanary源码解析

今天我们来看一下一个内存泄露检测神器 leakcanary(https://github.com/square/leakcanary) 首先我们来看一下leakcanary的使用说明 就这么多,只需要一行代码,太简单了,简单得都有点怀疑它了。 我们来看一下一个简单的例子,也是它官方源码中提供的一...
  • u011291205
  • u011291205
  • 2016-09-10 23:51
  • 4499

LeakCanary源码分析

LeakCanary是一个Android内存泄露自动分析工具,具有简单易用,结果可读性强,不仅适用于Android开发人员,也适用于测试人员使用。能快速提高软件质量。
  • bazhongren
  • bazhongren
  • 2016-03-17 19:37
  • 578

Github项目解析(三)-->Android内存泄露监测之leakcanary

(一)什么是内存泄露 Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。(二)什么是leakcanary LeakCanary 是一个...
  • qq_23547831
  • qq_23547831
  • 2016-01-18 15:52
  • 4825
    个人资料
    • 访问:142674次
    • 积分:1835
    • 等级:
    • 排名:千里之外
    • 原创:34篇
    • 转载:0篇
    • 译文:0篇
    • 评论:89条
    最新评论