Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
LeakCanary 內存泄漏 監測 性能優化 簡介 原理 MD
GitHub:https://github.com/square/leakcanary
Demo地址:https://github.com/baiqiantao/LeakCanaryTest.git
目錄
介紹
簡單使用
更多介紹
原理分析
自定義 LeakCanary
測試案例
Application
MainActivity
靜態成員導致的內存泄漏
單例導致的內存泄漏
使用LeakCanary檢測內存泄露中文翻譯
介紹 (0:00)
內存泄漏:非技術講解 (1:40)
LeakCanary 救援 (3:47)
技術講解內存泄漏 (8:06)
分析堆 (10:16)
LeakCanary 救你於水火 (12:04)
LeakCanary API 演練 (13:32)
什么是弱引用 (14:17)
HAHA 內存分析器 (16:55)
LeakCanary 的實現 (19:19)
Debug 一個真實的例子 (22:12)
忽略 SDK Crashes (28:10)
LeakCanary 的未來 (29:14)
Q&A (31:50)
介紹
A memory leak detection
內存泄露檢測
library for Android and Java.
A small leak will sink a great ship. -- Benjamin Franklin
千里之堤, 毀於蟻穴。 -- 《韓非子·喻老》
簡單使用
添加依賴:
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 文件中。
- 在另外一個獨立的進程中啟動
HeapAnalyzerService
,HeapAnalyzer
使用 HAHA 解析 heap dump 。 - 得益於唯一的
reference key
,HeapAnalyzer
在 heap dump 中找到KeyedWeakReference
,並且定位 leaking reference。 HeapAnalyzer
計算到GC roots
的最短強引用路徑,並確定是否有泄露。如果有的話,創建導致泄露的引用鏈。- 計算結果傳遞到 APP 進程中的
DisplayLeakService
中, 並以通知的形式展示出來。
- 通過
如何修復內存泄漏
要修復某個內存泄漏,您需要查看該鏈並查找導致泄漏的那個引用,即在泄漏時哪個引用本應該被清除的。LeakCanary以紅色下划線突出顯示可能導致泄漏的引用。
如何復制 leak trace 信息
可以通過 logcat 或通過 Leaks App 的菜單復制·
Android SDK可能導致泄漏嗎
是。 在AOSP以及制造商實現中,已經存在許多已知的內存泄漏。 當發生這樣的泄漏時,作為應用程序開發人員,您幾乎無法解決此問題。
出於這個原因,LeakCanary 有一個內置的已知Android漏洞列表可供忽略:AndroidExcludedRefs.java。如何通過 leak trace 挖掘泄漏信息
有時 leak trace 是不夠的,您需要使用 MAT 或 YourKit 挖掘 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_icon
和R.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