LeakCanary 內存泄漏 監測 性能優化 簡介 原理 MD

Markdown版本筆記 個人GitHub首頁 個人博客 個人微信 個人郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

LeakCanary 內存泄漏 監測 性能優化 簡介 原理 MD
GitHub:https://github.com/square/leakcanary
Demo地址:https://github.com/baiqiantao/LeakCanaryTest.git java


目錄

介紹

A memory leak detection 內存泄露檢測 library for Android and Java. android

A small leak will sink a great ship. -- Benjamin Franklin
千里之堤, 毀於蟻穴。 -- 《韓非子·喻老》git

簡單使用

添加依賴: github

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1' //當使用support庫時添加
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1' //發佈時用的是無任何操做的版本
}

初始化: 算法

LeakCanary.install(application);

配置完之後,在 debug 構建的版本中,若是檢測到某個 activity 或 fragment 有內存泄露,LeakCanary 就會自動地顯示一個通知。 編程

更多介紹

  • 爲何要使用LeakCanary性能優化

    • 內存泄漏是一種編程錯誤[programming error],會致使應用程序保留對再也不須要的對象的引用。所以就會致使沒法回收爲該對象分配[allocated]的內存,最終致使 OutOfMemoryError crash。
    • 例如,Android Activity 實例在調用 onDestroy 方法後就再也不須要了,可是若是在靜態字段中存儲了對該Activity的強引用將會阻止其被GC[garbage collected]
    • LeakCanary對一個 longer needed 的對象作了惟一標識,並找到阻止它被垃圾回收的引用鏈。
    • 看成者首次在Square公司的某款App中啓用 LeakCanary 後,他找到並修復了多個內存泄漏,並將 OutOfMemoryError 的崩潰率下降了94%。
  • LeakCanary是怎麼工做的服務器

    • 經過 RefWatcher.watch() 建立了一個KeyedWeakReference to the watched object。
    • 而後在後臺線程檢查引用是否被清除了,若是沒有,則triggers a GC
    • 若是引用仍是未被清除,則 dumps the heap 到文件系統中的 .hprof 文件中。
    • 在另一個獨立的進程中啓動 HeapAnalyzerServiceHeapAnalyzer 使用 HAHA 解析 heap dump 。
    • 得益於惟一的 reference key, HeapAnalyzer 在 heap dump 中找到 KeyedWeakReference,而且定位 leaking reference。
    • HeapAnalyzer 計算到 GC roots 的最短強引用路徑,並肯定是否有泄露。若是有的話,建立致使泄露的引用鏈。
    • 計算結果傳遞到 APP 進程中的 DisplayLeakService 中, 並以通知的形式展現出來。
  • 如何修復內存泄漏微信

    要修復某個內存泄漏,您須要查看該鏈並查找致使泄漏的那個引用,即在泄漏時哪一個引用本應該被清除的。LeakCanary以紅色下劃線突出顯示可能致使泄漏的引用。app

  • 如何複製 leak trace 信息

    能夠經過 logcat 或經過 Leaks App 的菜單複製·

  • Android SDK可能致使泄漏嗎

    是。 在AOSP以及製造商實現中,已經存在許多已知的內存泄漏。 當發生這樣的泄漏時,做爲應用程序開發人員,您幾乎沒法解決此問題。
    出於這個緣由,LeakCanary 有一個內置的已知Android漏洞列表可供忽略:AndroidExcludedRefs.java。

  • 如何經過 leak trace 挖掘泄漏信息

    有時 leak trace 是不夠的,您須要使用 MATYourKit 挖掘 heap dump。 如下是在堆轉儲中找到泄漏實例的方法:

    • 查找 com.squareup.leakcanary.KeyedWeakReference 的全部實例
    • 對於其中的每個,請查看 key 字段。
    • 找到 key 字段等於 LeakCanary 報告的 the reference key 的 KeyedWeakReference
    • 找到的那個 KeyedWeakReference 的 referent 字段就是您內存泄漏的對象。
    • 此後,問題就掌握在你手中。A good start 是查看 GC Roots的最短路徑(除了弱引用)。
  • 如何修復構建錯誤

    • 若是leakcan-android不在Android Studio的 external libraries 列表中,可是 leakcanary-analyzer 和 leakcanary-watcher 卻存在在那裏:嘗試作一個 Clean Build。 若是仍然存在問題,請嘗試經過命令行構建。
    • error: package com.squareup.leakcanary does not exist: 若是您有其餘 build types 而不是 debug 和 release,則還須要爲這些構建類型添加特定的依賴項(xxxCompile)。
  • LeakCanary添加了多少個方法

    • 若是您使用ProGuard,答案爲9或0。
    • LeakCanary 只應在調試版本中使用,並必定要在發佈版本中禁用。咱們爲您的發佈版本提供了一個特殊的空依賴項:leakcanary-android-no-op
    • LeakCanary 的完整版本更大,毫不應在您的 release 版本中發佈。
  • 誰在推進 LeakCanary

    LeakCanary由 @pyricau建立並開源,目前由@jrodbx@JakeWharton@pyricau維護。

  • 爲何叫 LeakCanary

    LeakCanary這個名稱是參考 煤礦中的金絲雀[canary in a coal mine],由於LeakCanary是一個用於經過提早預警危險[advance warning of a danger]來檢測風險[detect risks]的哨兵[sentinel]

  • Instant Run可能觸發無效的 leaks

    啓用Android Studio的即時運行功能可能會致使LeakCanary報告無效的內存泄漏。 請參閱 Android Issue Tracker 上的問題#37967114(https://issuetracker.google.com/issues/37967114)。

  • 我知道我有泄漏,爲何通知不顯示

    你是否 attached to a debugger? LeakCanary在調試時忽略泄漏檢測以免誤報。

原理分析

參考

JVM如何斷定一個對象是垃圾對象?

JVM採用圖論的可達遍歷算法來斷定一個對象是不是垃圾對象,若是對象A是可達的,則認爲該對象是被引用的,GC不會回收;若是對象A或者塊B(多個對象引用組成的對象塊)是不可達的,那麼該對象或者塊則斷定是不可達的垃圾對象,GC會回收。

內存泄漏的檢測機制:

LeakCanary經過ApplicationContext統一註冊監聽的方式,來監察全部的Activity生命週期,並在Activity的onDestroy時,執行RefWatcher的watch方法,該方法的做用就是檢測本頁面內是否存在內存泄漏問題。

Activity檢測機制是什麼?

經過application.registerActivityLifecycleCallbacks來綁定Activity生命週期的監聽,從而監控全部Activity; 在Activity執行onDestroy時,開始檢測當前頁面是否存在內存泄漏,並分析結果。所以,若是想要在不一樣的地方都須要檢測是否存在內存泄漏,須要手動添加。

檢測的流程:

  • 移除不可達引用,若是當前引用不存在了,則不繼續執行
  • 手動觸發GC操做,gcTrigger中封裝了gc操做的代碼
  • 再次移除不可達引用,若是引用不存在了,則不繼續執行
  • 若是兩次斷定都沒有被回收,則開始分析這個引用,最終生成HeapDump信息

原理:

  • 弱引用與ReferenceQueue聯合使用,若是弱引用關聯的對象被回收,則會把這個弱引用加入到ReferenceQueue中;經過這個原理,能夠看出removeWeaklyReachableReferences()執行後,會對應刪除KeyedWeakReference的數據。若是這個引用繼續存在,那麼就說明沒有被回收。
  • 爲了確保最大保險的斷定是否被回收,一共執行了兩次回收斷定,包括一次手動GC後的回收斷定。兩次都沒有被回收,很大程度上說明了這個對象的內存被泄漏了,但並不能100%保證;所以LeakCanary是存在極小程度的偏差的。

內存泄漏檢測機制是什麼?

KeyedWeakReference與ReferenceQueue聯合使用,在弱引用關聯的對象被回收後,會將引用添加到ReferenceQueue;清空後,能夠根據是否繼續含有該引用來斷定是否被回收;斷定回收, 手動GC, 再次斷定回收,採用雙重斷定來確保當前引用是否被回收的狀態正確性;若是兩次都未回收,則肯定爲泄漏對象。

總結下流程就是

  • 斷定是否回收(KeyedWeakReference是否存在該引用), Y -> 退出, N -> 向下執行
  • 手動觸發GC
  • 斷定是否回收, Y -> 退出, N-> 向下執行
  • 兩次未被回收,則分析引用狀況:
    • humpHeap : 這個方法是生成一個文件,來保存內存分析信息
    • analyze: 執行分析

內存泄漏的軌跡生成機制:

LeakCanary採用了MAT對內存信息進行分析,並生成結果。其中在分析時,分爲findLeakingReference與findLeakTrace來查找泄漏的引用與軌跡,根據GCRoot開始按樹形結構依次生成當前引用的軌跡信息。

自定義 LeakCanary

  • 如何觀察具備生命週期的對象

    在您的應用程序中,您可能有其餘具備生命週期的對象,例如Fragment,Service,Dagger組件等。可使用RefWatcher來監視應該進行垃圾回收的實例:refWatcher.watch(schrodingerCat);

  • 使用 no-op 依賴

    release 版本的leakcanary-android-no-op依賴項僅包含LeakCanary和RefWatcher類。若是您要自定義LeakCanary,您須要確保自定義僅出如今 debug 版本中,由於它可能會引用leakcanary-android-no-op依賴項中不存在的類。

  • 自定義圖標和標籤

    DisplayLeakActivity附帶了一個默認圖標和標籤,您能夠經過在應用中提供R.mipmap.leak_canary_iconR.string.leak_canary_display_activity_label來更改它:

  • install 方法的默認邏輯

public static RefWatcher install(Application application) {
   return refWatcher(application)
      .listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}
  • 不監測特定的Activity
    默認狀況下,LeakCanary會監視全部的Activity。 您能夠自定義 installation steps 以執行不一樣的操做,例如忽略某種類型Activity的泄漏:
RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .watchActivities(false)
      .buildAndInstall();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
   @Override
   public void onActivityDestroyed(Activity activity) {
      if (activity instanceof IgnoreActivity) {
         return;
      }
      refWatcher.watch(activity);
   }
   //...
});
  • 在運行時打開和關閉 LeakCanary
refWatcher = LeakCanary.refWatcher(this)
      .heapDumper(getHeapDumper()) //在運行時開啓和關閉LeakCanary
      .buildAndInstall();
public TogglableHeapDumper getHeapDumper() {
    if (heapDumper == null) {
        LeakDirectoryProvider leakDirectoryProvider = LeakCanaryInternals.getLeakDirectoryProvider(this);
        AndroidHeapDumper defaultDumper = new AndroidHeapDumper(this, leakDirectoryProvider);
        heapDumper = new TogglableHeapDumper(defaultDumper);
    }
    return heapDumper;
}
public class TogglableHeapDumper implements HeapDumper {
    private final HeapDumper defaultDumper;
    private boolean enabled = true;

    public TogglableHeapDumper(HeapDumper defaultDumper) {
        this.defaultDumper = defaultDumper;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public File dumpHeap() {
        return enabled ? defaultDumper.dumpHeap() : HeapDumper.RETRY_LATER;
    }
}
MyApplication.app().getHeapDumper().setEnabled(false);

測試案例

Application

public class MyApplication extends Application {
    private RefWatcher refWatcher;
    private static MyApplication app;
    private TogglableHeapDumper heapDumper;

    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            Log.i("bqt", "此進程是專用於LeakCanary進行堆分析用的。您不該該在此進程中初始化您的應用。");
            return;
        }

        refWatcher = LeakCanary.refWatcher(this)
                .watchActivities(true)  //默認爲true,會監視全部Activity,你能夠設置爲false而後再指定要監測的Activity
                .watchFragments(true) //默認爲true,會監視 native Fragment,若是添加了support依賴,則也會監視support中的Fragment
                .watchDelay(1, TimeUnit.SECONDS) //設置應該等待多長時間,直到它檢查跟蹤對象是否已被垃圾回收
                .maxStoredHeapDumps(7) //設置LeakCanary最多能夠保存的 heap dumps 個數,默認爲7
                .excludedRefs(getExcludedRefs()) //忽略特定的引用,這個垃圾東西設置後老是不生效
                .heapDumper(getHeapDumper()) //在運行時開啓和關閉LeakCanary
                //.listenerServiceClass() //能夠更改默認行爲以將 leak trace 和 heap dump 上載到您選擇的服務器。
                .buildAndInstall();
        app = this;
    }

    private ExcludedRefs getExcludedRefs() {
        return AndroidExcludedRefs.createAppDefaults()//通過大量測試,我感受TMD徹底忽略不了Activity和Fragment中內存泄漏
                .instanceField("com.bqt.test.Single", "imageView") //類名,字段名
                .staticField("com.bqt.test.StaticLeakActivity", "bitmap") //類名,靜態字段名
                .clazz("com.bqt.test.StaticLeakActivity") //忽略提供的類名的全部子類的全部字段和靜態字段
                .thread("Thread-10086") //忽略指定的線程,通常主線程名爲【main】,子線程名爲【Thread-整數】
                .build(); //忽略的引用若是又經過watch手動監測了,則仍會監測其內存泄漏狀況
    }

    public static MyApplication app() {
        return app;
    }

    public RefWatcher getRefWatcher() {
        return refWatcher;
    }

    public TogglableHeapDumper getHeapDumper() {
        if (heapDumper == null) {
            LeakDirectoryProvider leakDirectoryProvider = LeakCanaryInternals.getLeakDirectoryProvider(this);
            AndroidHeapDumper defaultDumper = new AndroidHeapDumper(this, leakDirectoryProvider);
            heapDumper = new TogglableHeapDumper(defaultDumper);
        }
        return heapDumper;
    }
}

MainActivity

public class MainActivity extends FragmentActivity implements AdapterView.OnItemClickListener {
    private FrameLayout frameLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView listView = new ListView(this);
        String[] array = {"靜態成員致使的內存泄漏",
                "單例致使的內存泄漏:Fragment",
                "禁用 LeakCanary",
                "",};
        listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, Arrays.asList(array)));
        listView.setOnItemClickListener(this);
        frameLayout = new FrameLayout(this);
        frameLayout.setId(R.id.fragment_id);
        listView.addFooterView(frameLayout);
        setContentView(listView);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        switch (position) {
            case 0:
                startActivity(new Intent(this, StaticLeakActivity.class));
                break;
            case 1:
                getSupportFragmentManager().beginTransaction()
                        .add(frameLayout.getId(), new SingleLeakFragment(), "SingleLeakFragment")
                        .commit();
                break;
            case 2:
                MyApplication.app().getHeapDumper().setEnabled(false);
                break;
            default:
                break;
        }
    }
}

靜態成員致使的內存泄漏

public class StaticLeakActivity extends Activity {
    private static Bitmap bitmap;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
        imageView.setImageBitmap(bitmap);
        setContentView(imageView);
    }
}


相關信息:

* com.bqt.test.StaticLeakActivity has leaked:
* InputMethodManager$ControlledInputConnectionWrapper.!(mParentInputMethodManager)!
* ↳ InputMethodManager.!(mLastSrvView)!
* ↳ PhoneWindow$DecorView.mContext
* ↳ StaticLeakActivity
* Reference Key: 7f96d2f1-bf17-47e2-84ad-cd5976d72766
* Device: HUAWEI HONOR PLK-UL00 PLK-UL00
* Android Version: 6.0 API: 23 LeakCanary: 1.6.1 26145bf
* Durations: watch=1007ms, gc=149ms, heap dump=1840ms, analysis=6567ms

單例致使的內存泄漏

public class SingleLeakFragment extends Fragment {
    private ImageView imageView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        imageView = new ImageView(getContext());
        return imageView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        imageView.setImageResource(R.drawable.icon);
        Single.SINGLETON.setImageView(imageView);//單例中引用View一樣會致使Activity內存泄漏
    }
}
public enum Single {
    @SuppressLint("StaticFieldLeak")
    SINGLETON; //定義一個枚舉的元素,它就表明了Single的一個實例
    private ImageView imageView;

    public void setImageView(ImageView imageView) {
        this.imageView = imageView;
    }
}


相關信息:

* com.bqt.test.SingleLeakFragment has leaked:
* InputMethodManager$ControlledInputConnectionWrapper.!(mParentInputMethodManager)!
* ↳ InputMethodManager.!(mLastSrvView)!
* ↳ ListView.mOnItemClickListener
* ↳ MainActivity.mFragments
* ↳ FragmentController.mHost
* ↳ FragmentActivity$HostCallbacks.mFragmentManager
* ↳ FragmentManagerImpl.mAdded
* ↳ ArrayList.array
* ↳ array Object[].[0]
* ↳ SingleLeakFragment
* Reference Key: 4877bf10-596c-440f-b69c-5d239f670944
* Device: HUAWEI HONOR PLK-UL00 PLK-UL00
* Android Version: 6.0 API: 23 LeakCanary: 1.6.1 26145bf
* Durations: watch=17245ms, gc=138ms, heap dump=1675ms, analysis=8159ms

使用LeakCanary檢測內存泄露中文翻譯

原文

Nov 18 2015

咱們的 App 曾經遇到不少的內存泄漏致使 OutOfMemoryError 的崩潰,一些甚至是在生產環境。Square 的 Pierre-Yvews Ricau 開發了 LeakCanary 最終解決了這些問題。LeakCanary 是一個幫助你檢測和修復內存泄漏的工具。在這個分享中,Pierre 教授你們如何修復內存泄漏的錯誤,讓你的 App 更穩定和可靠。

介紹 (0:00)

你們好,我是 Pierre-Yvews Ricau (叫我 PY 就行),如今在 Square 工做。

Square 出了一款名爲:Square Register 的 App, 幫助你用移動設備完成支付。在用這個 App 的時候,用戶先要登錄他的我的帳號。

不幸的是,在簽名頁面有的時候會由於內存溢出而出現崩潰。老實說,這個崩潰來的太不是時候了 — 用戶和商家都沒法確認交易是否完成了,更況且是在和錢打交道的時候。咱們也強烈的意識到,咱們須要處理下內存溢出或者內存泄露這種事情了。

內存泄漏:非技術講解 (1:40)

我想要聊的內存泄露解決方案是: LeakCanary。

LeakCanary 是一個能夠幫助你發現和解決內存泄露的開源工具。

可是到底什麼是內存泄露呢?咱們從一個非技術角度來開始,先來舉個例子。
...
有外部的引用指向了本不該該再指向的對象。相似這樣的小規模的內存泄露堆積之後就會形成大麻煩。

LeakCanary 救援 (3:47)

這就是咱們爲何要開發 LeakCanary。

我如今可能已經清楚了 可被回收的 Android 對象應該及時被銷燬。

可是我仍是無法清楚的看到這些對象是否已經被回收掉。有了 LeakCanary 之後,咱們:

給可被回收的 Android 對象上打了智能標記,智能標記能知道他們所指向的對象是否被成功釋放掉。

若是過一小段時間對象依然沒有被釋放,他就會給內存作個快照。LeakCanary 隨後會把結果發佈出來:

幫助咱們查看到內存到底怎麼泄露了,並清晰的向咱們展現那些沒法被釋放的對象的引用鏈。

舉個具體的例子:在咱們的 Square App 裏的簽名頁面。用戶準備簽名的時候,App 由於內存溢出出錯崩潰了。咱們不能確認內存錯誤到底出在哪兒了。

簽名頁面持有了一個很大的有用戶簽名的 Bitmap 圖片對象。圖片的大小和用戶手機屏幕大小一致 — 咱們猜想這個有可能會形成內存泄露。首先,咱們能夠配置 Bitmap 爲 alpha 8-bit 來節省內存。這是很常見的一種修復方案,並且效果也不錯。可是並無完全解決問題,只是減小了泄露的內存總量。可是內存泄露依然在哪兒。

最主要的問題是咱們 App 的堆滿了,應該要留有足夠的空間給咱們的簽名圖片,可是因爲不少處的內存泄露疊加在一塊兒佔用了不少內存。

技術講解內存泄漏 (8:06)

假設,我有一個 App,這個 App 點一下就能買一個法棍麪包。

private static Button buyNowButton;

因爲某種緣由,我把這個 button 設置成了 static 的。問題隨之而來:

這個按鈕除非你設置成了null,否則就內存泄露了!

你也許會說:「只是一個按鈕而已,沒啥大不了」。問題是這個按鈕還有一個成員變量:叫 mContext,這個東西指向了一個 Acitvity,Acitivty 又指向了一個 Window,Window 又擁有整個 View 繼承樹。算下來,那但是一大段的內存空間。

靜態的變量是 GC root 類型的一種。垃圾回收器會嘗試回收全部非 GC root 的對象,或者某些被 GC root 持有的對象。因此若是你建立一個對象,而且移除了這個對象的全部指向,他就會被回收掉。可是一旦你將一個對象設置成 GC root,那他就不會被回收掉。

當你看到相似「法棍按鈕」的時候,很顯然這個按鈕持有了一個 Activity 的引用,因此咱們必須清理掉它。當你沉浸在你的代碼的時候,你確定很難發現這個問題。你可能只看到了引出的引用。你能夠知道 Activity 引用了一個 Window,可是誰引用了 Activity?

你能夠用像 IntelliJ 這樣的工具作些分析,可是它並不會告訴你全部的東西。一般,你能夠把這些 Object 的引用關係組織成圖,可是是個單向圖。

分析堆 (10:16)

咱們能作些什麼呢?咱們來作個快照。

咱們拿出全部的內存而後導出到文件裏,這個文件會被用來分析和解析堆結構。
其中一個工具叫作 Memory Analyzer,也叫 MAT。
它會經過 dump 的內存,而後分析全部存活在內存中的對象和類。

你能夠用 SQL 對他作些查詢,相似以下:

SELECT * FROM INSTANCEOF android.app.Activity a WHERE a.mDestroyed = true

這條語句會返回全部的狀態爲 destroyed 的實例。一旦你發現了泄露的 Activity,你能夠執行 merge_shortest_paths 的操做來計算出最短的 GC root 路徑。從而找出阻止你 Acitivty 釋放的那個對象。

之因此說要 「最短路徑」,是由於一般從一個 GC root 到 Acitivty,有不少條路徑能夠到達。好比說:個人按鈕的 parent view,一樣也持有一個 mContext 對象。

當咱們看到內存泄露的時候,咱們一般不須要去查看全部的這些路徑。咱們只須要最短的一條。那樣的話,咱們就排除了噪音,很快的找到問題所在。

LeakCanary 救你於水火 (12:04)

有 MAT 這樣一個幫咱們發現內存泄露的工具是個很棒的事情。可是在一個正在運行的 App 的上下文中,咱們很難像咱們的用戶發現泄露那樣發現問題所在。咱們不能要求他們在作一遍相同操做,而後留言描述,再把 70MB+ 的文件發回給咱們。咱們能夠在後臺作這個,可是並不 Cool。咱們指望的是,咱們可以儘早的發現泄露,好比在咱們開發的時候就發現這些問題。這也是 LeakCanary 誕生的意義。

一個 Activity 有本身生命週期。你瞭解它是如何被建立的,如何被銷燬的,你指望他會在 onDestroy() 函數調用後,回收掉你全部的空閒內存。若是你有一個可以檢測一個對象是否被正常的回收掉了的工具,那麼你就會很驚訝的喊出:「這個可能形成內存泄露!它本該被回收掉,但卻沒有被垃圾回收掉!」

Activity 無處不在。不少人都把 Activity 當作神級 Object 通常的存在,由於它能夠操做 Services,文件系統等等。常常會發生對象泄漏的狀況,若是泄漏對象還持有 context 對象,那 context 也就跟着泄漏了。

Resources resources = context.getResources();
LayoutInflater inflater = LayoutInflater.from(context);
File filesDir = context.getFilesDir();
InputMethodManager inputMethodManager =(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

LeakCanary API 演練 (13:32)

咱們回過頭來再看看智能標記smart pin,咱們但願知道的是當生命後期結束後,發生了什麼。幸運的時,LearkCanary有一個很簡單的 API。

第一步:建立 RefWatcher。這裏的Ref 實際上是 Reference 的縮寫。給 RefWatcher 傳入一個對象的實例,它會檢測這個對象是否被成功釋放掉。

RefWatcher refWatcher = LeakCanary.install(this);

第二步:監聽 Activity 生命週期。而後,當 onDestroy 被調用的時候,咱們傳入 Activity。

refWatcher.watch(this);// Make sure you don’t get installed twice.

什麼是弱引用 (14:17)

想要了解這個是怎麼工做的,我得先跟你們聊聊弱引用Weak References

我剛纔提到過靜態域的變量會持有Activity 的引用。因此剛纔說的「下單」按鈕就會持有 mContext 對象,致使 Activity 沒法被釋放掉。這個被稱做強引用Strong Reference

一個對象能夠有不少的強引用,在垃圾回收過程當中,當這些強引用的個數總和爲零的時候,垃圾回收器就會釋放掉它。
弱引用就是一種不增長引用總數的持有引用方式,垃圾回收期是否決定要回收一個對象,只取決於它是否還存在強引用。

因此說,若是咱們:

將咱們的 Activity 持有爲弱引用,一旦咱們發現弱引用持有的對象已經被銷燬了,那麼這個 Activity 就已經被垃圾回收器回收了。
不然,那能夠大概肯定這個 Activity 已經被泄露了。

弱引用的主要目的是爲了作 Cache,並且很是有用。主要就是告訴 GC,儘管我持有了這個對象,可是若是一旦沒有對象在用這個對象的時候,GC 就能夠在須要的時候銷燬掉。

在下面的例子中,咱們繼承了 WeakReference:

final class KeyedWeakReference extends WeakReference<Object> {
    public final String key; //惟一標識符
    public final String name;

    KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
        super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
        this.key = checkNotNull(key, "key");
        this.name = checkNotNull(name, "name");
    }
}

你能夠看到,咱們給弱引用添加了一個 Key,這個 Key 是一個惟一字符串。想法是這樣的:當咱們解析一個heap dump文件的時候,咱們能夠遍歷全部的 KeyedWeakReference 實例,而後找到對應的 Key。

首先,咱們建立一個 weakReference,而後咱們寫入『一下子,我須要檢查弱引用』。(儘管一下子可能就是幾秒後)。當咱們調用 watch 函數的時候,其實就是發生了這些事情。

public void watch(Object watchedReference, String referenceName) {
   checkNotNull(watchedReference, "watchedReference");
   checkNotNull(referenceName, "referenceName");
   if (debuggerControl.isDebuggerAttached()) return;
   final long watchStartNanoTime = System.nanoTime();
   String key = UUID.randomUUID().toString();
   retainedKeys.add(key);
   final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

   watchExecutor.execute(() -> ensureGone(reference, watchStartNanoTime));
}

在這一切的背後,咱們調用了 System.GC (免責聲明— 咱們本不該該去作這件事情)。然而,這是一種告訴垃圾回收器:『Hey,垃圾回收器,如今是一個不錯的清理垃圾的時機。』,而後咱們再檢查一遍,若是發現有些對象依然存活着,那麼可能就有問題了。咱們就要觸發 heap dump 操做了。

HAHA 內存分析器 (16:55)

親手作 heap dump 是件超酷的事情。當我親手作這些的時候,花了不少時間和功夫。我每次都是作相同的操做:

下載 heap dump 文件,在內存分析工具裏打開它,找到實例,而後計算最短路徑。

可是我很懶,我根本不想一次次的作這個。(咱們都很懶對吧,由於咱們是開發者啊!)

我本能夠爲內存分析器寫一個 Eclipse 插件,可是 Eclipse 插件機制太糟糕了。後來我靈機一動,我其實能夠把某個 Eclipse 的插件,移除 UI,利用它的代碼。

HAHA 是一個無 UI Android 內存分析器。基本上就是把另外一我的寫的代碼從新打包。開始的時候,我就是 fork 了一份別的代碼而後移除了UI部分。兩年前,有人從新 fork 了個人代碼,而後添加了 Android 支持。又過了兩年,我才發現這我的的倉儲,而後我又從新打包上傳到了 maven center

我最近根據 Android Studio 修改了代碼實現。代碼還說的過去,還會繼續維護。

LeakCanary 的實現 (19:19)

咱們有本身的庫去解析 heap dump 文件,並且實現的很容易。咱們打開 heap dump,加載進來,而後解析。而後咱們根據 key 找到咱們的引用。而後咱們根據已有的 Key 去查看擁有的引用。咱們拿到實例,而後獲得對象圖,再反向推導發現泄漏的引用。

以上(下)全部的工做都發生在 Android 設備上。
  • 當 LeakCanary 探測到一個 Activity 已經被銷燬掉,而沒有被垃圾回收器回收掉的時候,它就會強制導出一份 heap dump 文件存在磁盤上。
  • 而後開啓另一個進程去分析這個文件獲得內存泄漏的結果。若是在同一進程作這件事的話,可能會在嘗試分析堆內存結構的時候而發生內存不足的問題。
  • 最後,你會獲得一個通知,點擊一下就會展現出詳細的內存泄漏鏈。並且還會展現出內存泄漏的大小,你也會很明確本身解決掉這個內存泄漏後到底可以解救多少內存出來。

LeakCanary 也是支持 API 的,這樣你就能夠添加內存泄漏的回調,比方說能夠把內存泄漏問題傳到服務器上
用上 API 之後,咱們的程序崩潰率下降了 94%!簡直棒呆!

Debug 一個真實的例子 (22:12)

這個是 Android 4年前的一次代碼修改留下的問題,當時是爲了修復另外一個 bug,然而帶來了沒法避免的內存泄漏。咱們也不知道什麼時候能被修復。

忽略 SDK Crashes (28:10)

一般來講,老是有些內存泄漏是你沒法修復的。咱們某些時候須要忽略掉這些沒法修復的內存泄漏提醒。在 LeakCanary 裏,有內置的方法去忽略沒法修復的問題。

我想要重申一下,LeakCanary 只是一個開發工具。不要將它用到生產環境中。一旦有內存泄漏,就會展現一個通知給用戶,這必定不是用戶想看到的。

咱們即使用上了 LeakCanary 依然有內存溢出的錯誤出現。咱們的內存泄露依然有多個。有沒有辦法改變這些呢?

LeakCanary 的將來 (29:14)

public class OomExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler defaultHandler;
    private final Context context;

    public OomExceptionHandler(Thread.UncaughtExceptionHandler defaultHandler, Context context) {...}

    @Override
    public void UncaughtException(Thread thread, Throwable ex) {
        if (containsOom(ex)) {
            File heapDumpFile = new File(context.getFilesDir(), "out-of-memory.hprof");
            Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
        }
        defaultHandler.uncaughtException(thread, ex);
    }

    private boolean containsOom(Throwable ex) {...}
}

這是一個 Thread.UncaughtExceptionHandler,你能夠將線程崩潰委託給它,它會導出 heap dump 文件,而且在另外一個進程裏分析內存泄漏狀況。

有了這個之後,咱們就能作一些好玩兒的事情了,好比:列出全部的應該被銷燬卻依然在內存裏存活的 Activity,而後列出全部的 Detached View。咱們能夠依此來爲泄漏的內存按重要性排序。

我實際上已經有一個很簡單的 Demo 了,是我在飛機上寫的。尚未發佈,由於還有些問題,最嚴重的問題是沒有足夠的內存去解析 heap dump 文件。想要修復這個問題,得想一想別的辦法。好比採用 stream 的方法去加載文件等等。

Q&A (31:50)

Q: LeakCanary 能用於 Kotlin 開發的 App?
PY: 我不知道,可是應該是能夠的,畢竟到最後他們都是字節碼,並且 Kotlin 也有引用。

Q:大家是在 Debug 版本一直開啓 LeakCanary 麼?仍是隻在最後的某些版本開啓作作測試
PY: 不一樣的人有不一樣的方法,咱們一般是一直都開着的。

備註:這篇文章是2015年做者視頻內容的中文翻譯,有一些內容可能已經改變了。

2018-10-2

相關文章
相關標籤/搜索