本文主要分析內(nèi)存泄漏的檢測原理和如何實現(xiàn)生產(chǎn)環(huán)境應(yīng)用,代碼分析基于Leakcanary 1.6版本。
如何檢測內(nèi)存泄漏
要想搞懂如何檢測內(nèi)存泄漏,有一個基本知識點需要知道:
| 引用類型 | 被垃圾回收時間 | 用途 | 用途 |
|---|---|---|---|
| 強引用 | 從來不會 | 對象的一般狀態(tài) | JVM停止運行時終止 |
| 軟引用 | 當(dāng)內(nèi)存不足時 | 對象緩存 | 內(nèi)存不足時終止 |
| 弱引用 | 正常垃圾回收時 | 對象緩存 | 垃圾回收后終止 |
| 虛引用 | 正常垃圾回收時 | 跟蹤對象的垃圾回收 | 垃圾回收后終止 |
這里我們需要用到弱引用,因為弱引用符合我們檢測的場景,就是當(dāng)內(nèi)存充足的時候觸發(fā)的系統(tǒng)gc,弱引用對象也能被回收,這樣有利于我們觀察對象回收情況,并且弱引用在初始化的時候,有如下構(gòu)造方法:
WeakReference(T referent, ReferenceQueue<? super T> q)
其中第二個參數(shù)是個隊列,當(dāng)對象被回收后會被放入該隊列中,我們就可以通過在activity或者fragment onDestroy的時候,主動觸發(fā)系統(tǒng)gc,然后查看隊列中是否有該對象,如果沒有,我們就認(rèn)為發(fā)生了一次內(nèi)存泄漏。我們看下leakcanry的具體實現(xiàn):
public static void install(Context context, RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
};
通過application的registerActivityLifecycleCallbacks監(jiān)聽所有activity的生命周期。接下去再看下在onActivityDestoryed中做了什么:
public void watch(Object watchedReference, String referenceName) {
.....
final long watchStartNanoTime = System.nanoTime();
// 隨機生成一個key
String key = UUID.randomUUID().toString();
// key放入Set<String>
retainedKeys.add(key);
// new 一個包含key信息的弱引用對象
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
// 異步監(jiān)測
ensureGoneAsync(watchStartNanoTime, reference);
}
// 啟動異步監(jiān)測
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
// 更新retainedKeys,把存在queue中回收掉的key在retainedKeys去除掉
removeWeaklyReachableReferences();
// 如果當(dāng)前應(yīng)用的key已經(jīng)不在retainedKeys中,說明引用在之前就被gc了
if (gone(reference)) {
return DONE;
}
// gc
gcTrigger.runGc();
// 再次更新retainedKeys
removeWeaklyReachableReferences();
// 這個時候引用的key還在retainedKeys,說明引用已經(jīng)內(nèi)存泄漏
if (!gone(reference)) {
......dump操作
}
return DONE;
}
以上是acitivty的監(jiān)測方法,接下去我們看下fragment的相關(guān)流程。
// 1、install
public static void install(Context context, RefWatcher refWatcher) {
List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
if (SDK_INT >= O) {
// os包下面的Fragment
fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
}
try {
// v4包下面的Fragment,這邊用反射因為類放在另外一個module下
Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
Constructor<?> constructor = fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
FragmentRefWatcher supportFragmentRefWatcher = (FragmentRefWatcher) constructor.newInstance(refWatcher);
fragmentRefWatchers.add(supportFragmentRefWatcher);
} catch (Exception ignored) {
}
if (fragmentRefWatchers.size() == 0) {
return;
}
Helper helper = new Helper(fragmentRefWatchers);
Application application = (Application) context.getApplicationContext();
// 添加Activity的生命周期回調(diào)
application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
}
// 2、在activity的onActivityCreated生命周期調(diào)用的時候添加檢測fragment邏輯
private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 遍歷os包和v4包下所有的watcher
for (FragmentRefWatcher watcher : fragmentRefWatchers) {
// 在onActivityCreated的時候給這個activity添加檢測fragment邏輯
watcher.watchFragments(activity);
}
}
};
// 3、給activity添加檢測fragment的邏輯
@Override public void watchFragments(Activity activity) {
FragmentManager fragmentManager = activity.getFragmentManager();
// 添加遞歸回調(diào)
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
}
// 4、 在fragment的生命周期中檢測
private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
// 在fragment的生命周期中檢測
refWatcher.watch(fragment);
}
};
接下去具體的watch邏輯就和activity的一模一樣,這里就不多贅述了。
生產(chǎn)環(huán)境應(yīng)用
如果把leakcanary直接搬到線上的話有有以下幾個問題需要解決:
- dump出來的文件有100M左右甚至更大
-
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());dump方法會凍結(jié)應(yīng)用所有進程,持續(xù)數(shù)秒到數(shù)十秒的卡頓
基于以上兩個問題,我們可以參考下幾家大廠的解決方案:
微信開源的 Martix 中的Matrix-Android-ResourceCanary
Martix 把分析過程需要用到的部分字符串?dāng)?shù)據(jù)和Bitmap的buffer數(shù)組外,其余的buffer數(shù)據(jù)都直接剔除,這樣處理之后的Hprof文件通常能比原始文件小1/10以上。
其他主要增加了一些誤報的優(yōu)化。
美團的 Probe:Android線上OOM問題定位組件
通過 native hook 了虛擬機寫入 hprof 文件的操作,在這里先過濾了某些數(shù)據(jù),最終生成的 hprof 文件就是裁剪后的了。在不考慮可能會有的兼容性問題,這個方案要比其他的方案要高效,因為它解決了 hprof 加載的內(nèi)存問題。
但是Probe是閉源的。
快手的 KOOM——高性能線上內(nèi)存監(jiān)控方案
快手最近開源的KOOM有這幾個亮點:
- 實現(xiàn)了Probe中提出 hook 生成 hprof 的 native 方法
- 解決了
dumpHprofData方法阻塞問題 - 使用了 LeakCanary2 中使用的 hprof 分析庫 shark
對于 dumpHprofData 方法阻塞問題,koom的解決方式是fork新進程來執(zhí)行dump操作,這樣對父進程的正常執(zhí)行沒有影響。以下是耗時對比:
| fork dump | normal dump | |
|---|---|---|
| 耗時(ms) | 139.5 | 19962.5 |
綜上對比,只有koom解決了線上應(yīng)用的兩大痛點。