什么是內存泄漏?
每個應用程序都需要內存作為資源來完成其工作。為了確保Android中的每個應用都有足夠的內存,Android系統需要高效管理內存分配。當內存不足時,Android運行時會觸發垃圾收集(GC)。GC的目的是通過清理不再有用的對象來回收內存。它通過三個步驟實現它。
- 從GC根開始遍歷內存中的所有對象引用,并標記具有GC根引用的活動對象。
- 所有未標記的對象(垃圾)都會從內存中清除掉。
- 重新排列活著的對象
簡而言之,為用戶提供服務的所有內容都應該保存在內存中,其他內容都會從內存中清除以釋放資源。
但是,如果代碼寫得很糟糕,未使用的對象以某種方式從可訪問對象中引用,則GC會將未使用的對象標記為有用的對象,因此無法將其刪除。這被稱為內存泄漏。
為什么內存泄漏不好?
沒有任何對象應該長時間停留在內存中。它們占用寶貴的資源,否則這些資源可用于為用戶提供有實際價值的東西。特別地,對于Android,它會導致以下問題。
1。發生內存泄漏時可用內存不足。結果,Android系統將觸發更頻繁的GC事件。GC事件是世界末日事件。這意味著GC發生時,UI的呈現和事件處理將停止。Android有一個16ms的繪圖窗口。當GC需要很長時間時,Android就會開始丟幀。一般來說,100到200ms是一個閾值,在此以上用戶在應用程序中會感覺到緩慢[1]。
在Android中,應用程序響應性由活動管理器(Activity Manager)和窗口管理器(Window Manager )系統服務進行監視。當Android檢測到以下某種情況時,它將顯示特定應用程序的ANR對話框:[1]:
- 在5秒內沒有對輸入事件(例如按鍵或屏幕觸摸事件)做出響應。
-
BroadcastReceiver
在10秒內尚未完成執行。
我相信沒有用戶會喜歡看到這個應用程序沒有響應的彈出框。
2。當你的應用程序有內存泄漏時,它不能從未使用的對象聲明內存。因此,它會問Android系統要更多的內存。但是有一個限制。系統最終會拒絕為你的應用分配更多內存。發生這種情況時,應用程序用戶將會發生內存不足(out-of-memory)的崩潰。當然沒有人喜歡崩潰。用戶可能會卸載你的應用程序或開始給你的應用程序不好的評論。
3。內存泄漏問題在QA測試中很難找到。他們很難重現。而崩潰報告通常很難推理,因為它可能在Android系統拒絕內存分配的任何時間,任何地方發生。
如何識別內存泄漏?
發現泄漏需要對GC工作原理有很好的理解。它需要努力編寫代碼并做代碼審查。但是在Android中,有一些很好的工具可以幫助你識別可能的泄漏,或者在某些代碼看起來可疑時確定是否有泄漏。
1。 來自Square的Leak Canary 是一款檢測應用程序內存泄漏的好工具。它會在你的應用中創建對活動(activities)的弱引用。(你也可以通過添加到任何其他對象的觀察點來自定義它。)然后它檢查GC之后引用是否被清除。如果沒有,它將堆(heap)轉儲到一個.hprof文件并分析它,以確認是否有泄漏。如果有,它會顯示一個通知,并在一個單獨的應用程序中顯示泄漏發生的引用樹。你可以在這篇文章中找到更多關于Leak Canary的信息: LeakCanary:檢測所有內存泄漏 。我強烈建議你將Leak Canary安裝到你的開發人員/測試版本中。它可以幫助開發人員和QA在你的應用到達用戶手中之前找到內存泄漏。
2。Android Studio有一個方便的工具來檢測內存泄漏。如果你懷疑你的應用程序中有一段代碼可能會泄漏一個活動(activity),你可以執行此操作。
步驟1:編譯并在連接到你計算機的設備或仿真器上運行調試版本。
步驟2:轉到可疑活動(activity),然后返回到上一個活動(activity),這將從任務堆棧中彈出可疑活動(activity)。
第3步:在Android Studio -> Android Monitor窗口 -> Memory 部分,單擊Initiate GC
按鈕。然后點擊Dump Java Heap
按鈕。
第4步:按下Dump Java Heap
按鈕時,Android Studio將打開轉儲的.hprof
文件。在hprof文件查看器中,有幾種方法可以檢查內存泄漏。你可以使用右上角的Analyzer Tasks
工具自動檢測泄漏的活動。或者你可以從左上角的切換器將視圖模式切換到Package Tree View
,找到應該銷毀的活動(activity)。檢查活動對象的Total Count
。如果有一個或多個實例,則表示存在泄漏。
第5步:一旦找到泄漏的活動,請檢查底部的引用樹,找出哪些對象正在引用應該已經死掉了(should-have-been-dead)的活動。
你可以從HPROF Viewer and Analyzer中找到有關Android Studio功能的更多信息。
什么是常見的泄漏模式?
有很多方法可以導致Android中的內存泄漏。總而言之,主要有三類。
- 將活動泄漏到靜態引用(static reference)
- 將活動泄漏到工作者線程(worker thread)
- 泄漏線程本身
在我的Github的repoSinsOfMemoryLeaks中,我做了一個應用程序,它以各種方式泄漏內存。
frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android開發中一些常見的內存泄漏模式以及如何修復/避免它們
在Leak
分支中,你可以看到具有各種內存泄漏的所有代碼。你也可以在設備或仿真器上運行它,并使用前面提到的工具來跟蹤泄漏。在FIXED
分支中,你將看到泄漏是如何修復的。如果你不確信,你可以再次使用前面提到的工具來查看泄漏是否真的被修復。這兩個分支具有不同的應用ID,因此你可以將它們安裝在同一設備上同時試用。
現在我將迅速瀏覽3個主要類別中不同方式的泄漏。
將活動泄漏到靜態引用
只要你的應用程序在內存中,靜態引用就會存在。一個活動的生命周期通常會在應用程序的生命周期中被釋放并重新創建多次。如果你直接或間接從靜態引用處引用活動,活動在被銷毀后不會被垃圾收集。一個活動的大小范圍可以從幾千字節到許多兆字節,具體取決于它的內容。如果它具有大視圖層次結構或高分辨率圖像,則會導致大量內存泄漏。
這個類別的一些泄漏可以是
將活動泄漏到工作者線程
一個工作者線程也可以放生(out-live)一個活動。如果你直接或間接地從活動時間比較長的工作者線程中引用活動(Activity),就會泄漏活動(Activity)對象。這個類別的幾個方式可以是
同樣的原則適用于其他線程技術,如thread pool
或ExecutorService
。
泄漏線程本身
每當你從一個活動開始一個工作者線程時,你就有責任自己管理工作者線程。因為工作者線程的活動時間可能比活動時間長,所以當活動被銷毀時,應該正確停止工作者線程。如果你忘記了這一點,你就冒著泄漏工作者線程的危險。示例在這里。
特定泄漏的影響是什么?
理想情況下,你應該避免編寫任何導致內存泄漏的代碼,并修復應用程序中存在的所有內存泄漏。但實際上,如果你正在處理舊的代碼庫并需要優先處理不同任務(包括修復內存泄漏),則可以在以下幾個方面評估嚴重性。
1。泄漏的內存有多大?
并非所有的內存泄漏都是相同的。一些泄漏幾千字節;有些可能會泄漏很多兆字節。你可以使用前面提到的工具找出它,并確定內存泄漏的大小是否對你的用戶群的設備至關重要。
2。泄漏的對象在內存中駐留多長時間?
只要工作者線程自己存在,工作者線程中的一些泄漏就會一直存在。你應該檢查你的工作者線程在最糟糕的情況下會持續多久。在我的代碼示例中,我在工作者線程中有無限循環,所以它永遠持有泄漏對象的內存。但實際上,大多數工作者線程都會執行簡單的任務,例如訪問文件系統或進行網絡調用,這可能是短暫的,或者你通常會設置超時。泄漏的最大時間是確定修復內存泄漏優先級的考慮因素。
3。有多少對象可以泄漏?
有些內存泄漏只泄露一個對象,比如我的repo中靜態引用示例中的對象。只要創建新活動,該靜態引用就開始引用新活動。泄露的舊活動很明顯會被垃圾收集。所以最大泄漏總是一個活動實例的大小。但是,其他泄漏,在創建新對象后會不斷泄漏。在Leaking Threads示例中,該活動每次創建時都會泄漏一個線程。所以如果你旋轉設備20次,就會有20個工作者線程被泄漏。這可能是非常糟糕的,因為如果應用程序不斷泄漏新的實例,該應用程序將很快將設備上的所有可用內存用完。即使一個對象實例相對較小,我也很可能會修復所有這種類型的泄漏。
如何修復/避免它?
看看我的repo的FIXED
分支 。
關鍵要點是:
- 當你決定在你的活動類中有一個靜態變量時要非常小心。它真的有必要嗎?是否有可能靜態變量直接或間接引用活動(間接可以引用內部類對象,附加視圖(attached view)等)?如果是這樣,你是否在Activity的onDestroy時清除引用呢?
- 當你將活動作為偵聽器(listener)傳遞給單例對象或x管理器實例時,請確保你了解其他對象對你傳入的活動實例的作用。如果需要,請在Activity onDestroy上清除引用(將偵聽器(listener)設置為null)。
- 在活動類中創建內部類時,如果可能,請將其設置為靜態。內部類和匿名類具有對包含類的隱式引用。因此,如果內部/匿名類的實例比包含類的實例壽命更長,那么你遇到了麻煩。例如,如果你創建一個匿名可運行類(anonymous runnable class)并將其傳遞給工作者線程或匿名處理程序類(anonymous handler class),并使用它將任務傳遞給其他線程,則可能會泄漏包含的類對象。為了避免泄漏風險,請使用靜態類而不是內部/匿名類。
- 如果你正在編寫單例或x管理器類,則需要存儲偵聽器(listener)實例的引用,并且不能控制類的用戶如何管理引用,可以請使用
WeakReference
作為偵聽器(listener)引用。WeakReference
并不妨礙他們的引用被GC清除并回收[4]。雖然這個功能在防止內存泄漏方面聽起來不錯,但它也可能是一個副作用,因為不能保證被引用的對象在需要時處于活動狀態。所以用它作為修復內存泄漏的最后手段。 - 在Activity的onDestroy()中,始終記住要終止你啟動過的工作者線程。
## 總結
我們研究了什么是內存泄漏,它是如何發生的,以及它在Android系統中造成的后果。然后我們介紹了兩種檢測和識別內存泄漏的工具,分析了Android中常見的內存泄漏模式,如何評估泄漏的嚴重程度以及如何避免/修復常見泄漏。不要忘了查看我的Github倉庫中常見內存泄漏模式和修復的代碼示例。快樂制作Android應用,每個人:)
frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android開發中一些常見的內存泄漏模式以及如何修復/避免它們
參考
[1] 保持你的應用程序響應性
[2] Java內存管理
[3] HPROF查看器和分析器
[4] WeakReference