天天看點

記憶體洩漏解析

永遠的Singleton

單例的使用在我們的程式中随處可見,因為使用它可以完美的解決我們在程式中重複建立對象的問題,不過可别小瞧它。由于單例的靜态特性,使得它的生命周期和應用的生命周期會一樣長,是以一旦使用有誤,小心無限制的持有Activity的引用而導緻記憶體洩漏。比如,下面的例子。

 這個錯誤在生活中再普遍不過,很正常的一個單例模式,可就由于傳入的是一個 Context,而這個 Context 的生命周期的長短就尤為重要了。如果我們傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出的時候,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應用程式的生命周期,是以目前 Activity 退出時它的記憶體并不會回收,這造成的記憶體洩漏就可想而知了。

正确的方式應該是把傳入的 Context 換為和應用的生命周期一樣長的 Application 的 Context:

 當然,你也可以直接連 Context 都不用傳入了。重寫 application,提供靜态的 getContext 方法:

 自然就可以直接不用傳入Context:

令人心塞的Handler

這個東西在我最近遇到的最多了,而它也是我們在記憶體洩漏中最為常見的,也許你的一個小忽略就會導緻記憶體洩漏。在Android的新版本中,我們被要求必須把網絡任務等耗時操作置于新線程來處理,我們通常會采用 Handler。

但 Handler 不是萬能的,若是我們的編寫不規範就有可能會造成記憶體洩漏。另外,我們知道,Handler、Message 和 MessageQueue 都是互相關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象将會被線程 MessageQueue 一直持有。

由于 Handler 屬于 TLS(Thread Local Storage)變量,生命周期和 Activity 是不一緻的。是以這種實作方式一般很難保證跟 View 或者 Activity 的生命周期一緻,故很容易導緻無法正确釋放。比如:

在例子中,我們申明了一個 延遲5分鐘 執行的消息 Message。當該 Activity 被 finish 的時候,延遲任務的 Message 還存在于主線程中,它持有該 Activity 的 Handler 引用,是以此時 finish 掉的 Activity 就不會回收了,是以造成了記憶體洩漏(因 handler 為非靜态内部類,它會持有外部類的引用,在這裡就是目前的 Activity)。

修複:這個解決也是可以通過把其聲明為 static 的,則其存活期就跟 activity 的生命周期無關了。不過倘若用到 Context 等外部類的 非static 對象,還是應該通過弱引用傳入。比如:

綜述:推薦使用靜态内部類+弱引用 WeakReference 這種方式,但要注意每次使用前判空。

說到若引用,這裡再提下java的幾種引用類型:Strong, reference,SoftReference,WeakReference 和 PhatomReference:

記憶體洩漏解析

在 Android 開發中,為了防止記憶體溢出,在處理一些占用記憶體大并且生命周期較長的對象的時候,可以盡量地使用 軟引用 和 弱引用 技術。

比如,儲存 Bitmap 的軟引用到 HashMap:

使用軟引用以後,在 OutOfMemory 異常發生之前,這些緩存的圖檔資源的記憶體空間可以被釋放掉的,進而避免記憶體達到上限,避免Crash發生。如果隻是想避免 OutOfMemory 異常的發生,則可以使用 軟引用。如果對于應用的性能更在意,想盡快回收一些占用記憶體比較大的對象,則可以使用 弱引用。另外可以根據對象是否經常使用來判斷選擇 軟引用 還是 弱引用。如果該對象可能會經常使用的,就盡量用 軟引用。如果該對象不被使用的可能性更大些,就可以用 弱引用。

 前面所說的,建立一個 靜态Handler内部類,然後對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象,但是這樣做雖然避免了 Activity 洩漏,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,是以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

下面幾個方法都可以移除 Message:

記憶體洩漏解析

匿名内部類/非靜态内部類

它們友善卻暗藏殺機。Android開發經常會繼承實作 Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導緻記憶體洩漏的:

 runnable1 和 runnable2 的差別就是,runnable2 使用了匿名内部類,我們看看引用時的引用記憶體:

記憶體洩漏解析

可以看到,runnable1 是沒有什麼特别的。但 runnable2 多出了一個 MainActivity 的引用,若是這個引用再傳入到一個異步線程,此線程在和Activity生命周期不一緻的時候,也就造成了Activity的洩露。

善用static成員變量

前面就很明顯,當我們的成員變量是 static 的時候,那麼它的生命周期将和整個app的生命周期一緻。

這必然會導緻一系列問題,如果你的app程序設計上是長駐記憶體的,那即使app切到背景,這部分記憶體也不會被釋放。按照現在手機app記憶體管理機制,占記憶體較大的背景程序将優先回收,因為如果此app做過程序互保保活,那會造成app在背景頻繁重新開機。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被使用者解除安裝或者靜默。

這裡修複的方法是:

不要在類初始時初始化靜态成員。可以考慮 lazy初始化(延遲加載)。架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這麼設計,那麼此對象的生命周期你有責任管理起來。

遠離非靜态内部類和匿名類,多用private static class

在我們的日常代碼中,這樣的情況似乎很常見,及直接寫一個class就這麼光秃秃的情況。

記憶體洩漏解析

這樣就在 Activity 内部建立了一個非靜态内部類的單例,每次啟動 Activity 時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜态内部類預設會持有外部類的引用,而該非靜态内部類又建立了一個靜态的執行個體,該執行個體的生命周期和應用的一樣長,這就導緻了該靜态執行個體一直會持有該 Activity 的引用,導緻 Activity 的記憶體資源不能正常回收。

正确的做法為:

将該内部類設為靜态内部類或将該内部類抽取出來封裝成一個單例,如果需要使用 Context,請按照上面推薦的使用 Application 的 Context。當然,Application 的 context  不是萬能的,是以也不能随便亂用,對于有些地方則必須使用 Activity 的 Context,對于Application,Service,Activity三者的 Context 的應用場景如下:

記憶體洩漏解析

其中: NO1 表示 Application 和 Service 可以啟動一個 Activity,不過需要建立一個新的 task 任務隊列。而對于 Dialog 而言,隻有在 Activity 中才能建立。

集合對象善清除,以免記憶體洩漏觸不及防

我們通常會把一些對象的引用加入到集合容器(比如 ArrayList)中,當我們不再需要該對象時,并沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是 static 的話,那情況就更嚴重了。

是以在退出程式之前,将集合裡面的東西 clear,然後置為 null,再退出程式,如下:

webView雖火,記憶體洩漏卻也火的其所

當我們不再需要使用 webView 的時候,應該調用它的 destory() 方法來銷毀它,并釋放其占用的記憶體,否則其占用的記憶體長期也不能回收,進而造成記憶體洩漏。

解決方案:

為 webView 開啟另外一個程序,通過AIDL與主線程進行通信,webView 所在的程序可以根據業務的需要選擇合适的時機進行銷毀,進而達到記憶體的完整釋放。

而另外一些諸如 listView 的 Adapter 沒有緩存之類的等

總結

構造 Adapter 時,沒有使用緩存的 convertView

Bitmap 對象不在使用時調用 recycle() 釋放記憶體

Context 使用不當造成記憶體洩露:不要對一個 Activity Context 保持長生命周期的引用。盡量在一切可以使用應用 ApplicationContext 代替 Context 的地方進行替換。

非靜态内部類的靜态執行個體容易造成記憶體洩漏:即一個類中如果你不能夠控制它其中内部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜态類和弱引用來處理(譬如ViewRoot的實作)。

警惕線程未終止造成的記憶體洩露;譬如在 Activity 中關聯了一個生命周期超過 Activity 的 Thread,在退出 Activity 時切記結束線程。一個典型的例子就是 HandlerThread 的 run 方法是一個死循環,它不會自己結束,線程的生命周期超過了 Activity 生命周期,我們必須手動在Activity的銷毀方法中中調運 thread.getLooper().quit(); 才不會洩露。

對象的注冊與反注冊沒有成對出現造成的記憶體洩露;譬如注冊廣播接收器、注冊觀察者(典型的譬如資料庫的監聽)等。

建立與關閉沒有成對出現造成的洩露;譬如 Cursor 資源必須手動關閉,WebView 必須手動銷毀,流等對象必須手動關閉等。

不要在執行頻率很高的方法或者循環中建立對象(比如onmeasure),可以使用 HashTable 等建立一組對象容器從容器中取那些對象,而不用每次new與釋放。

避免代碼設計模式的錯誤造成記憶體洩露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。