Android内存泄漏的检测工具——LeakCanary

首先了解什么是内存泄露

1Leakcancary的优势
LeakCanary是一个可视化的内存泄露分析工具,他具备以下优势

· 简单:只需设置一段代码即可,打开应用运行一下就能够发现内存泄露。而MAT分析需要Heap Dump,获取文件,手动分析等多个步骤。

· 易于发现问题:在手机端即可查看问题即引用关系,而MAT则需要你分析,找到Path To GC Roots等关系。

· 人人可参与:开发人员,测试测试,产品经理基本上只要会用App就有可能发现问题。而传统的MAT方式,只有部分开发者才有精力和能力实施。

 

 

2. 使用说明
2.1 在build.gradle中添加依赖

首先,在必须在对应的app模块的gradle添加对应的库,在其他module模块添加无效

dependencies {

debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5.4’
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.4’

}

到这里,就添加了对他的依赖。这里说明一下,使用其他版本可能会因为版本问题导致报错,建议使用*新版本,也就是这个1.5.4

2.2 在application中配置

public class BaseApplication extends Application {

private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();//2
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);//1
}

public static RefWatcher getRefWatcher(Context context) {
BaseApplication leakApplication = (BaseApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}
在注释2处,,完成对LeakCancary的安装,经过以上两个步骤,你的手机界面出现

%title插图%num

这个黄色的图标就是我们的监控工具

 

2.3 检测activity内存泄漏问题,原理是application中监控着所有activity生命周期在activity的onDestory中

@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = BaseApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}

Activity生命周期结束的时候,如果你的activity发生内存泄漏,状态栏会提示你

%title插图%num

那么,黄色的应用  Leaks中

%title插图%num

会把内存泄漏的详情告诉你,像这里就是因为我的TestActivity中开了LeakThread这个线程,造成了内存泄漏

泄漏内存是115kb

 

2.4 监控的对象当我们需要对某个对象进行监控时,

BaseApplication.getRefWatcher().watch(sleaky)

其中sleaky就是我们要检测的对象

那么,哪些是需要我们检测的对象呢

默认情况下,是对Activity进行了检测。另一个需要监控的重要对象就是Fragment实例。因为它和Activity实例一样可能持有大量的视图以及视图需要的资源

其他也可以监控的对象

BroadcastReceiver ,Service, 其他有生命周期的对象,直接间接持有大内存占用的对象(即Retained Heap值比较大的对象)

何时进行监控
首先,我们需要明确什么是内存泄露,简而言之,某个对象在该释放的时候由于被其他对象持有没有被释放,因而造成了内存泄露。

因此,我们监控也需要设置在对象(很快)被释放的时候,如Activity和Fragment的onDestroy方法。

一个错误示例,比如监控一个Activity,放在onCreate就会大错特错了,那么你每次都会收到Activity的泄露通知。

 

 

 

 

以上就是LeakCancary的使用方法

3.LeakCanary的原理
Android 应用的整个生命周期由其组件的生命周期组成,如下图中所示。用户使用应用的过程中,在不同界面之间跳转,每个界面都经历着”生死“的转换,可在此建立检测点。Activity/Fragment 都有 onDestory() 回调方法, 进入此方法后,Activity/Fragment生命周期结束,应该被回收。

 

简述声明周期

然后我们需要解决:如何得到未被回收的对象。ReferenceQueue+WeakReference+手动调用 GC可实现这个需求。

WeakReference 创建时,传入一个 ReferenceQueue 对象。当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中,待ReferenceQueue处理。当 GC 过后对象一直不被加入 ReferenceQueue,它可能存在内存泄漏。

 

获得未被回收的 Object

找到了未被回收的对象,如何确认是否真的内存泄漏?这里可以将问题转换为:未被回收的对象,是否被其他对象引用?找出其*短引用链。VMDebug + HAHA 完成需求。

VM 会有堆内各个对象的引用情况,并能以hprof文件导出。HAHA 是一个由 square 开源的 Android 堆分析库,分析 hprof 文件生成Snapshot对象。Snapshot用以查询对象的*短引用链。

 

解析hprof

找到*短引用链后,定位问题,排查代码将会事半功倍。

关于 kotlin 协程 lifecycleScope 用法和内存泄漏的问题

在随便一个 Activity 上启动此 Activity,然后迅速关闭,leakcanary 就会报内存泄漏。引用链包括了传入的第二个参数 lambda 对象和 Okhttpclient,这里泄漏的原因是什么呢? 一般 retrofit 或者 okhttpclient 对象全局只需要一个就行了吧, 如果还是需要传参和传回调的方式访问网络,该如何正确修改下面的代码呢?

class TestActivity:AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        requestBylifecycleCoroutine("https://www.baidu1.com/"){
            Log.w("TestActivityTAG","result:"+it)
        }
    }

    val client = OkHttpClient.Builder().build()
    fun requestBylifecycleCoroutine(url: String, callBack: (s: String) -> Unit) {
        lifecycleScope.launch {
            val result = withContext(Dispatchers.IO) {
                suspendCoroutine<String> { continuation ->
                    val request = Request.Builder().url(url).build()
                    val newCall = client.newCall(request)
                    newCall.enqueue(object : Callback {
                        override fun onFailure(call: Call, e: IOException) {
                            continuation.resume("fail")
                        }
                        override fun onResponse(call: Call, response: Response) {
                            continuation.resume("success")
                        }
                    })
                }
            }
            callBack(result)
        }
    }
}

21 条回复    2021-01-13 10:25:26 +08:00
lianyue13
    1

lianyue13   92 天前

协程取消的时候,请求没取消吧。用 suspendCancellableCoroutine,在 cancel 里把请求取消试试
m30102
    2

m30102   92 天前

@lianyue13 我换了 suspendCancellableCoroutine, 也在 invokeOnCancellation 添加了 call.cancel(). 和之前一样,leakcanary 还是会有 x retained objects,tap to dump heap,不过很快通知就变成了 All retained objects were garbage collected . 这样是为什呢,我还需要担心吗?
k10ndike
    3

k10ndike   92 天前

@m30102 OKHttpClient 对象是 Activity 持有的么?
hlayk
    4

hlayk   92 天前

如果在 viewmodel 中使用对应的 viewModelScope 那么携程 launch 返回的 job 会由 viewmodel 的 onCleared 统一自动 cancel
你在 activity 中这样使用 lifecycleScope 可以在 onDestory() 将 lifecycleScope.launch {} 返回的对象 job 主动取消下

[Easy Coroutines in Android: viewModelScope]( https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471)

m30102
    5

m30102   92 天前

@k10ndike OKHttpClient 对象就算写在其他类中,同样是单例的话, 也会间接持有到 activity
m30102
    6

m30102   92 天前

@hlayk 如果还需要考虑 onDestory,那么 lifecycleScope 就不用叫 lifefcycleScope 了。实际上 lifefcycleScope 也确实自动 cancel 了,*终的 callBack 没有执行。但是网络请求不一定能成功 cancel,而且回调时间较长,leakcanary 不知为什么显示引用到了 activity 。 如果替换为 viewModelScope 也有效,有时候一个页面就一个网络请求懒得再写一个类直接在 activity 中请求网络,这样貌似 无解?
k10ndike
    7

k10ndike   92 天前

@m30102 呃,Activity 怎么间接持有到的?有测试过吗,创建一个类里面就一个静态的 OkHttpClient 实例
vanxy
    8

vanxy   92 天前 via iPad

你这个等于用了一半的协程,用错了
应该同步地在协程里调用 okhttp,而不是通过回调 callback 的方式

直接 newCall.execute() 拿到 response

m30102
    9

m30102   92 天前

@vanxy 同步的我试了,还是一样。同步的话只是自动取消协程,但是 call.execute()方法开始执行后并不会立即取消。
m30102
    10

m30102   92 天前

@k10ndike 测试过, static 的 OkHttpClient *终会通过 activity 中传入的 lambda 回调,引用到 activity
vanxy
    11

vanxy   91 天前 via iPad

@m30102 你需要在 execute 外面包一个 withContext,这样当 activity finish 了,就不会执行接下来的代码了
sankemao
    12

sankemao   91 天前

我觉得应该是 callback 持有了 activity 的引用,可以试试把结果传给 livedata
m30102
    13

m30102   91 天前

@vanxy 是的,我有用 withContext. 无论是 execute 还是 enqueue ,activity finish 后传的 callback 不会执行,但是 okhttp 的 call 还是会执行的。
m30102
    14

m30102   91 天前

@sankemao liveData 一般配合 viewmodel 用吧,难道非得 mvp 或者 mvvm 把 activity 完全隔开才行吗。。。。
sankemao
    15

sankemao   91 天前 via iPhone

@m30102 你可以在 ac 里面用 livedata 验证下是否还有泄漏。你也可以用其他办法
vanxy
    16

vanxy   91 天前

@m30102 #13 那是自然地呀,call 是一整段的阻塞式调用。 不止是协程, 实际上没有任何方式可以取消它(除非强制停止线程),协程只是可以帮你在取消 withContext 之后的所有代码执行。

因为不执行接下来的 callback 了, 所以 activity 也不会内存泄露了。 也不需要关心 execute 是否执行完成

m30102
    17

m30102   91 天前

@vanxy 我反编译了半天对比了下找到原因了, 是 okhttpclient 的原因,activity 虽然执行 destory 了,但是 okhttpclient 还在执行 call,所以延长了 activity 生命,报泄漏。如果把 okhttpclient 写在其他类中声明 static, 那么 activity 中调用协程方法传的 callBack 必须不能引用 activity 任何成员变量或者 view 等,不然还是会被延长生命,一般传回调就是为了改变 view 等,所以这个是无解的!
vanxy
    18

vanxy   91 天前

@m30102 #17
哦哦, 因为 kotlin 的 lambda 持有了 activity 的引用。

所以要把执行的放到 viewmodel 或者 presenter 里,activity 解除与 viewmodel 或者 presenter 之间的引用之后, 就不会造成泄露了

k10ndike
    19

k10ndike   91 天前 via Android

@m30102 你本地的代码应该有其他操作吧,帖子里那样打 Log 应该不会泄露。可以在请求 callback 里调用 livedata,就不用处理生命周期了
xhpan10
    20

xhpan10   88 天前

协程的代码没有 rxjava 的好看
DiDiz
    21

DiDiz   87 天前

@xhpan10 也要封装的,封装好了协程代码就和普通同步代码一样。相比之下 rxjava 就显得很啰嗦了