LZ-Says:此生入鸡门,此生无憾~ 感谢阳阳当年在廊坊将我挖出来,谢谢~
❈
前言
最近在群里看到有人在讨论有关内存分析的话题,比较好奇,Enmmm,也就有了今天这篇博文。
一起学习,一起进步吧~
一、LeakCanary 简介
LeakCanary:用于检测所有内存泄漏,适用于 Android 和 Java 的内存泄漏检测库。
为毛要叫做这个呢?
LeakCanary 这个名称是煤矿中金丝雀描述,因为 LeakCanary 类似一个用于通过提前预警危险来检测风险的哨兵。
1. 官方述说,为毛我们要使用 LeakCanary?
原文博客地址见文末,有兴趣可自行查阅,这里列举部分内容。
The First:
没有人喜欢OutOfMemoryError崩溃
在Square Register中,我们在 bitmaps 缓存上绘制客户的签名。此 bitmaps 是设备屏幕的大小,创建它时我们有大量的内存不足(OOM)导致崩溃。
我们尝试了几种方法,但都没有解决问题:
- 使用Bitmap.Config.ALPHA_8(签名不需要颜色);
- 捕获OutOfMemoryError,触发GC 并重试几次(灵感来自GCUtils);
- 我们没有想到从Java堆中分配 bitmaps。幸运的是,Fresco还没有存在。
We were looking at it the wrong way
The bitmap size was not a problem. When the memory is almost full, an OOM can happen anywhere. It tends to happen more often in places where you create big objects, like bitmaps. The OOM is a symptom of a deeper problem: memory leaks.
bitmap 大小不是问题。当内存几乎已满时,OOM 可以在任何地方发生。它往往会在创建大对象(如 bitmap)的位置更频繁地发生。OOM 是一个更深层次问题的症状:内存泄漏。
什么是内存泄漏?
有些物体的寿命有限(在程序中,当某个对象已经使用完毕后,GC 则会对此进行回收)。当他们的工作完成后,他们将被当作垃圾回收。如果引用链在其预期生命周期结束后将对象保存在内存中,则会产生内存泄漏(也就是说,当 GC 回收时,由于某个对象依然具有将要回收值得引用,就会阻碍 GC 正常回收)。当这些泄漏累积时,应用程序则内存不足。
例如,在调用Activity.onDestroy()之后,Activity 其视图层次结构及其关联的位图应该都是可进行垃圾回收的。如果在后台运行的线程持有对活动的引用,则无法回收相应的内存。这最终导致 OutOfMemoryError ,以及最终的崩溃。
而我们又该如何收集内存泄漏?
收集并记录泄漏是一个手动过程,在Raizlabs的Wrangling Dalvik系列中有详细描述。
以下是关键步骤:
- 通过Bugsnag,Crashlytics 或 Developer Console 了解 OutOfMemoryError 崩溃;
- 尝试重现问题。可能需要购买,借用或窃取(手机)遭受崩溃的特定设备信息。(并非所有设备都会出现所有泄漏!)还需要弄清楚导航泄漏的导航顺序,可能是纯粹暴力方式;
- 在OOM发生时转储堆;
- 使用MAT或YourKit在堆转储周围查找并找到应该被垃圾回收的对象;;
- 计算从该对象到GC根的最短强引用路径。
- 找出路径中哪个引用不应该存在,并修复内存泄漏。
如果一个库可以在你进入OOM之前完成所有这些,并让你专注于修复内存泄漏怎么办?
这样岂不是让我们很爽么?
So,我们的 LeakCanary 应用而生了~
2. Enmmm,我怎么用它呢?
最简单的选择是调用 LeakCanary.install(this); ,它会安装一个 ActivityRefWatcher,从而自动检测 Activity 在 Activity.onDestroy() 被调用后是否泄漏。
代码语言:javascript复制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);
// Normal app init code...
}
}
Enmmm,那假如我想监听:具有生命周期的对象,例如片段,服务,Dagger组件等怎么破?
很 Easy 啊,直接使用一个 RefWatcher 来监听应该进行垃圾回收的引用即可:
代码语言:javascript复制RefWatcher refWatcher = {...};
// We expect schrodingerCat to be gone soon (or not), let's watch it.
refWatcher.watch(schrodingerCat);
LeakCanary.install() 返回一个预先配置好的 RefWatcher,如下:
代码语言:javascript复制public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@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;
}
refWatcher = LeakCanary.install(this);
}
}
So,我们可以使用 RefWatcher 来监视 Fragment 泄漏:
代码语言:javascript复制public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
3. 它又是如何工作的?
- RefWatcher.watch() 为被监视对象创建 KeyedWeakReference;
- 稍后,在后台线程中,它会检查引用是否已被清除,如果没有,则会触发GC;
- 如果仍未清除引用,则它会将堆转储到 .hprof 存储在文件系统上的文件中;
- HeapAnalyzerService 在单独的进程中启动并 HeapAnalyzer 使用 HAHA 解析堆转储;
- HeapAnalyzer 发现 KeyedWeakReference 堆转储由于唯一的参考键和定位的泄漏引用;
- HeapAnalyzer 计算到 GC 根的最短的强引用路径,以确定是否存在泄漏,然后构建导致泄漏的引用链;
- 结果将传递回 DisplayLeakService 应用程序进程,并显示泄漏通知。
4. 官方不好用,我要自定义
这里首先要注意:
使用 no-op 依赖
确保发布版本的 leakcanary-android-no-op 依赖项仅包含 LeakCanary 和 RefWatcher类。 如果开始自定义 LeakCanary,需要确保自定义仅在调试版本中发生,因为它可能会引用 leakcanary-android-no-op 依赖项中不存在的类异常。
假设发布版本在 AndroidManifest.xml 中声明了一个 ExampleApplication 类,并且调试版本声明了一个扩展 ExampleApplication 的 DebugExampleApplication,那么,在 Application 中,你应该进行如下操作:
代码语言:javascript复制public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// 此过程专用于 LeakCanary 进行堆分析。
// 不应该在此过程中初始化应用。
return;
}
refWatcher = installLeakCanary();
}
protected RefWatcher installLeakCanary() {
return RefWatcher.DISABLED;
}
}
如果你想在 Debug 模式下操作,那么你应该注意如下:
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
@Override protected RefWatcher installLeakCanary() {
// 构建自定义的RefWatcher
RefWatcher refWatcher = LeakCanary.refWatcher(this)
.watchDelay(10, TimeUnit.SECONDS)
.buildAndInstall();
return refWatcher;
}
}
这样,除了 leakcanary-android-no-op 依赖项中存在的两个空类之外,发布代码将不包含对 LeakCanary 的引用。
Step 1:我想修改图标和提示怎么办?
DisplayLeakActivity 附带一个默认图标和提示,可以通过提供 R.drawable.leak_canary_icon 和 R.string.leak_canary_display_activity_label 在应用中更改:
代码语言:javascript复制res/
drawable-hdpi/
leak_canary_icon.png
drawable-mdpi/
leak_canary_icon.png
drawable-xhdpi/
leak_canary_icon.png
drawable-xxhdpi/
leak_canary_icon.png
drawable-xxxhdpi/
leak_canary_icon.png
以及对应提示:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="leak_canary_display_activity_label">MyLeaks</string>
</resources>
Step 2:我想修改存储泄漏痕迹数量怎么办?
由于 LeakCanary 最多可以保存 7 个堆转储信息。So,如果改变这种情况,按照如下姿势即可:
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
protected RefWatcher installLeakCanary() {
RefWatcher refWatcher = LeakCanary.refWatcher(this)
.maxStoredHeapDumps(42)
.buildAndInstall();
return refWatcher;
}
}
Step 3:我想修改将这些信息上传服务器怎么办?
创建自己的 AbstractAnalysisResultService。最简单的方法是在调试源中扩展 DisplayLeakService:
代码语言:javascript复制public class LeakUploadService extends DisplayLeakService {
@Override protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
if (!result.leakFound || result.excludedLeak) {
return;
}
myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
}
}
在调试应用程序类中构建自定义 RefWatcher:
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
@Override protected RefWatcher installLeakCanary() {
RefWatcher refWatcher = LeakCanary.refWatcher(this)
.listenerServiceClass(LeakUploadService.class);
.buildAndInstall();
return refWatcher;
}
}
不要忘记在 AndroidManifest.xml 中注册该服务:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application android:name="com.example.DebugExampleApplication">
<service android:name="com.example.LeakUploadService" />
</application>
</manifest>
Step 4:我想修改忽略已知内存泄漏的引用怎么办?
可以创建自己的 ExcludedRefs 版本,以忽略知道导致泄漏的特定引用,但我们仍然要进行如下设置:
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
@Override protected RefWatcher installLeakCanary() {
ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
.instanceField("com.example.ExampleClass", "exampleField")
.build();
RefWatcher refWatcher = LeakCanary.refWatcher(this)
.excludedRefs(excludedRefs)
.buildAndInstall();
return refWatcher;
}
}
Step 5:我不想看特定的 Activity 类怎么办?
默认情况下安装 ActivityRefWatcher 并监视所有活动。当然可以自定义安装步骤以使用不同的东西:
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
@Override protected RefWatcher installLeakCanary() {
LeakCanary.enableDisplayLeakActivity(this);
RefWatcher refWatcher = LeakCanary.refWatcher(this)
// Notice we call build() instead of buildAndInstall()
.build();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
public void onActivityDestroyed(Activity activity) {
if (activity instanceof ThirdPartyActivity) {
return;
}
refWatcher.watch(activity);
}
// ...
});
return refWatcher;
}
}
Step 6:我想在运行时打开和关闭 LeakCanary 怎么办?
自定义RefWatcher的创建方式,并为其提供有时候会执行 no-op 的 HeapDumper。
代码语言:javascript复制public class DebugExampleApplication extends ExampleApplication {
TogglableHeapDumper heapDumper;
@Override protected RefWatcher installLeakCanary() {
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
AndroidHeapDumper defaultDumper = new AndroidHeapDumper(context, leakDirectoryProvider);
heapDumper = new TogglableHeapDumper(defaultDumper);
RefWatcher refWatcher = LeakCanary.refWatcher(this)
.heapDumper(heapDumper)
.buildAndInstall();
return refWatcher;
}
public static class TogglableHeapDumper implements HeapDumper {
private final HeapDumper defaultDumper;
private boolean enabled = true;
public TogglableHeapDumper(HeapDumper defaultDumper) {
this.defaultDumper = defaultDumper;
}
public void toggle() {
enabled = !enabled;
}
@Override public File dumpHeap() {
return enabled? defaultDumper.dumpHeap() : HeapDumper.RETRY_LATER;
}
}
}
5. 如何挖掘泄漏痕迹?
有时泄漏跟踪是不够的,还需要使用 MAT 或 YourKit 挖掘堆转储。以下是在堆转储中找到泄漏实例的方法:
- 寻找所有的实例 com.squareup.leakcanary.KeyedWeakReference;
- 对于其中的每一个,请查看该 key 字段;
- 找到 KeyedWeakReference 具有 key 等于 LeakCanary 报告引用键的字段;
- 那个 referent 引用的 KeyedWeakReference 是你泄漏的对象;
- 从那时起,问题就掌握在你手中。一个好的开始首先先查看 GC Roots 的最短路径(不包括弱引用)。
6. 如何在测试中禁用 LeakCanary?
要在单元测试中禁用 LeakCanary,请将以下内容添加到 build.gradle 即可:
代码语言:javascript复制// Ensure the no-op dependency is always used in JVM tests.
configurations.all { config ->
if (config.name.contains('UnitTest')) {
config.resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
}
}
}
}
7. 常见异常以及解决方案
- 如何修复构建错误? 如果 leakcan-android 不在 Android Studio 的外部库列表中,但是泄漏分析器和泄漏监视器就在那里:尝试做一个Clean Build。 如果仍然存在问题,请尝试从命令行构建。。 error: package com.squareup.leakcanary does not exist: 如果你有其他构建类型比 debug 和 release,你需要为它们(xxxCompile)添加特定的依赖。
- 构建错误:无法解决 如果在 Android Studio 处于脱机工作模式时添加 LeakCanary 依赖项,则会发生这种情况。打开 Preferences > Build, Execution, Deployment > Build Tools > Gradle 并取消选中离线工作。
- Instant Run 可以触发无效泄漏 启用Android Studio的 Instant Run 功能可能会导致LeakCanary报告无效的内存泄漏。So,关闭吧,兄dei~
- 明知道有泄漏。为什么通知不显示? 首先确认是否附加到调试器?LeakCanary 会在调试时忽略泄漏检测以避免误报。
并且,我们需要注意:
LeakCanary 只应在调试版本中使用,并应在发布版本中禁用。 因为,专门为发布版本提供了一个特殊的空依赖项:leakcanary-android-no-op。
LeakCanary的完整版本更大,绝不应在发布版本中发布使用。
8. 发现彩蛋
- Android SDK可能导致泄漏吗? 是。在AOSP以及制造商实现中,已经存在许多已知的内存泄漏。当发生这样的泄漏时,作为应用程序开发人员,我们几乎无法解决此问题。出于这个原因,LeakCanary 有一个内置的已知 Android 漏洞列表可供忽略:AndroidExcludedRefs.java。
如果找到新的问题,请创建问题并按照以下步骤操作:
二、来波实战~
添加依赖项:
代码语言:javascript复制debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
在 Application 中添加 LeakCanary:
代码语言:javascript复制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);
// Normal app init code...
}
}
现在运行一波你的项目。
首先查看我们桌面:
接着打开 Apk,正常运行,发现如下弹框提示:
Enmmm,一般通知栏也会有提示信息(此处需要注意,有些设备隐藏在不重要通知中,需要单独点开查看):
接下来打开 Leaks 这个小程序:
Enmmm,发生泄漏了,好尴尬。。。
点击查看详情,查看泄漏堆栈信息:
三、关于内存泄漏了怎么办?
如上例子,我们可以从内存泄漏堆栈中发现,最终的泄漏源发生在腾讯 IM 中,那么针对这些第三方 SDK 导致泄漏,我们又该如何操作呢?
下面 LZ 简单附上几条建议:
- 官方查看最新的 SDK 版本更新说明,查看官方是否修复了此项内存泄漏;
- 检测自身代码编写问题,看看是否由于自身操作有误,导致内存泄漏?
- Enmmm,实在没辙,提交工单,附上初始化过程以及发生内存泄漏场景,最好把对应的详细内存堆栈附上,好方便对方开发人员定位并解决问题。
结束语
最后,感谢各位观看~!!!
如有不足之处,欢迎沟通~~~
我是贺利权,为自己代言~
欢迎各位老铁关注~不定期发布~见证你我的成长路~!!!
觉得不错,动动小手,转发让更多人看到,3Q,比心~