LeakCanary是Square公司基于MAT開源的一個(gè)內(nèi)存泄漏檢測(cè)神器,在發(fā)生內(nèi)存泄漏的時(shí)候LeakCanary會(huì)自動(dòng)顯示泄漏信息,現(xiàn)在更新了好幾個(gè)版本,用kotlin語言重新實(shí)現(xiàn)了一遍;鵝場(chǎng)APM性能監(jiān)控框架也集成了內(nèi)存泄露模塊 ResourcePlugin ,這里就兩者進(jìn)行對(duì)比。
1、組件啟動(dòng)
LeakCanary自動(dòng)注冊(cè)啟動(dòng)
原理:專門定制了一個(gè)ContentProvider,來注冊(cè)啟動(dòng)LeakCanary
實(shí)現(xiàn)如下:
/**
* 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 需要手動(dòng)啟動(dòng)
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、watcher范圍和自動(dòng)wacherd的對(duì)象
LeakCanary RefWatcher可以watcher任何對(duì)象(包括Activity、Fragment、Fragment.View)
class RefWatcher{
fun watch(watchedInstance: Any) {...}
fun watch( watchedInstance: Any,name: String) {...}
}
支持自動(dòng)watcher Activity、Fragment、Fragment.View對(duì)象
1.自動(dòng)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間接調(diào)用,注冊(cè)ActivityLifecycleCallbacks 監(jiān)聽Activity的生命周期,從而實(shí)現(xiàn)自動(dòng)watcher Activity對(duì)象。
2.自動(dòng)watcher 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間接調(diào)用,注冊(cè)ActivityLifecycleCallbacks 監(jiān)聽Activity的生命周期函數(shù)onCreate,然后對(duì)activity.fragmentManager注冊(cè)FragmentLifecycleCallbacks監(jiān)聽Fragment的周期函數(shù),從而實(shí)現(xiàn)自動(dòng)watcher 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)
}
}
}
Replugin 只有一個(gè)ActivityRefWatcher,只支持watcher Activity,也是通過注冊(cè)ActivityLifecycleCallbacks 監(jiān)聽Activity的生命周期,從而實(shí)現(xiàn)自動(dòng)watcher Activity對(duì)象。
public class ActivityRefWatcher extends FilePublisher implements Watcher {
@Override
public void start() {
stopDetect();
final Application app = mResourcePlugin.getApplication();
if (app != null) {
app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
//輪詢檢測(cè)是否發(fā)生溢出
scheduleDetectProcedure();
}
}
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
//push mDestroyedActivityInfos集合中,通過輪詢檢測(cè)對(duì)mDestroyedActivityInfos進(jìn)行處理
pushDestroyedActivityInfo(activity);
synchronized (mDestroyedActivityInfos) {
mDestroyedActivityInfos.notifyAll();
{
}};
3、檢測(cè)泄露實(shí)現(xiàn)
1.檢測(cè)線程
LeakCanay檢測(cè)實(shí)現(xiàn),舊版本是在一個(gè)HandlerThread 輪詢檢測(cè),現(xiàn)在發(fā)生改變,先在主線程中觸發(fā)檢測(cè),由RefWatcher.watch主動(dòng)觸發(fā),對(duì)activity,F(xiàn)ragment,F(xiàn)ragment.view的檢測(cè),即由生命周期觸發(fā),然后在
非主線程中進(jìn)行真正的check。
現(xiàn)在主線中被動(dòng)觸發(fā)檢測(cè)依據(jù)如下:
class RefWatcher{
fun watch( watchedInstance: Any,name: String) {
...
watchedInstances[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
}
internal object InternalLeakSentry {
...
private val checkRetainedExecutor = Executor {
mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis)
}
val refWatcher = RefWatcher(
clock = clock,
checkRetainedExecutor = checkRetainedExecutor,
onInstanceRetained = { listener.onReferenceRetained() },
isEnabled = { LeakSentry.config.enabled }
)
...
}
從moveToRetained調(diào)用,最終輾轉(zhuǎn)到HeapDumpTrigger的方法scheduleRetainedInstanceCheck方法,然后在非主線中進(jìn)行真正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舊版本,采用線程輪詢檢測(cè),依據(jù)如下:
//ActivityRefWatcher.start
private void scheduleDetectProcedure() {
//檢測(cè)輪詢 mScanDestroyedActivitiesTask execute函數(shù)一直返回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、檢測(cè)泄露邏輯實(shí)現(xiàn)**
** LeakCanay Check檢測(cè)**
原理:VM會(huì)將可回收的對(duì)象加入 WeakReference 關(guān)聯(lián)的 ReferenceQueue
1)根據(jù)retainedReferenceCount > 0,觸發(fā)一次gc請(qǐng)求,再次獲取retainedReferenceCount
var retainedReferenceCount = refWatcher.retainedInstanceCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = refWatcher.retainedInstanceCount
}
2)判斷retainedReferenceCount 是否大于retainedVisibleThreshold(默認(rèn)為5),小于則跳過接下來的檢測(cè)
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
3)根據(jù)dumpHeapWhenDebugging開關(guān)和是否在Debug調(diào)試,如果配置開關(guān)開啟且在調(diào)試,則延時(shí)輪詢等待,調(diào)試結(jié)束
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進(jìn)行Hprof分析
在舊版本中,在個(gè)別系統(tǒng)上可能存在誤報(bào),原因大致如下:
VM 并沒有提供強(qiáng)制觸發(fā) GC 的 API ,通過
System.gc()或Runtime.getRuntime().gc()只能“建議”系統(tǒng)進(jìn)行 GC ,如果系統(tǒng)忽略了我們的 GC 請(qǐng)求,可回收的對(duì)象就不會(huì)被加入 ReferenceQueue將可回收對(duì)象加入 ReferenceQueue 需要等待一段時(shí)間,LeakCanary 采用延時(shí) 100ms 的做法加以規(guī)避,但似乎并不絕對(duì)管用
監(jiān)測(cè)邏輯是異步的,如果判斷 Activity 是否可回收時(shí)某個(gè) Activity 正好還被某個(gè)方法的局部變量持有,就會(huì)引起誤判
若反復(fù)進(jìn)入泄漏的 Activity ,LeakCanary 會(huì)重復(fù)提示該 Activity 已泄漏
現(xiàn)在這個(gè)2.0-alpha-2版本也沒有進(jìn)行排重,當(dāng)然這個(gè)也不好說,假如一個(gè)Activity有多處泄露,且泄露原因不同,排重 就會(huì)導(dǎo)致漏報(bào)。
ResourcePlugin Check檢測(cè)
原理:直接通過WeakReference.get()來判斷對(duì)象是否已被回收,避免因延遲導(dǎo)致誤判
1)判斷當(dāng)前mDestroyedActivityInfos是否空,為空的話,就沒必要泄露,因?yàn)槭禽喸?,所以要防止CPU空轉(zhuǎn),浪費(fèi)電
// If destroyed activity list is empty, just wait to save power.
while (mDestroyedActivityInfos.isEmpty()) {
synchronized (mDestroyedActivityInfos) {
try {
mDestroyedActivityInfos.wait();
} catch (Throwable ignored) {
// Ignored.
}
}
}
2)根據(jù)配置開關(guān)和是否在Debug調(diào)試,如果配置開關(guān)開啟且在調(diào)試,跳過此次check,等待下次輪詢,調(diào)試結(jié)束
// 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)增加一個(gè)一定能被回收的“哨兵”對(duì)象,用來確認(rèn)系統(tǒng)確實(shí)進(jìn)行了GC,沒有進(jìn)行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)對(duì)已判斷為泄漏的Activity,記錄其類名,避免重復(fù)提示該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;
}
前面已經(jīng)提過排重還是有缺陷的,比如一個(gè)Activity有多處泄露,且泄露原因不同,排重 就會(huì)導(dǎo)致漏報(bào)
5)若發(fā)現(xiàn)某個(gè)Activity無法被回收,再重復(fù)判斷3次,且要求從該Activity被記錄起有2個(gè)以上的Activity被創(chuàng)建才認(rèn)為是泄漏,以防在判斷時(shí)該Activity被局部變量持有導(dǎo)致誤判
++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.根據(jù)是否設(shè)置了mHeapDumper(即配置快關(guān)),若設(shè)置了,進(jìn)行dumpHeap,然后開啟服務(wù)CanaryWorkerService,進(jìn)行shrinkHprofAndReport,否則進(jìn)行簡(jiǎn)單的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裁剪和分析(暫時(shí)不詳細(xì)分析)
LeakCanary沒有對(duì)Hprof文件進(jìn)行shrink裁剪,使用haha進(jìn)行解析,分析出其泄露對(duì)象的GC Root引用鏈,把檢測(cè)和分析都放在客戶端。
ResourcePlugin只有檢測(cè)和Hprof文件shrink功能,不支持在客戶端Hprof文件,需要利用其提供的jar單獨(dú)對(duì)進(jìn)行分析Hprof,在分析過程中也可以把找出冗余Bitmap的GC ROOT鏈。
裁剪Hprof文件源碼見:HprofBufferShrinker().shrink
冗余Bitmap分析器:DuplicatedBitmapAnalyzer
Activity泄露分析器:ActivityLeakAnalyzer
Hprof 文件的大小一般約為 Dump 時(shí)的內(nèi)存占用大小,Dump 出來的 Hprof 大則一百多M,,如果不做任何處理直接將此 Hprof 文件上傳到服務(wù)端,一方面會(huì)消耗大量帶寬資源,另一方面服務(wù)端將 Hprof 文件長期存檔時(shí)也會(huì)占用服務(wù)器的存儲(chǔ)空間。
通過分析 Hprof 文件格式可知,Hprof 文件中 buffer 區(qū)存放了所有對(duì)象的數(shù)據(jù),包括字符串?dāng)?shù)據(jù)、所有的數(shù)組等,而我們的分析過程卻只需要用到部分字符串?dāng)?shù)據(jù)和 Bitmap 的 buffer 數(shù)組,其余的 buffer 數(shù)據(jù)都可以直接剔除,這樣處理之后的 Hprof 文件通常能比原始文件小 1/10 以上。
LeakCanary 中的引用鏈查找算法都是針對(duì)單個(gè)目標(biāo)設(shè)計(jì)的,ResourceCanary 中查找冗余 Bitmap 時(shí)可能找到多個(gè)結(jié)果,如果分別對(duì)每個(gè)結(jié)果中的 Bitmap 對(duì)象調(diào)用該算法,在訪問引用關(guān)系圖中的節(jié)點(diǎn)時(shí)會(huì)遇到非常多的重復(fù)訪問的節(jié)點(diǎn),降低了查找效率。為此我們修改了 LeakCanary 的引用鏈查找算法,使其在一次調(diào)用中能同時(shí)查找多個(gè)目標(biāo)到 GC Root 的最短引用鏈。