性能優(yōu)化工具知識梳理(7) - LeakCanary

性能優(yōu)化工具知識梳理(1) - TraceView
性能優(yōu)化工具知識梳理(2) - Systrace
性能優(yōu)化工具知識梳理(3) - 調(diào)試GPU過度繪制 & GPU呈現(xiàn)模式分析
性能優(yōu)化工具知識梳理(4) - Hierarchy Viewer
性能優(yōu)化工具知識梳理(5) - MAT
性能優(yōu)化工具知識梳理(6) - Memory Monitor & Heap Viewer & Allocation Tracker
性能優(yōu)化工具知識梳理(7) - LeakCanary
性能優(yōu)化工具知識梳理(8) - Lint

一、概述

LeakCanary提供了一種很便捷的方式,讓我們在開發(fā)階段檢測內(nèi)存泄漏問題硝逢,我們不需要自己去根據(jù)內(nèi)存快照來分析內(nèi)存泄漏的原因,所需要做的僅僅是在Debug包中集成它绣檬,它會自動地幫我們檢測內(nèi)存泄漏,并給出導(dǎo)致泄漏的引用鏈嫂粟。

二娇未、集成

下面,就來看一下如何在項目當(dāng)中集成它:

  • 第一步:需要引入遠(yuǎn)程依賴星虹,這里我們引入了兩個零抬,在release版本中,所有的調(diào)用都是空實現(xiàn)宽涌,這樣就會避免在release的版本中也在桌面生成一個泄漏檢測結(jié)果的圖標(biāo)平夜。
dependencies {
        //在 debug 版本中才會實現(xiàn)真正的功能
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
        //在 release 版本中為空實現(xiàn)
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
  • 第二步:重寫Application,初始化一個全局RefWatcher對象卸亮,它負(fù)責(zé)監(jiān)視所有應(yīng)當(dāng)要被回收的對象:
public class LeakCanaryApplication extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        mRefWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        LeakCanaryApplication application = (LeakCanaryApplication) context.getApplicationContext();
        return application.mRefWatcher;
    }
}
  • 第三步:在需要回收的對象上忽妒,添加監(jiān)測代碼,這里我們以Activity為例就需要在它的onDestory()方法中加入監(jiān)測的代碼兼贸,我們通過單例持有Activity的引用段直,模擬了一種內(nèi)存泄漏發(fā)生的場景:
public class LeakSingleton {

    private static LeakSingleton sInstance;
    private Context mContext;

    public static LeakSingleton getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new LeakSingleton(context);
        }
        return sInstance;
    }

    private LeakSingleton(Context context) {
        mContext = context;
    }
}

public class LeakCanaryActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_canary);
        //讓這個單例對象持有 Activity 的引用
        LeakSingleton.getInstance(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //在 onDestroy 方法中使用 Application 中創(chuàng)建的 RefWatcher 監(jiān)視需要回收的對象
        LeakCanaryApplication.getRefWatcher(this).watch(this);
    }
}

在退出應(yīng)用程序之后,我們會發(fā)現(xiàn)在桌面上生成了一個新的圖標(biāo)溶诞,點擊圖標(biāo)進(jìn)入鸯檬,就是LeakCanary為我們分析出的導(dǎo)致泄漏的引用鏈:


以上就是把LeakCanary集成到項目中的方法,下面螺垢,我們來討論一下它的實現(xiàn)原理喧务。

三颜及、原理

當(dāng)調(diào)用了RefWatcher.watch()方法之后,會觸發(fā)以下邏輯:

  • 創(chuàng)建一個KeyedWeakReference蹂楣,它內(nèi)部引用了watch傳入的對象:
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
  • 在后臺線程檢查引用是否被清除:
 this.watchExecutor.execute(new Runnable() {
      public void run() {
           RefWatcher.this.ensureGone(reference, watchStartNanoTime);
      }
});
  • 如果沒有清除,那么首先調(diào)用一次GC讯蒲,假如引用還是沒有被清除痊土,那么把當(dāng)前的內(nèi)存快照保存到.hprof文件當(dāng)中,并調(diào)用heapdumpListener進(jìn)行分析:
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
        long gcStartNanoTime = System.nanoTime();
        long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        this.removeWeaklyReachableReferences();
        if(!this.gone(reference) && !this.debuggerControl.isDebuggerAttached()) {
            this.gcTrigger.runGc();
            this.removeWeaklyReachableReferences();
            if(!this.gone(reference)) {
                long startDumpHeap = System.nanoTime();
                long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
                File heapDumpFile = this.heapDumper.dumpHeap();
                if(heapDumpFile == null) {
                    return;
                }
                long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
                this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs, heapDumpDurationMs));
            }
        }
}
  • 上面說到的heapdumpListener的實現(xiàn)類為ServiceHeapDumpListener墨林,它會啟動內(nèi)部的HeapAnalyzerService
 public void analyze(HeapDump heapDump) {
        Preconditions.checkNotNull(heapDump, "heapDump");
        HeapAnalyzerService.runAnalysis(this.context, heapDump, this.listenerServiceClass);
}
  • 這是一個IntentService赁酝,因此它的onHandlerIntent方法是運行在子線程中的,在通過HeapAnalyzer分析完畢之后旭等,把最終的結(jié)果傳回給App端展示檢測的結(jié)果:
   protected void onHandleIntent(Intent intent) {
        String listenerClassName = intent.getStringExtra("listener_class_extra");
        HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heapdump_extra");
        AnalysisResult result = this.heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
        AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
    }
  • HeapAnalyzer會計算未能回收的引用到Gc Roots的最短引用路徑酌呆,如果泄漏,那么建立導(dǎo)致泄漏的引用鏈并通過AnalysisResult返回:
    public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
        long analysisStartNanoTime = System.nanoTime();
        if(!heapDumpFile.exists()) {
            IllegalArgumentException snapshot1 = new IllegalArgumentException("File does not exist: " + heapDumpFile);
            return AnalysisResult.failure(snapshot1, this.since(analysisStartNanoTime));
        } else {
            ISnapshot snapshot = null;

            AnalysisResult className;
            try {
                snapshot = this.openSnapshot(heapDumpFile);
                IObject e = this.findLeakingReference(referenceKey, snapshot);
                if(e != null) {
                    String className1 = e.getClazz().getName();
                    AnalysisResult result = this.findLeakTrace(analysisStartNanoTime, snapshot, e, className1, true);
                    if(!result.leakFound) {
                        result = this.findLeakTrace(analysisStartNanoTime, snapshot, e, className1, false);
                    }

                    AnalysisResult var9 = result;
                    return var9;
                }

                className = AnalysisResult.noLeak(this.since(analysisStartNanoTime));
            } catch (SnapshotException var13) {
                className = AnalysisResult.failure(var13, this.since(analysisStartNanoTime));
                return className;
            } finally {
                this.cleanup(heapDumpFile, snapshot);
            }

            return className;
        }
    }

四搔耕、自定義處理行為

默認(rèn)LeakCanary是會在桌面生成一個圖標(biāo)隙袁,點擊圖標(biāo)之后,會展示導(dǎo)致泄漏的引用鏈弃榨,有時候菩收,我們希望把這些信息上傳到服務(wù)器中,那么就需要自定義收到結(jié)果后的處理的行為鲸睛,下面娜饵,我們看一下要怎么做:

  • 第一步:繼承DisplayLeakService,進(jìn)行自己的處理邏輯官辈,這里我們只是打印出泄漏的信息箱舞,heapDump為對應(yīng)的內(nèi)存快照,result為分析的結(jié)果拳亿,leakInfo則是相關(guān)的信息:
public class MyLeakUploadService extends DisplayLeakService {

    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
            return;
        }
        Log.d("MyLeakUploadService", "leakInfo=" + leakInfo);
    }

}
  • 第二步:改變Application中初始化RefWatcher的方式晴股,第二個參數(shù)中傳入我們自定義的Service類名:
public class LeakCanaryApplication extends Application {
    private RefWatcher mRefWatcher;
    @Override
    public void onCreate() {
        super.onCreate();
        mRefWatcher = LeakCanary.install(this, MyLeakUploadService.class);
    }
}
  • 第三步:在AndroidManifest.xml中注冊自定義的Service
   <application>
        <service android:name=".leakcanary.MyLeakUploadService"/>
    </application>
  • 最后,我們運行和之前一樣的操作风瘦,會看到在輸出臺上打印出了泄漏的分析結(jié)果:


五队魏、小結(jié)

在調(diào)試階段,我們可以通過引入LeakCanary万搔,讓它幫助我們排查出一些會導(dǎo)致內(nèi)存泄漏的問題胡桨。

六、參考文獻(xiàn)

https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/


更多文章瞬雹,歡迎訪問我的 Android 知識梳理系列:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昧谊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子酗捌,更是在濱河造成了極大的恐慌呢诬,老刑警劉巖涌哲,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尚镰,居然都是意外死亡阀圾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門狗唉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來初烘,“玉大人,你說我怎么就攤上這事分俯∩隹穑” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵缸剪,是天一觀的道長吗铐。 經(jīng)常有香客問我,道長杏节,這世上最難降的妖魔是什么唬渗? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮奋渔,結(jié)果婚禮上谣妻,老公的妹妹穿的比我還像新娘。我一直安慰自己卒稳,他們只是感情好蹋半,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著充坑,像睡著了一般减江。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捻爷,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天辈灼,我揣著相機(jī)與錄音,去河邊找鬼也榄。 笑死巡莹,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的甜紫。 我是一名探鬼主播降宅,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼囚霸!你這毒婦竟也來了腰根?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤拓型,失蹤者是張志新(化名)和其女友劉穎额嘿,沒想到半個月后瘸恼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡册养,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年东帅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片球拦。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冰啃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刘莹,到底是詐尸還是另有隱情,我是刑警寧澤焚刚,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布点弯,位于F島的核電站,受9級特大地震影響矿咕,放射性物質(zhì)發(fā)生泄漏抢肛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一碳柱、第九天 我趴在偏房一處隱蔽的房頂上張望捡絮。 院中可真熱鬧,春花似錦莲镣、人聲如沸福稳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽的圆。三九已至,卻和暖如春半火,著一層夾襖步出監(jiān)牢的瞬間越妈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工钮糖, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留梅掠,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓店归,卻偏偏與公主長得像阎抒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子消痛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 性能優(yōu)化工具知識梳理(1) - TraceView[http://www.reibang.com/p/37c26...
    澤毛閱讀 2,965評論 0 12
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理挠蛉,服務(wù)發(fā)現(xiàn),斷路器肄满,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • LeakCanary LeakCanary是Square公司開源的檢測內(nèi)存泄露的工具, 如果懷疑自己或者隊友寫的代...
    常強(qiáng)兒閱讀 904評論 1 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,075評論 25 707
  • 早上早早起來谴古,在菜市場质涛,來來往往,獨有的味道掰担。向來很少在菜市場這些地方走動汇陆,不是不喜歡,而是還沒有擔(dān)起家庭的擔(dān)子带饱。...
    丘禾閱讀 199評論 0 2