Andriod 內存泄露問題的整理

這篇文章是我很久以前寫的,感覺寫得太官方了,於是我下定決心想要改得更加貼近大家的思維,於是我將把改版後的推薦給大家。
1、內存泄漏:
當出現對Activity、View或drawable等類的對象長期持有無用的引用,就會造成被引用的對象無法在GC時回收,而是長期佔用堆空間,此時就會發生內存泄漏。
簡單來說,就是保留下來卻永遠不再使用的對象引用。
2、內存溢出:
如果應用程序在消耗光了所有的可用堆空間(16M到48M),那麼再試圖在堆上分配新對象時就會引起OOM(Out Of Memory Error)異常,此時應用程序就會崩潰退出。
3、兩者的區別:
簡單的說,就是內存溢出是佔用內存太大,超過了其可以承受的範圍;
內存泄漏是回收不及時甚至是沒有被回收,而在推空間中產生的許多無用的引用。
於是過多的內存泄漏就會導致內存溢出,從而迫使程序崩潰退出。
4、四種不同類型的引用:
引用名稱
Strong Reference(強引用)
Soft Reference (軟引用)
Weak Reference (弱引用)
Phantom Reference (虛引用)
引用特點
通常我們編寫的代碼都是Strong Ref,於此對應的是強可達性,只有去掉強可達,對象才被回收。
對應軟可達性,只要有足夠的內存,就一直保持對象,直到發現內存吃緊且沒有Strong Ref時纔回收對象。一般可用來實現緩存,
比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收對象而不必等到內存吃緊的時候
根本不會在內存中保持任何對象,你只能使用Phantom Ref本身。一般用於在進入finalize()方法後進行特殊的清理過程
引用方式
 
通過java.lang.ref.
SoftReference類實現
通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現
通過java.lang.ref.PhantomReference實現
 
 
5、持有Context引用造成的泄漏。
在Android應用程序中,很多操作都用到了Context對象,但是大多數都是用來加載和訪問資源的。
這就是爲什麼所有的顯示控件都需要一個Context對象作爲構造方法的參數。
在Android應用程序中通常可以使用兩種Context對象:Activity和Application。
當類或方法需要Context對象的時候常見的作法是使用第一個作爲Context參數。
但這就意味着View對象對整個activity保持引用,因此也就保持對activity內的所有東西的引用,
也就是整個View結構和它所有的資源都無法被及時的回收,而且對activity的長期引用是比較隱蔽的。
當屏幕方向改變時,Android系統默認作法是會銷燬當前的Activity,然後創建一個新的Activity,這個新的Activity會顯示剛纔的狀態。
在這樣做的過程中,Android系統會重新加載UI用到的資源。
現在假設的應用程序中有一個比較大的bitmap類型的圖片,每次旋轉時都重新加載圖片所用的時間較多。
爲了提高屏幕旋轉時Activity的創建速度,最簡單的方法是用靜態變量的方法。
這樣的代碼執行起來是快速的,但同時是錯誤的:這樣寫會一直保持着對Activity的引用。
當一個Drawable對象附屬於一個View時,這個View就相當於drawable對象的一個回調(引用)。
在上面的代碼片段中,就意味着drawable和TextView存在着引用的關係,
而TextView自己持有了對Activity(Context對象)的引用,這個Activity又引用了相當多的東西。
有兩種簡單的方法可以避免由引用context對象造成的內存泄露。
首先第一個方法是避免context對象超出它的作用範圍。
上面的例子展示了靜態引用的情況,但是在類的內部,隱式的引用外部的類同樣的危險。
第二種方法是,使用Application對象。這個context對象會隨着應用程序的存在而存在,而不依賴於activity的生命週期。
如果你打算對context對象保持一個長期的引用,請記住這個application對象。
通過調用Context.getApplicationContext() 或者 Activity.getApplication().方法,你可以很容易的得到這個對象。
如果在activity中使用靜態的類時如果需要引用activity,應該採用WeakReference弱引用來引用Activity。
總結一下避免Context泄漏應該注意的問題: 儘量使用Application這種Context類型, 
注意對Context的引用不要超過它本身的生命週期,慎重的對Context使用“static”關鍵字,Context裏如果有線程,一定要在onDestroy()裏及時停掉。 
爲了防止內存泄露,我們應該注意以下幾點:
(1)不要讓生命週期長的對象引用activity context,即保證引用activity的對象要與activity本身生命週期是一樣的.
(2)對於生命週期長的對象,可以使用application context。
(3)避免非靜態的內部類,儘量使用靜態類,避免生命週期問題,注意內部類對外部對象引用導致的生命週期變化。
6、線程之間通過Handler通信引起的內存泄漏
Android中線程之間進行通信時最常用的作法是通過接收消息的目標線程所持有Handler對象來創建Message對象,然後再向目標線程發送該Message。
在目標線程中Handler在執行handleMessage()時會根據相應Message來執行相應不同功能。
另外一種作法是通過Handler對象向目標線程直接發送Runnable對象來執行該Runnable對象中不同的功能代碼。
在通過Handler進行通信時如果不注意,也很有可能引起內存泄漏。
在sendMessage完成之後顯示的將msg成員變量置爲null,並且在退出整個應用程序之前,將handler置爲null。
7、將變量的作用域設置爲最小
最小化作用域意味着對垃圾收集器更快的可見。讓我們舉一個例子。
我們有一個變量定義在方法級,當方法執行完畢後,也就是說,控制跳出方法後,則該變 量將不會被引用。
這也就意味着它已經可以被垃圾回收。但是如果該變量是類級的,這時垃圾回收器就需要等待直到對該類的所有的引用都被移除後才能進行垃圾回收。
優化屬性的作用域是必須的。同樣這也是封裝原則。最小化作用域降低了通過包訪問變量並減少了耦合。
8、構造Adapter時,沒有使用緩存的convertView。
初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的view對象,同時ListView會將這些view對象 緩存起來。
當向上滾動ListView時,原先位於最上面的list item的view對象會被回收,然後被用來構造新出現的最下面的list item。
這個構造過程就是由getView()方法完成的,getView()的第二個形參View convertView就是被緩存起來的list item的view對象(初始化時緩存中沒有view對象則convertView是null)。
由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新實例化一個View對象的話,
即浪費時間,也造 成內存垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機將不得不給該應用進程分配更多的內存,造成不必要的內存開支。
9、Bitmap的回收和置空。
Bitmap 對象不在使用時調用 recycle()釋放內存,有時我們會手工的操作 Bitmap 對象, 
如果一個Bitmap 對象比較佔內存, Bitmap對象在不使用時,我們應該先調用recycle()釋放內存,然後才它設置爲null。
雖然recycle()從源碼上看,調用它應該能立即釋放Bitmap的主要內存,但是測試結果顯示它並沒能立即釋放內存。
但是我它應該還是能大大的加速Bitmap的主要內存的釋放。
10、資源對象沒關閉造成的內存泄露。
資源性對象比如(Cursor,File文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。
它們的 緩衝不僅存在於java虛擬機內,還存在於java虛擬機外。
如果我們僅僅是把它的引用設置爲null,而不關閉它們,往往會造成內存泄露。
因爲有些資源 性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),
如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。
因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然後才置爲null. 
在我們的程序退出時一定要確保我們的資源性對象已經關閉。
程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。
如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會復現內存問題,這樣就會給以後的測試和問題排查帶來困難和風險。
11、各種註冊沒取消。
這種情況造成的內存泄露這種Android的內存泄露比純java的內存泄露還要嚴重,因爲其他一些Android程序可能引用我們的Anroid程序的對象(比如註冊機制)。
即使我們的Android程序已經結束了,但是別的引用程序仍然還有對我們的Android程序的某個對象的引用,泄露的內存依然不能被垃圾回收
。比如 假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息(如信號強度等),
則可以在LockScreen中定義一個PhoneStateListener的對象,同時將它註冊到TelephonyManager服務中。
對於LockScreen對象,當需要顯示鎖屏界面的時候就會創建一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。
但是如果在釋放LockScreen對象的時候忘記取消我們之前註冊的PhoneStateListener對象,則會導致LockScreen無法被垃圾回收。
如果不斷的使鎖屏界面顯示和消失,則最終會由於大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process進程掛掉。
雖然有些系統程序,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在我們的程序中明確的取消註冊,程序結束時應該把所有的註冊都取消掉。
12、集合容器對象沒清理造成的內存泄露。
我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,
並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
所以要在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序。
13、static關鍵字的濫用。
當類的成員變量聲明成static後,它是屬於類的而不是屬於對象的,
如果我們將很大的資源對象(Bitmap,context等)聲明成static,那麼這些資源不會隨着對象的回收而回收, 
會一直存在,所以在使用static關鍵字定義成員變量的時候要慎重。
14、WebView對象沒有銷燬。
當我們不要使用WebView對象時,應該調用它的destory()函數來銷燬它,並釋放其佔用的內存,否則其佔用的內存長期也不能被回收,從而造成內存泄露。
15、GridView的濫用。
GridView和ListView的實現方式不太一樣。GridView的View不是即時創建的,而是全部保存在內存中的。
比如一個GridView有100項,雖然我們只能看到10項,但是其實整個100項都是在內存中的。
所以在應用程序退出之前,要講gridView和adapter全部置爲null。
 
 
 
PS:希望大家能喜歡我的總結,雖然有點囉嗦,但是這些都是我在做項目的時候赤裸裸的教訓啊,很高興和大家分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章