Android中的記憶體洩漏模式

weixin_33751566發表於2018-06-07

原文

3764796-94bcf21a7046a5ee.png

什麼是記憶體洩漏?

每個應用程式都需要記憶體作為資源來完成其工作。為了確保Android中的每個應用都有足夠的記憶體,Android系統需要高效管理記憶體分配。當記憶體不足時,Android執行時會觸發垃圾收集(GC)。GC的目的是通過清理不再有用的物件來回收記憶體。它通過三個步驟實現它。

  1. 從GC根開始遍歷記憶體中的所有物件引用,並標記具有GC根引用的活動物件。
  2. 所有未標記的物件(垃圾)都會從記憶體中清除掉。
  3. 重新排列活著的物件
3764796-2e1cf0f6e3a66914.png
從GC根標記活體物件

簡而言之,為使用者提供服務的所有內容都應該儲存在記憶體中,其他內容都會從記憶體中清除以釋放資源。

但是,如果程式碼寫得很糟糕,未使用的物件以某種方式從可訪問物件中引用,則GC會將未使用的物件標記為有用的物件,因此無法將其刪除。這被稱為記憶體洩漏。

3764796-354a799ba9f48ce2.png
記憶體洩漏[2]

為什麼記憶體洩漏不好?

沒有任何物件應該長時間停留在記憶體中。它們佔用寶貴的資源,否則這些資源可用於為使用者提供有實際價值的東西。特別地,對於Android,它會導致以下問題。

1。發生記憶體洩漏時可用記憶體不足。結果,Android系統將觸發更頻繁的GC事件。GC事件是世界末日事件。這意味著GC發生時,UI的呈現和事件處理將停止。Android有一個16ms的繪圖視窗。當GC需要很長時間時,Android就會開始丟幀。一般來說,100到200ms是一個閾值,在此以上使用者在應用程式中會感覺到緩慢[1]。

3764796-626f2677855f814e.png
Android繪圖視窗[3]
3764796-3be499142c56c396.png
由於頻繁GC而丟幀[3]

在Android中,應用程式響應性由活動管理器(Activity Manager)和視窗管理器(Window Manager )系統服務進行監視。當Android檢測到以下某種情況時,它將顯示特定應用程式的ANR對話方塊:[1]:

  • 在5秒內沒有對輸入事件(例如按鍵或螢幕觸控事件)做出響應。
  • BroadcastReceiver在10秒內尚未完成執行。
3764796-2d73881a697af15a.png
Android未響應(ANR)

我相信沒有使用者會喜歡看到這個應用程式沒有響應的彈出框。

2。當你的應用程式有記憶體洩漏時,它不能從未使用的物件宣告記憶體。因此,它會問Android系統要更多的記憶體。但是有一個限制。系統最終會拒絕為你的應用分配更多記憶體。發生這種情況時,應用程式使用者將會發生記憶體不足(out-of-memory)的崩潰。當然沒有人喜歡崩潰。使用者可能會解除安裝你的應用程式或開始給你的應用程式不好的評論。

3。記憶體洩漏問題在QA測試中很難找到。他們很難重現。而崩潰報告通常很難推理,因為它可能在Android系統拒絕記憶體分配的任何時間,任何地方發生。

如何識別記憶體洩漏?

發現洩漏需要對GC工作原理有很好的理解。它需要努力編寫程式碼並做程式碼審查。但是在Android中,有一些很好的工具可以幫助你識別可能的洩漏,或者在某些程式碼看起來可疑時確定是否有洩漏。

1。 來自Square的Leak Canary 是一款檢測應用程式記憶體洩漏的好工具。它會在你的應用中建立對活動(activities)的弱引用。(你也可以通過新增到任何其他物件的觀察點來自定義它。)然後它檢查GC之後引用是否被清除。如果沒有,它將堆(heap)轉儲到一個.hprof檔案並分析它,以確認是否有洩漏。如果有,它會顯示一個通知,並在一個單獨的應用程式中顯示洩漏發生的引用樹。你可以在這篇文章中找到更多關於Leak Canary的資訊: LeakCanary:檢測所有記憶體洩漏 。我強烈建議你將Leak Canary安裝到你的開發人員/測試版本中。它可以幫助開發人員和QA在你的應用到達使用者手中之前找到記憶體洩漏。

3764796-c77955f6aaca0cd6.png
3764796-cf6d1612d88c3d0c.png
Leak Canary的螢幕截圖

2。Android Studio有一個方便的工具來檢測記憶體洩漏。如果你懷疑你的應用程式中有一段程式碼可能會洩漏一個活動(activity),你可以執行此操作。

步驟1:編譯並在連線到你計算機的裝置或模擬器上執行除錯版本。

步驟2:轉到可疑活動(activity),然後返回到上一個活動(activity),這將從任務堆疊中彈出可疑活動(activity)。

第3步:在Android Studio -> Android Monitor視窗 -> Memory 部分,單擊Initiate GC按鈕。然後點選Dump Java Heap按鈕。

3764796-751138b3c1302449.png

第4步:按下Dump Java Heap按鈕時,Android Studio將開啟轉儲的.hprof檔案。在hprof檔案檢視器中,有幾種方法可以檢查記憶體洩漏。你可以使用右上角的Analyzer Tasks工具自動檢測洩漏的活動。或者你可以從左上角的切換器將檢視模式切換到Package Tree View ,找到應該銷燬的活動(activity)。檢查活動物件的Total Count 。如果有一個或多個例項,則表示存在洩漏。

3764796-211e79464d4572ec.png

第5步:一旦找到洩漏的活動,請檢查底部的引用樹,找出哪些物件正在引用應該已經死掉了(should-have-been-dead)的活動。

你可以從HPROF Viewer and Analyzer中找到有關Android Studio功能的更多資訊。

什麼是常見的洩漏模式?

有很多方法可以導致Android中的記憶體洩漏。總而言之,主要有三類。

  1. 將活動洩漏到靜態引用(static reference)
  2. 將活動洩漏到工作者執行緒(worker thread)
  3. 洩漏執行緒本身

在我的Github的repoSinsOfMemoryLeaks中,我做了一個應用程式,它以各種方式洩漏記憶體。

frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android開發中一些常見的記憶體洩漏模式以及如何修復/避免它們

Leak分支中,你可以看到具有各種記憶體洩漏的所有程式碼。你也可以在裝置或模擬器上執行它,並使用前面提到的工具來跟蹤洩漏。在FIXED分支中,你將看到洩漏是如何修復的。如果你不確信,你可以再次使用前面提到的工具來檢視洩漏是否真的被修復。這兩個分支具有不同的應用ID,因此你可以將它們安裝在同一裝置上同時試用。

3764796-50a5f578bb838a25.png
3764796-5ff0aa74deb079fd.png
3764796-dc6195af241748f6.png

現在我將迅速瀏覽3個主要類別中不同方式的洩漏。

將活動洩漏到靜態引用

只要你的應用程式在記憶體中,靜態引用就會存在。一個活動的生命週期通常會在應用程式的生命週期中被釋放並重新建立多次。如果你直接或間接從靜態引用處引用活動,活動在被銷燬後不會被垃圾收集。一個活動的大小範圍可以從幾千位元組到許多兆位元組,具體取決於它的內容。如果它具有大檢視層次結構或高解析度影像,則會導致大量記憶體洩漏。

這個類別的一些洩漏可以是

  1. 將活動洩漏到靜態檢視
  2. 將活動洩漏到靜態變數
  3. 將活動洩漏到單例物件(singleton object)
  4. 將活動洩漏到活動的內部類的靜態例項

將活動洩漏到工作者執行緒

一個工作者執行緒也可以放生(out-live)一個活動。如果你直接或間接地從活動時間比較長的工作者執行緒中引用活動(Activity),就會洩漏活動(Activity)物件。這個類別的幾個方式可以是

  1. 洩漏活動到一個執行緒
  2. 洩漏活動到處理程式(handler)
  3. 將漏洞活動新增到

同樣的原則適用於其他執行緒技術,如thread poolExecutorService

洩漏執行緒本身

每當你從一個活動開始一個工作者執行緒時,你就有責任自己管理工作者執行緒。因為工作者執行緒的活動時間可能比活動時間長,所以當活動被銷燬時,應該正確停止工作者執行緒。如果你忘記了這一點,你就冒著洩漏工作者執行緒的危險。示例在這裡

特定洩漏的影響是什麼?

理想情況下,你應該避免編寫任何導致記憶體洩漏的程式碼,並修復應用程式中存在的所有記憶體洩漏。但實際上,如果你正在處理舊的程式碼庫並需要優先處理不同任務(包括修復記憶體洩漏),則可以在以下幾個方面評估嚴重性。

1。洩漏的記憶體有多大?

並非所有的記憶體洩漏都是相同的。一些洩漏幾千位元組;有些可能會洩漏很多兆位元組。你可以使用前面提到的工具找出它,並確定記憶體洩漏的大小是否對你的使用者群的裝置至關重要。

2。洩漏的物件在記憶體中駐留多長時間?

只要工作者執行緒自己存在,工作者執行緒中的一些洩漏就會一直存在。你應該檢查你的工作者執行緒在最糟糕的情況下會持續多久。在我的程式碼示例中,我在工作者執行緒中有無限迴圈,所以它永遠持有洩漏物件的記憶體。但實際上,大多數工作者執行緒都會執行簡單的任務,例如訪問檔案系統或進行網路呼叫,這可能是短暫的,或者你通常會設定超時。洩漏的最大時間是確定修復記憶體洩漏優先順序的考慮因素。

3。有多少物件可以洩漏?

有些記憶體洩漏只洩露一個物件,比如我的repo中靜態引用示例中的物件。只要建立新活動,該靜態引用就開始引用新活動。洩露的舊活動很明顯會被垃圾收集。所以最大洩漏總是一個活動例項的大小。但是,其他洩漏,在建立新物件後會不斷洩漏。在Leaking Threads示例中,該活動每次建立時都會洩漏一個執行緒。所以如果你旋轉裝置20次,就會有20個工作者執行緒被洩漏。這可能是非常糟糕的,因為如果應用程式不斷洩漏新的例項,該應用程式將很快將裝置上的所有可用記憶體用完。即使一個物件例項相對較小,我也很可能會修復所有這種型別的洩漏。

如何修復/避免它?

看看我的repo的FIXED分支

關鍵要點是:

  1. 當你決定在你的活動類中有一個靜態變數時要非常小心。它真的有必要嗎?是否有可能靜態變數直接或間接引用活動(間接可以引用內部類物件,附加檢視(attached view)等)?如果是這樣,你是否在Activity的onDestroy時清除引用呢?
  2. 當你將活動作為偵聽器(listener)傳遞給單例物件或x管理器例項時,請確保你瞭解其他物件對你傳入的活動例項的作用。如果需要,請在Activity onDestroy上清除引用(將偵聽器(listener)設定為null)。
  3. 在活動類中建立內部類時,如果可能,請將其設定為靜態。內部類和匿名類具有對包含類的隱式引用。因此,如果內部/匿名類的例項比包含類的例項壽命更長,那麼你遇到了麻煩。例如,如果你建立一個匿名可執行類(anonymous runnable class)並將其傳遞給工作者執行緒或匿名處理程式類(anonymous handler class),並使用它將任務傳遞給其他執行緒,則可能會洩漏包含的類物件。為了避免洩漏風險,請使用靜態類而不是內部/匿名類。
  4. 如果你正在編寫單例或x管理器類,則需要儲存偵聽器(listener)例項的引用,並且不能控制類的使用者如何管理引用,可以請使用WeakReference作為偵聽器(listener)引用。WeakReference並不妨礙他們的引用被GC清除並回收[4]。雖然這個功能在防止記憶體洩漏方面聽起來不錯,但它也可能是一個副作用,因為不能保證被引用的物件在需要時處於活動狀態。所以用它作為修復記憶體洩漏的最後手段。
  5. 在Activity的onDestroy()中,始終記住要終止你啟動過的工作者執行緒。

## 總結

我們研究了什麼是記憶體洩漏,它是如何發生的,以及它在Android系統中造成的後果。然後我們介紹了兩種檢測和識別記憶體洩漏的工具,分析了Android中常見的記憶體洩漏模式,如何評估洩漏的嚴重程度以及如何避免/修復常見洩漏。不要忘了檢視我的Github倉庫中常見記憶體洩漏模式和修復的程式碼示例。快樂製作Android應用,每個人:)

frank-tan/SinsOfMemoryLeaks
SinsOfMemoryLeaks - Android開發中一些常見的記憶體洩漏模式以及如何修復/避免它們

參考

[1] 保持你的應用程式響應性

[2] Java記憶體管理

[3] HPROF檢視器和分析器

[4] WeakReference

[5] 最後瞭解引用在Android和Java中的工作原理

相關文章