一、 Android的內存機制
Android的程序由Java語言編寫,所以Android的內存管理與Java的內存管理相似。程序員通過new爲對象分配內存,所有對象在java堆內分配空間;然而對象的釋放是由垃圾回收器來完成的.
那麼GC怎麼能夠確認某一個對象是不是已經被廢棄了呢?Java採用了有向圖的原理。Java將引用關係考慮爲圖的有向邊,有向邊從引用者指向引用對象。線程對象可以作爲有向圖的起始頂點,該圖就是從起始頂點開始的一棵樹,根頂點可以到達的對象都是有效對象,GC不會回收這些對象。如果某個對象(連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼我們認爲這個(這些)對象不再被引用,可以被GC回收。
二、Android的內存溢出
1、內存泄露導致
由於我們程序的失誤,長期保持某些資源(如Context)的引用,造成內存泄露,資源造成得不到釋放。
Android 中常見就是Activity 被引用沒有在調用finish之後卻沒有釋放,第二次打開activity又重新創建,這樣的內存泄露則會導致內存的溢出。
2、佔用內存較多的對象
保存了多個耗用內存過大的對象(如Bitmap)或加載單個超大的圖片,造成內存超出限制。
三、常見的內存泄漏
1.萬惡的static
static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。
privatestatic ActivitymContext; //省略
如何纔能有效的避免這種引用的發生呢?
第一,應該儘量避免static成員變量引用資源耗費過多的實例,比如Context。
第二、Context儘量使用ApplicationContext,因爲Application的Context的生命週期比較長,引用它不會出現內存泄露的問題。
第三、使用WeakReference代替強引用。比如可以使用WeakReference mContextRef;
2.線程惹的禍
線程也是造成內存泄露的一個重要的源頭。線程產生內存泄露的主要原因在於線程生命週期的不可控。我們來考慮下面一段代碼。
public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new MyThread().start();
}
private class MyThread extends Thread{
@Override
public void run() {
super.run();
//do somthing
}
}
}
我們思考一個問題:假設MyThread的run函數是一個很費時的操作,當調用finish的時候Activity會銷燬掉嗎?
事實上由於我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了內存泄露的問題。
解決方案
第一、將線程的內部類,改爲靜態內部類。
第二、如果需要引用Acitivity,使用弱引用。
另外在使用handler的時候, 尤其用到循環調用的時候,在Activity 退出的時候注意移除。否則也會導致泄露
public class ThreadDemo extends Activity {
private static final String TAG = "ThreadDemo";
private int count = 0;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
public void run() {
//爲了方便 查看,我們用Log打印出來
Log.e(TAG, Thread.currentThread().getName() + " " +count);
//每2秒執行一次
mHandler.postDelayed(mRunnable, 2000);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//通過Handler啓動線程
mHandler.post(mRunnable);
}
}
//所以我們在應用退出時,要將線程銷燬,我們只要在Activity中的,onDestory()方法處理一下就OK了,如下代碼所示:
@Override
protected void onDestroy() {
mHandler.removeCallbacks(mRunnable);
super.onDestroy();
}
3.Bitmap
可以說出現OutOfMemory問題的絕大多數人,都是因爲Bitmap的問題。因爲Bitmap佔用的內存實在是太多了,特別是分辨率大的圖片,如果要顯示多張那問題就更顯著了。
解決方案:
第一、及時的銷燬。
雖然,系統能夠確認Bitmap分配的內存最終會被銷燬,但是由於它佔用的內存過多,所以很可能會超過java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機一個暗示:“該圖片可以釋放了”。
第二、設置一定的採樣率。
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設置一定的採樣率,那麼就可以大大減小佔用的內存。如下面的代碼:
private ImageView preview;
BitmapFactory.Options options = newBitmapFactory.Options();
options.inSampleSize =2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri),null, options); preview.setImageBitmap(bitmap);
第三、巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法調用Recycle函數。這時候巧妙的運用軟引用,可以使Bitmap在內存快不足時得到有效的釋放。
4.行蹤詭異的Cursor
Cursor是Android查詢數據後得到的一個管理數據集合的類,正常情況下,如果查詢得到的數據量較小時不會有內存問題,而且虛擬機能夠保證Cusor最終會被釋放掉。
然而如果Cursor的數據量特表大,特別是如果裏面有Blob信息時,應該保證Cursor佔用的內存被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於編程者手動的將Cursorclose掉
而且android數據庫中對Cursor資源的是又限制個數的,如果不及時close掉,會導致別的地方無法獲得
5.構造Adapter時,沒有使用緩存的convertView
以構造ListView的BaseAdapter爲例,在BaseAdapter中提高了方法:
public View getView(intposition, View convertView, ViewGroup parent)
AdapterView在使用View會有一個循環的View隊列的,把不顯示的View重新投入使用,所以在convertView不爲空的時候,不要直接創建新的View
小結:
static:引用了大對象如context
線程:切屏時Activity因爲線程引用而沒有如期被銷燬;handler有關,Activity意外終止但線程還在
Bitmap:要及時recycle,降低採樣率
Cursor:要及時關閉
Adapter:沒有使用緩存的convertView
四、內存泄漏調試:
(1).內存監測工具 DDMS --> Heap
用 Heap監測應用進程使用內存情況的步驟如下:
1.切換到DDMS透視圖,並確認Devices視圖、Heap視圖都是打開的;
2.正常與手機鏈接成功後,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;
3.點擊選中想要監測的進程
4.點擊選中Devices視圖界面中最上方一排圖標中的“Update Heap”圖標;
5.點擊Heap視圖中的“Cause GC”按鈕;
6.此時在Heap視圖中就會看到當前選中的進程的內存使用量的詳細情況。
Heap視圖界面會定時刷新,在對應用的不斷的操作過程中就可以看到內存使用的變化;
如何才能知道我們的程序是否有內存泄漏的可能性呢。這裏需要注意一個值:Heap視圖中部有一個Type叫做dataobject,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是“TotalSize”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:
a) 不斷的操作當前應用,同時注意觀察dataobject的Total Size值;
b) 正常情況下TotalSize值都會穩定在一個有限的範圍內,也就是說由於程序中的的代碼良好,沒有造成對象不被垃圾回收的情況,所以說 雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;
c) 反之如果代碼中存在沒有釋放對象引用的情況,則dataobject的Total Size值在每次GC後不會有明顯的回落,隨着操作次數的增多TotalSize的值會越來越大,
(2)內存分析工具MAT(Memory Analyzer Tool)
這裏介紹一個極好的內存分析工具 -- Memory AnalyzerTool(MAT)。
MAT是一個Eclipse插件,同時也有單獨的RCP客戶端。官方下載地址、MAT介紹和詳細的使用教程請參見:www.eclipse.org/mat,在此不進行說明了。另外在MAT安裝後的幫助文檔裏也有完備的使用教程。在此僅舉例說明其使用方法。我自己使用的是MAT的eclipse插件,使用插件要比RCP稍微方便一些。插件安裝成功後,分析步驟(安裝方法有多重,大家隨便)
(a)生成.hprof文件
1.打開eclipse並切換到DDMS
2.點擊選中想要分析的應用的進程,在Devices視圖上方的一行圖標按鈕中,選中“UpdateHeap”。
3.當內存你感覺異常的時候,按下“DumpHPROF file”按鈕,這個時候會提示設置hprof文件的保存路徑。
(二) 使用MAT導入.hprof文件
1.通過/ANDROID_SDK/tools目錄下的hprof-conv.exe工具(使用命令同adb),輸入命令hprof-convxxx.hprofyyy.hprof,其中xxx.hprof爲原始文件,yyy.hprof爲轉換過後的文件。
2.在Eclipse中點擊Windows->Open Perspective->Other->MemoryAnalysis perspective界面。在MAT中點擊File->Open File,瀏覽並導入剛剛轉換而得到的.hprof文件。
(三) 使用MAT的視圖工具分析內存
導入.hprof文件以後,MAT會自動解析並生成報告,報告中會列出使用內存過多或者初始化的實例過多的類。
點擊DominatorTree,並按Package分組,選擇報告中提到的可疑實例的類,在彈出菜單中選擇List objects->Withincoming references。這時會列出所有可疑類,右鍵點擊某一項,並選擇Path to GC Roots ->exclude weak/softreferences,會進一步篩選出跟程序相關的所有有內存泄露的類。據此,可以追蹤到代碼中的某一個產生泄露的類。
主要是看可疑類的引用是因爲什麼代碼的引用而導致無法釋放的
總之使用MAT分析內存查找內存泄漏的根本思路,就是找到哪個類的對象的引用沒有被釋放,找到沒有被釋放的原因,也就可以很容易定位代碼中的哪些片段的邏輯有問題了。