Android 記憶體洩漏案例和解析

發表於2016-03-25

Android 程式設計所使用的 Java 是一門使用垃圾收集器(GC, garbage collection)來自動管理記憶體的語言,它使得我們不再需要手動呼叫程式碼來進行記憶體回收。那麼它是如何判斷的呢?簡單說,如果一個物件,從它的根節點開始不可達的話,那麼這個物件就是沒有引用的了,是會被垃圾收集器回收的,其中,所謂的 “根節點” 往往是一個執行緒,比如主執行緒。因此,如果一個物件從它的根節點開始是可達的有引用的,但實際上它已經沒有再使用了,是無用的,這樣的物件就是記憶體洩漏的物件,它會在記憶體中佔據我們應用程式原本就不是很多的記憶體,導致程式變慢,甚至記憶體溢位(OOM)程式崩潰。

記憶體洩漏的原因並不難理解,但僅管知道它的存在,往往我們還是會不知覺中寫出致使記憶體洩漏的程式碼。在 Android 程式設計中,也是有許多情景容易導致記憶體洩漏,以下將一一列舉一些我所知道的記憶體洩漏案例,從這些例子中應該能更加直觀瞭解怎麼導致了記憶體洩漏,從而在程式設計過程中去避免。

靜態變數造成記憶體洩漏

首先,比較簡單的一種情況是,靜態變數致使記憶體洩漏,說到靜態變數,我們至少得了解其生命週期才能徹底明白。靜態變數的生命週期,起始於類的載入,終止於類的釋放。對於 Android 而言,程式也是從一個 main 方法進入,開始了主執行緒的工作,如果一個類在主執行緒或旁枝中被使用到,它就會被載入,反過來說,假如一個類存在於我們的專案中,但它從未被我們使用過,算是個孤島,這時它是沒有被載入的。一旦被載入,只有等到我們的 Android 應用程式結束它才會被解除安裝。

於是,當我們在 Activity 中宣告一個靜態變數引用了 Activity 自身,就會造成記憶體洩漏:

這樣的程式碼會導致當這個 Activity 結束的時候,sContext 仍然持有它的引用,致使 Activity 無法回收。解決辦法就是在這個 Activity 的 onDestroy 時將 sContext 的值置空,或者避免使用靜態變數這樣的寫法。

同樣的,如果一個 Activity 的靜態 field 變數內部獲得了當前 Activity 的引用,比如我們經常會把 this 傳給 View 之類的物件,這個物件若是靜態的,並且沒有在 Activity 生命週期結束之前置空的話,也會導致同樣的問題。

非靜態內部類和匿名內部類造成記憶體洩漏

也是一個很常見的情景,經常會遇到的 Handler 問題就是這樣一種情況,如果我們在 field 宣告一個 Handler 變數:

由於在 Java 中,非靜態內部類(包括匿名內部類,比如這個 Handler 匿名內部類)會引用外部類物件(比如 Activity),而靜態的內部類則不會引用外部類物件。所以這裡 Handler 會引用 Activity 物件,當它使用了 postDelayed 的時候,如果 Activity 已經 finish 了,而這個 handler 仍然引用著這個 Activity 就會致使記憶體洩漏,因為這個 handler 會在一段時間內繼續被 main Looper 持有,導致引用仍然存在,在這段時間內,如果記憶體吃緊至超出,就很危險了。

解決辦法就是大家都知道的使用靜態內部類加 WeakReference:

另外,綜合上面兩種情況,如果一個變數,既是靜態變數,而且是非靜態的內部類物件,那麼也會造成記憶體洩漏:

注意,這裡我們定義的 Hello 雖然是空的,但它是一個非靜態的內部類,所以它必然會持有外部類即 LeakActivity.this 引用,導致 sHello 這個靜態變數一直持有這個 Activity,於是結果就和第一個例子一樣,Activity 無法被回收。

到這裡大家應該可以看出,記憶體洩漏經常和靜態變數有關。和靜態變數有關的,還有一種常見情景,就是使用單例模式沒有解綁致使記憶體洩漏,單例模式的物件經常是和我們的應用相同的生命週期,如果我們使用 EventBus 或 Otto 並生成單例,註冊了一個 Activity 而沒有在頁面結束的時候進行解除註冊,那麼單例會一直持有我們的 Activity,這個 Activity 雖然沒有使用了,但會一直佔用著記憶體。

屬性動畫造成記憶體洩漏

另外當我們使用屬性動畫,我們需要呼叫一些方法將動畫停止,特別是無限迴圈的動畫,否則也會造成記憶體洩漏,好在使用 View 動畫並不會出現記憶體洩漏,估計 View 內部有進行釋放和停止。

RxJava 使用不當造成記憶體洩漏

最後說一說 RxJava 使用不當造成的記憶體洩漏,RxJava 是一個非常易用且優雅的非同步操作庫。對於非同步的操作,如果沒有及時取消訂閱,就會造成記憶體洩漏:

同樣是匿名內部類造成的引用沒法被釋放,使得如果在 Activity 中使用就會導致它無法被回收,即使我們的 Action1 看起來什麼也沒有做。解決辦法就是接收 subscribe 返回的 Subscription 物件,在 Activity onDestroy 的時候將其取消訂閱即可:

除了以上這種解決方式之外,還有一種解決方式就是通過 RxJava 的 compose 操作符和 Activity 的生命週期掛鉤,我們可以使用一個很方便的第三方庫叫做 RxLifecycle 來快捷做到這點,使用起來就像這樣:

另外,它還提供了和 View 的便捷繫結,詳情可以點選我提供的連結進行了解,這裡不多說了。

總結來說,仍然是前面說的內部類或匿名內部類引用了外部類造成了記憶體洩漏,所以在實際程式設計過程中,如果涉及此類問題或者執行緒操作的,應該特別小心,很可能不知不覺中就寫出了帶記憶體洩漏的程式碼了。

記憶體洩漏的檢測

前面說了不少記憶體洩漏的場景和對應的解決辦法,但如果我們不知不覺中寫出了帶有記憶體洩漏隱患的程式碼怎麼辦,面對這個問題,其實到現在,我們是很幸運的,因為有很多相關的檢查方式或元件可以選擇,比如最簡單的:觀察 Memory Monitor 記憶體走勢圖,可以或多或少知道記憶體情況,但如果要精確地追蹤到記憶體洩漏點,這裡特別推薦偉大的 Square 公司開源的 LeakCanary 方案,LeakCanary 可以做到非常簡單方便、低侵入性地捕獲記憶體洩漏程式碼,甚至很多時候你可以捕捉到 Android 官方元件的記憶體洩漏程式碼,具體使用大家可以自行參看其說明,由於本文主要想講的是記憶體洩漏的原因和一些常見場景,對於檢測,這裡就不多說啦

相關文章