LeakCanary使用詳細(xì)教程(附Demo)

在Android的性能優(yōu)化中,內(nèi)存優(yōu)化是必不可少的點(diǎn),而內(nèi)存優(yōu)化最重要的一點(diǎn)就是解決內(nèi)存泄漏的問(wèn)題,在Android的內(nèi)存泄漏分析工具也不少,比如PC端的有:AndroidStudio自帶的Android Profiler、MAT等工具;手機(jī)端也有,就是我們今天要介紹的LeakCanary

LeakCanary簡(jiǎn)介

LeakCanary是Square公司為Android開(kāi)發(fā)者提供的一個(gè)自動(dòng)檢測(cè)內(nèi)存泄漏的工具,
LeakCanary本質(zhì)上是一個(gè)基于MAT進(jìn)行Android應(yīng)用程序內(nèi)存泄漏自動(dòng)化檢測(cè)的的開(kāi)源工具,我們可以通過(guò)集成LeakCanary提供的jar包到自己的工程中,一旦檢測(cè)到內(nèi)存泄漏,LeakCanary就好dump Memory信息,并通過(guò)另一個(gè)進(jìn)程分析內(nèi)存泄漏的信息并展示出來(lái),隨時(shí)發(fā)現(xiàn)和定位內(nèi)存泄漏問(wèn)題,而不用每次在開(kāi)發(fā)流程中都抽出專(zhuān)人來(lái)進(jìn)行內(nèi)存泄漏問(wèn)題檢測(cè),極大地方便了Android應(yīng)用程序的開(kāi)發(fā)。

LeakCanary顯示內(nèi)存泄漏的頁(yè)面:


image

LeakCanary優(yōu)點(diǎn):

1、針對(duì)Android Activity組件完全自動(dòng)化的內(nèi)存泄漏檢查。

2、可定制一些行為(dump文件和leaktrace對(duì)象的數(shù)量、自定義例外、分析結(jié)果的自定義處理等)。

3、集成到自己工程并使用的成本很低。

4、友好的界面展示和通知。

LeakCanary工作機(jī)制:

引用LeakCanary中文使用說(shuō)明,它的基本工作機(jī)制如下:

1、RefWatcher.watch() 創(chuàng)建一個(gè) KeyedWeakReference 到要被監(jiān)控的對(duì)象。

2、然后在后臺(tái)線程檢查引用是否被清除,如果沒(méi)有,調(diào)用GC。

3、如果引用還是未被清除,把 heap 內(nèi)存 dump 到 APP 對(duì)應(yīng)的文件系統(tǒng)中的一個(gè) .hprof 文件中。

4、在另外一個(gè)進(jìn)程中的 HeapAnalyzerService 有一個(gè) HeapAnalyzer 使用HAHA 解析這個(gè)文件。

5、得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位內(nèi)存泄露。

6、HeapAnalyzer 計(jì)算 到 GC roots 的最短強(qiáng)引用路徑,并確定是否是泄露。如果是的話,建立導(dǎo)致泄露的引用鏈。

7、引用鏈傳遞到 APP 進(jìn)程中的 DisplayLeakService, 并以通知的形式展示出來(lái)。

LeakCanary的Android Studio集成

一、 在build.gradle中添加LeakCanary的依賴(lài)包,截止目前l(fā)eakcanary的最新版本是1.6.1:
dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.6.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
}

在開(kāi)發(fā)中一般同時(shí)集成debug和release版本,其中:

  • com.squareup.leakcanary:leakcanary-android:1.6.1 是debug版本,在你的app編譯的是debug版本,加載的是該jar包,一旦出現(xiàn)內(nèi)存泄漏會(huì)在通知欄中通知開(kāi)發(fā)者產(chǎn)生了內(nèi)存泄漏;

  • com.squareup.leakcanary:leakcanary-android-no-op:1.6.1 是release版本,如果你的app編譯的是release版本時(shí),加載的是該jar包,no-op是指No Operation Performed,代表不會(huì)做任何操作,不會(huì)干擾正式用戶(hù)的使用;

二、 在我們自定義Application的onCreate方法中注冊(cè)LeakCanary
@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);
}

  • 解釋一下為什么要先判斷LeakCanary.isInAnalyzerProcess(this)

在注冊(cè)之前先判斷LeakCanary是否已經(jīng)運(yùn)行在手機(jī)上,比如你同時(shí)有多個(gè)APP集成了LeakCanary,其他app已經(jīng)運(yùn)行了LeakCanary則不需要重新install

我們看一下isInAnalyzerProcess方法的源碼:

/**
   * Whether the current process is the process running the {@link HeapAnalyzerService}, which is
   * a different process than the normal app process.
   */
  public static boolean isInAnalyzerProcess(Context context) {
    Boolean isInAnalyzerProcess = LeakCanaryInternals.isInAnalyzerProcess;
    // This only needs to be computed once per process.
    if (isInAnalyzerProcess == null) {
      isInAnalyzerProcess = isInServiceProcess(context, HeapAnalyzerService.class);
      LeakCanaryInternals.isInAnalyzerProcess = isInAnalyzerProcess;
    }
    return isInAnalyzerProcess;
  }

從源碼中可以看到真正調(diào)用的是isInServiceProcess方法,在來(lái)看下isInServiceProcess的源碼:

public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
      CanaryLog.d(e, "Could not get package info for %s", context.getPackageName());
      return false;
    }
    String mainProcess = packageInfo.applicationInfo.processName;

    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
      serviceInfo = packageManager.getServiceInfo(component, 0);
    } catch (PackageManager.NameNotFoundException ignored) {
      // Service is disabled.
      return false;
    }

    if (serviceInfo.processName.equals(mainProcess)) {
      CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false;
    }

    int myPid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    List<ActivityManager.RunningAppProcessInfo> runningProcesses;
    try {
      runningProcesses = activityManager.getRunningAppProcesses();
    } catch (SecurityException exception) {
      // https://github.com/square/leakcanary/issues/948
      CanaryLog.d("Could not get running app processes %d", exception);
      return false;
    }
    if (runningProcesses != null) {
      for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
        if (process.pid == myPid) {
          myProcess = process;
          break;
        }
      }
    }
    if (myProcess == null) {
      CanaryLog.d("Could not find running process for %d", myPid);
      return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
  }

我們可以看到ComponentName component = new ComponentName(context, serviceClass);
而這個(gè)serviceClass參數(shù)是isInAnalyzerProcess方法中調(diào)用的HeapAnalyzerService,HeapAnalyzerService正是用來(lái)分析內(nèi)存泄漏的單獨(dú)進(jìn)程,說(shuō)以說(shuō)LeakCanary在同一個(gè)手機(jī)只需要執(zhí)行一次install就可以了,當(dāng)然執(zhí)行多次也是可以的;

  • 回到正題,正常情況下Application調(diào)用LeakCanary.install(this)后就可以正常監(jiān)聽(tīng)該app程序的內(nèi)存泄漏;

LeakCanary監(jiān)聽(tīng)指定對(duì)象的內(nèi)存泄漏

如果想讓LeakCanary監(jiān)聽(tīng)指定對(duì)象的內(nèi)存泄漏,我們就需要使用到RefWatcher的watch功能,使用方式如下:

  • 在Application的onCreate中調(diào)用install方法,并獲取RefWatcher對(duì)象:
private static RefWatcher sRefWatcher;

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

public static RefWatcher getRefWatcher() {
    return sRefWatcher;
}

注意:因?yàn)檫@時(shí)候需要獲取sRefWatcher對(duì)象,所以sRefWatcher = LeakCanary.install(this)一定需要執(zhí)行,不需要判斷LeakCanary.isInAnalyzerProcess(this)

  • 為了方便演示使用LeakCanary獲取和解決內(nèi)存泄漏的問(wèn)題,我們先寫(xiě)一個(gè)內(nèi)存泄漏的場(chǎng)景,我們知道最常見(jiàn)的內(nèi)存泄漏是單列模式使用Activity的Context場(chǎng)景,所以我們也用單列模式來(lái)演示:
public class Singleton {
    private static Singleton singleton;
    private Context context;
    private Singleton(Context context) {
        this.context = context;
    }

    public static Singleton newInstance(Context context) {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null){//雙重檢查鎖定
                    singleton = new Singleton(context);
                }
            }
        }
        return singleton;
    }
}

  • 在需要監(jiān)聽(tīng)的對(duì)象中調(diào)用RefWatcher的watch方法進(jìn)行監(jiān)聽(tīng),比如我想監(jiān)聽(tīng)一個(gè)Activity,我們可以在該Acitivity中onCreate方法中添加DemoApp.getRefWatcher().watch(this);:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    DemoApp.getRefWatcher().watch(this);
    setContentView(R.layout.activity_second);
    Singleton singleton = Singleton.newInstance(this);
}

LeakCanary內(nèi)存泄漏展示頁(yè)面

同時(shí)上面的步驟,當(dāng)我們?cè)谶\(yùn)行app程序的時(shí)候,出現(xiàn)內(nèi)存泄漏后,過(guò)一小段時(shí)間后就會(huì)在通知欄中通知出現(xiàn)內(nèi)存泄漏的情況:


image

同時(shí)會(huì)在桌面上生成一個(gè)Leaks的圖標(biāo),這個(gè)就是展示內(nèi)存泄漏列表的,內(nèi)存泄漏列表頁(yè)面如下:

image

這是一個(gè)內(nèi)存泄漏的列表,我們可以通過(guò)點(diǎn)擊進(jìn)入查看泄漏的內(nèi)容


image

還可以通過(guò)點(diǎn)擊右邊的“+”號(hào)查看更詳細(xì)的信息,內(nèi)容太長(zhǎng)就不截圖了,內(nèi)部有詳細(xì)介紹調(diào)用的流程;

內(nèi)存泄漏解決

上面的演示列子我們使用的是單列模式來(lái)產(chǎn)生內(nèi)存泄漏,我們當(dāng)然知道怎樣正確的解決這個(gè)內(nèi)存泄漏,
就是Singleton singleton = Singleton.newInstance(this)的調(diào)用傳入Contxt時(shí)使用ApplicationContext;

這里總結(jié)一下產(chǎn)生內(nèi)存泄漏的常見(jiàn)場(chǎng)景和常用的解決方案

  • 常見(jiàn)的內(nèi)存泄漏場(chǎng)景:

1、單例設(shè)計(jì)模式造成的內(nèi)存泄漏

2、非靜態(tài)內(nèi)部類(lèi)創(chuàng)建的靜態(tài)實(shí)例造成的內(nèi)存泄漏

3、Handler造成的內(nèi)存泄漏

4、線程造成的內(nèi)存泄漏

5、資源未關(guān)閉造成的內(nèi)存泄漏

  • 常見(jiàn)的解決方案(思路)

1、盡量使用Application的Context而不是Activity的漏

2、使用弱引用或者軟引用漏

3、手動(dòng)設(shè)置null,解除引用關(guān)系漏

4、將內(nèi)部類(lèi)設(shè)置為static,不隱式持有外部的實(shí)例漏

5、注冊(cè)與反注冊(cè)成對(duì)出現(xiàn),在對(duì)象合適的生命周期進(jìn)行反注冊(cè)操作。漏

6、如果沒(méi)有修改的權(quán)限,比如系統(tǒng)或者第三方SDK,可以使用反射進(jìn)行解決持有關(guān)系

7、在使用完BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源時(shí),一定要在Activity中的OnDestry中及時(shí)的關(guān)閉、注銷(xiāo)或者釋放內(nèi)存,

由于篇幅過(guò)長(zhǎng)這里就不對(duì)此展開(kāi)介紹。

Demo附件

LeakCanary原理分析

參考鏈接:

https://github.com/square/leakcanary

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371

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