推薦閱讀:
LeakCanary是Square公司基於MAT開源的一個內存泄漏檢測神器,在發生內存泄漏的時候LeakCanary會自動顯示泄漏信息,現在更新了好幾個版本,用kotlin語言重新實現了一遍;鵝場APM性能監控框架也集成了內存泄露模塊 ResourcePlugin ,這里就兩者進行對比。
1、組件啟動
LeakCanary自動注冊啟動
原理:專門定制了一個ContentProvider,來注冊啟動LeakCanary
實現如下:
/** * Content providers are loaded before the application class is created. [LeakSentryInstaller] is * used to install [leaksentry.LeakSentry] on application start. */ internal class LeakSentryInstaller : ContentProvider() { override fun onCreate(): Boolean { CanaryLog.logger = DefaultCanaryLog() val application = context!!.applicationContext as Application InternalLeakSentry.install(application) return true } ... }
ResourcePlugin 需要手動啟動
public class MatrixApplication extends Application { ... @Override public void onCreate() { super.onCreate(); ... ResourcePlugin resPlugin = null; if (matrixEnable) { resPlugin = new ResourcePlugin(new ResourceConfig.Builder() .dynamicConfig(dynamicConfig) .setDumpHprof(false) .setDetectDebuger(true) //only set true when in sample, not in your app .build()) //resource builder.plugin(resPlugin ); ResourcePlugin.activityLeakFixer(this); ... } Matrix.init(builder.build()); if(resPlugin != null){ resPlugin.start(); } } }
2、watch范圍和自動watch的對象
LeakCanary RefWatcher可以watch任何對象(包括Activity、Fragment、Fragment.View)
class RefWatcher{ fun watch(watchedInstance: Any) {...} fun watch( watchedInstance: Any,name: String) {...} }
支持自動watch Activity、Fragment、Fragment.View對象
1.自動watcher Activity
internal class ActivityDestroyWatcher { private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() { override fun onActivityDestroyed(activity: Activity) { if (configProvider().watchActivities) { refWatcher.watch(activity) } } } companion object { fun install(... ) { val activityDestroyWatcher = ActivityDestroyWatcher(refWatcher, configProvider) application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks) } } }
ActivityDestroyWatcher.install在LeakSentryInstaller.onCreate間接調用,注冊ActivityLifecycleCallbacks 監聽Activity的生命周期,從而實現自動watch Activity對象。
2.自動watch Fragment、Fragment.View
//子類有 //SupportFragmentDestroyWatcher //AndroidOFragmentDestroyWatcher internal interface FragmentDestroyWatcher { fun watchFragments(activity: Activity) companion object { ... fun install(... ) { ... application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks by noOpDelegate() { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { for (watcher in fragmentDestroyWatchers) { watcher.watchFragments(activity) } } }) } } }
FragmentDestroyWatcher .install在LeakSentryInstaller.onCreate間接調用,注冊ActivityLifecycleCallbacks 監聽Activity的生命周期函數onCreate,然后對activity.fragmentManager注冊FragmentLifecycleCallbacks監聽Fragment的周期函數,從而實現自動watch Fragment、Fragment.View如下:
internal class XXXFragmentDestroyWatcher(...) : FragmentDestroyWatcher { private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentViewDestroyed( fm: FragmentManager, fragment: Fragment ) { val view = fragment.view if (view != null && configProvider().watchFragmentViews) { //watcher view refWatcher.watch(view) } } override fun onFragmentDestroyed( fm: FragmentManager, fragment: Fragment ) { if (configProvider().watchFragments) { //watcher fragment refWatcher.watch(fragment) } } } //AndroidOFragmentDestroyWatcher override fun watchFragments(activity: Activity) { val fragmentManager = activity.fragmentManager fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true) } //SupportFragmentDestroyWatcher override fun watchFragments(activity: Activity) { if (activity is FragmentActivity) { val supportFragmentManager = activity.supportFragmentManager supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true) } } }
從源碼上可以看出,貌似只自動watch 以及Fragment,嵌套的Fragment就不行了,如果是watch其他對象(包括子Fragment),則需要手動調用 RefWatcher.watch方法。
Replugin 只有一個ActivityRefWatcher,只支持watcher Activity,也是通過注冊ActivityLifecycleCallbacks 監聽Activity的生命周期,從而實現自動watcher Activity對象。
public class ActivityRefWatcher extends FilePublisher implements Watcher { @Override public void start() { stopDetect(); final Application app = mResourcePlugin.getApplication(); if (app != null) { app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor); //輪詢檢測是否發生溢出 scheduleDetectProcedure(); } }
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
@Override
public void onActivityDestroyed(Activity activity) {
//push mDestroyedActivityInfos集合中,通過輪詢檢測對mDestroyedActivityInfos進行處理
pushDestroyedActivityInfo(activity);
synchronized (mDestroyedActivityInfos) {
mDestroyedActivityInfos.notifyAll();
}
}
};
3、檢測泄露實現
1.檢測線程
LeakCanay檢測實現,舊版本是在一個HandlerThread 輪詢檢測,現在發生改變,先在主線程中觸發檢測,由RefWatcher.watch主動觸發,對activity,Fragment,Fragment.view的檢測,即由生命周期觸發,然后在 非主線程中進行真正的check。
現在主線中被動觸發檢測依據如下:
class RefWatcher{ fun watch( watchedInstance: Any,name: String) { ... watchedInstances[key] = reference checkRetainedExecutor.execute { moveToRetained(key) } } } internal object InternalLeakSentry { ... private val checkRetainedExecutor = Executor {
//主線程handler mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis) } val refWatcher = RefWatcher( clock = clock, checkRetainedExecutor = checkRetainedExecutor, onInstanceRetained = { listener.onReferenceRetained() }, isEnabled = { LeakSentry.config.enabled } ) ... }
從moveToRetained調用,最終輾轉到HeapDumpTrigger的方法scheduleRetainedInstanceCheck方法,然后在非主線中進行真正check,代碼如下:
internal class HeapDumpTrigger() { private fun scheduleRetainedInstanceCheck(reason: String) { if (checkScheduled) { CanaryLog.d("Already scheduled retained check, ignoring ($reason)") return } checkScheduled = true //非主線程hanlder backgroundHandler.post { checkScheduled = false checkRetainedInstances(reason) } } ... }
ResourcePlugin參考LeakCanary舊版本,采用線程輪詢檢測,依據如下:
//ActivityRefWatcher.start
private void scheduleDetectProcedure() {
//檢測輪詢 mScanDestroyedActivitiesTask execute函數一直返回RetryableTask.Status.RETRY
mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
class RetryableTaskExecutor{ private void postToBackgroundWithDelay(final RetryableTask task, final int failedAttempts) { //非主線程 handler mBackgroundHandler.postDelayed(new Runnable() { @Override public void run() { RetryableTask.Status status = task.execute(); if (status == RetryableTask.Status.RETRY) { postToBackgroundWithDelay(task, failedAttempts + 1); } } }, mDelayMillis); } }
2、檢測泄露邏輯實現
LeakCanay Check檢測
原理:VM會將可回收的對象加入 WeakReference 關聯的 ReferenceQueue
1)根據retainedReferenceCount > 0,觸發一次gc請求,再次獲取retainedReferenceCount
var retainedReferenceCount = refWatcher.retainedInstanceCount if (retainedReferenceCount > 0) { gcTrigger.runGc() retainedReferenceCount = refWatcher.retainedInstanceCount }
2)判斷retainedReferenceCount 是否大於retainedVisibleThreshold(默認為5),小於則跳過接下來的檢測
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
3)根據dumpHeapWhenDebugging開關和是否在Debug調試,如果配置開關開啟且在調試,則延時輪詢等待,調試結束
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) { showRetainedCountWithDebuggerAttached(retainedReferenceCount) scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS) return }
4)dump Hprof文件
val heapDumpFile = heapDumper.dumpHeap() if (heapDumpFile == null) { showRetainedCountWithHeapDumpFailed(retainedReferenceCount) return
}
5)開啟HeapAnalyzerService進行Hprof分析
在舊版本中,在個別系統上可能存在誤報,原因大致如下:
-
VM 並沒有提供強制觸發 GC 的 API ,通過
System.gc()
或Runtime.getRuntime().gc()
只能“建議”系統進行 GC ,如果系統忽略了我們的 GC 請求,可回收的對象就不會被加入 ReferenceQueue -
將可回收對象加入 ReferenceQueue 需要等待一段時間,LeakCanary 采用延時 100ms 的做法加以規避,但似乎並不絕對管用
-
監測邏輯是異步的,如果判斷 Activity 是否可回收時某個 Activity 正好還被某個方法的局部變量持有,就會引起誤判
-
若反復進入泄漏的 Activity ,LeakCanary 會重復提示該 Activity 已泄漏
現在這個2.0-alpha-2版本也沒有進行排重,當然這個也不好說,假如一個Activity有多處泄露,且泄露原因不同,排重 就會導致漏報。
ResourcePlugin Check檢測
原理:直接通過WeakReference.get()
來判斷對象是否已被回收,避免因延遲導致誤判
1)判斷當前mDestroyedActivityInfos是否空,為空的話,就沒必要泄露,因為是輪詢,所以要防止CPU空轉,浪費電
// If destroyed activity list is empty, just wait to save power. while (mDestroyedActivityInfos.isEmpty()) { synchronized (mDestroyedActivityInfos) { try { mDestroyedActivityInfos.wait(); } catch (Throwable ignored) { // Ignored. } } }
2)根據配置開關和是否在Debug調試,如果配置開關開啟且在調試,跳過此次check,等待下次輪詢,調試結束
// Fake leaks will be generated when debugger is attached. if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) { MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed."); return Status.RETRY; }
3)增加一個一定能被回收的“哨兵”對象,用來確認系統確實進行了GC,沒有進行GC,則跳過此次check,等待下次輪詢
final WeakReference<Object> sentinelRef = new WeakReference<>(new Object()); triggerGc(); if (sentinelRef.get() != null) { // System ignored our gc request, we will retry later. MatrixLog.d(TAG, "system ignore our gc request, wait for next detection."); return Status.RETRY; }
4)對已判斷為泄漏的Activity,記錄其類名,避免重復提示該Activity已泄漏,有效期一天
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next(); if (isPublished(destroyedActivityInfo.mActivityName)) { MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName); infoIt.remove(); continue; }
前面已經提過排重還是有缺陷的,比如一個Activity有多處泄露,且泄露原因不同,排重 就會導致漏報
5)若發現某個Activity無法被回收,再重復判斷3次,且要求從該Activity被記錄起有2個以上的Activity被創建才認為是泄漏,以防在判斷時該Activity被局部變量持有導致誤判
++destroyedActivityInfo.mDetectedCount; long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount; if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes || (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) { // Although the sentinel tell us the activity should have been recycled, // system may still ignore it, so try again until we reach max retry times. continue; }
6.根據是否設置了mHeapDumper(即配置快關),若設置了,進行dumpHeap,然后開啟服務CanaryWorkerService,進行shrinkHprofAndReport,否則進行簡單的onDetectIssue
if (mHeapDumper != null) { final File hprofFile = mHeapDumper.dumpHeap(); if (hprofFile != null) { markPublished(destroyedActivityInfo.mActivityName); final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName); mHeapDumpHandler.process(heapDump); infoIt.remove(); } else { infoIt.remove(); } } else { markPublished(destroyedActivityInfo.mActivityName); if (mResourcePlugin != null) { ... mResourcePlugin.onDetectIssue(new Issue(resultJson)); } }
4、Hprof裁剪和分析(暫時不詳細分析)
LeakCanary沒有對Hprof文件進行shrink裁剪,使用haha進行解析,分析出其泄露對象的GC Root引用鏈,把檢測和分析都放在客戶端。
ResourcePlugin只有檢測和Hprof文件shrink功能,不支持在客戶端Hprof文件,需要利用其分析庫源碼打成jar單獨Hprof對進行分析,在分析過程中也可以把找出冗余Bitmap的GC ROOT鏈。
裁剪Hprof文件源碼見:HprofBufferShrinker().shrink
冗余Bitmap分析器:DuplicatedBitmapAnalyzer
Activity泄露分析器:ActivityLeakAnalyzer
Hprof 文件的大小一般約為 Dump 時的內存占用大小,Dump 出來的 Hprof 大則一百多M,,如果不做任何處理直接將此 Hprof 文件上傳到服務端,一方面會消耗大量帶寬資源,另一方面服務端將 Hprof 文件長期存檔時也會占用服務器的存儲空間。通過分析 Hprof 文件格式可知,Hprof 文件中 buffer 區存放了所有對象的數據,包括字符串數據、所有的數組等,而我們的分析過程卻只需要用到部分字符串數據和 Bitmap 的 buffer 數組,其余的 buffer 數據都可以直接剔除,這樣處理之后的 Hprof 文件通常能比原始文件小 1/10 以上。
LeakCanary 中的引用鏈查找算法都是針對單個目標設計的,ResourceCanary 中查找冗余 Bitmap 時可能找到多個結果,如果分別對每個結果中的 Bitmap 對象調用該算法,在訪問引用關系圖中的節點時會遇到非常多的重復訪問的節點,降低了查找效率。ResourcePlugin 修改了 LeakCanary 的引用鏈查找算法,使其在一次調用中能同時查找多個目標到 GC Root 的最短引用鏈。
總結
參考資料:
Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余檢測
如果您對博主的更新內容持續感興趣,請關注公眾號!