1. 程式人生 > >android 記憶體基本原理和機制管理

android 記憶體基本原理和機制管理

java語言相對於c/c++語言來說人性化的一點就是java有專門管理回收的垃圾回收器。而c/c++語言只能是“誰造成,誰處理”。

GC

1,GC是垃圾收集的意思(Gabage Collection)
2,Java提供的GC功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的。
3,Java語言沒有提供釋放已分配記憶體的顯示操作方法。

垃圾回收原理

垃圾回收器通常是作為一個單獨的低級別的執行緒執行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的物件進行清除和回收,程式設計師不能實時的呼叫垃圾回收器對某個物件或所有物件進行垃圾回收。
回收機制有分代複製垃圾回收

標記垃圾回收增量垃圾回收

垃圾回收器原理

對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。
GC採用有向圖的方式記錄和管理堆(heap)中的所有物件。通過這種方式確定哪些物件是”可達的”,哪些物件是”不可達的”。當GC確定一些物件為”不可達”時,GC就有責任回收這些記憶體空間。
程式設計師可以手動執行System.gc(),通知GC執行,但是Java語言規範並不保證GC一定會執行。

Android中GC什麼情況下會出現記憶體洩露?

Java 記憶體洩露的根本原因就是儲存了不可能再被訪問的變數型別的引用。
例如:一個靜態方法引用了上下文(context),並把這個context設定成了全域性變數。那麼這個context就成了記憶體洩漏的“事發者”了。

/**
 * 記憶體洩漏例子
 */
public class TestClass {

    private static Context mContext;

    //外界呼叫該方法,傳入上下文,並傳遞物件。這個上下文就不會被記憶體回收
    public static void setContext(Context context) {
        mContext = context;
    }

    private void getContext() {
        Toast.makeText(mContext, "記憶體洩漏了", Toast.LENGTH_LONG).show();
    }
}

Android的記憶體溢位是如何發生的?

Android的虛擬機器是基於暫存器的Dalvik,它的最大堆大小一般是16M,有的機器為24M

如果我們的記憶體佔用超過了一定的水平就會出現OOM(OutOfMemory)的錯誤。

例如:
1,程式的失誤,長期保持某些資源(如Context)的引用
2,儲存了多個耗用記憶體過大的物件(如Bitmap)

一般情況就是:資源得不到釋放佔用記憶體過大的物件造成記憶體超出限制。

記憶體管理

1,static
當用static來修飾成員變數時,那麼該變數就屬於該類,而不是該類的例項。所以用static修飾的變數,它的生命週期是很長的,謹慎使用它來引用一些資源耗費過多的例項(Context的情況最多)。

還有一種比較容易忽視的現象:

private static Drawable sBackground;    

   public void example (){  
        ImageView iv = new ImageView(this);  
        iv.setBackgroundDrawable(sBackground);  
   }  

如程式碼,我們將一個drawable設定成靜態的,方便任何時候去呼叫這個物件。殊不知,這個靜態物件已經造成了記憶體洩漏。

我們並沒有顯式的儲存Contex的引用,但是,因為Imageview是View的子類,當Drawable與View連線之後,Drawable就將View設定為一個回撥:(原始碼)

public class View implements Drawable.Callback  

   /** 
     * The application environment this view lives in. 
     * This field should be made private, so it is hidden from the SDK. 
     * {@hide} 
     */  
    protected Context mContext;

由於View中是包含Context的引用的,所以,實際上我們依然儲存了Context的引用。所以最終的引用鏈如下:
Drawable->ImageView->Context

所以,Context儘量使用Application Context,因為Application的Context的生命週期比較長;
使用WeakReference代替強引用。比如可以使用WeakReference mContextReference。

2,資料庫遊標
關於資料庫的遊標Cursor,在資料量小的時候我們是非常不容易察覺的。但是資料量大的時候,記憶體問題才會體現出來。
我們可以將Cursor裡面的值給實現parcelable的類的成員變數,在響應的地方取值,這樣可以及時關閉遊標,釋放資源。

3,執行緒

new Thread(new Runnable() {  
            public void run() {  
               // TODO do something  
           }  
        }).start(); 

當開啟一個執行緒後,線上程中執行耗時操作。這個時候,如果這個執行緒還在執行。執行了橫豎屏切換,並且new了一個activity。
這時候,老activity中的執行緒還在執行,所以新activity執行的時候,老activity不會被銷燬。
這樣,因為這個執行緒是內部類,所以引用了這個activity,只要run方法不結束,這個老activity就不會被銷燬,造成了記憶體洩漏。

另外,非同步任務AsyncTask的問題更加嚴重。
Thread只有在run函式不結束時才出現這種記憶體洩露問題,然而AsyncTask內部的實現機制是運用了ThreadPoolExcutor,該類產生的Thread物件的生命週期是不確定的,是應用程式無法控制的,因此如果AsyncTask作為Activity的內部類,就更容易出現記憶體洩露的問題。

那麼,如果遇到這種情況,怎麼解決呢?
1,將執行緒的內部類,改為靜態內部類。
2,線上程內部採用弱引用儲存Context引用。

4,Bitmap

它是一個“超級大胖子”,特別是解析度大的圖片,如果要顯示多張那問題就更顯著了。

解決辦法:
1,及時銷燬。在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。所以Bitmap物件在不使用時,我們應該先呼叫recycle()釋放記憶體,然後才它設定為null. 雖然recycle()從原始碼上看,呼叫它應該能立即釋放Bitmap的主要記憶體,但是測試結果顯示它並沒能立即釋放記憶體。
2,設定取樣率。記載一個縮小過的圖片,只要能夠供使用者觀看就可以了。用完Bitmap時,要及時的recycle掉。
3,巧妙的運用軟引用(SoftRefrence)。我們使用Bitmap後沒有保留對它的引用,因此就無法呼叫Recycle函式。這時候巧妙的運用軟引用,可以使Bitmap在記憶體快不足時得到有效的釋放。

5,Adapter
就listview或者gridview的介面卡來說,不做優化的後果估計都知道。
關鍵在於圖片:

private ArrayList<SoftReference<Bitmap>> mBitmaps = new ArrayList<SoftReference<Bitmap>>();

和view的複用:ViewHolder。

6,將變數的作用域設定為最小。
如果一個變數是方法級的,那麼當這個方法執行完畢後,這個變數就會被回收掉。
而如果這個變數是類級的,那麼垃圾回收機制必須等到該類的所有引用都被移除後才能被回收。

所以,變數的作用域要根據實際情況而定。

7,僅僅當一個物件確實需要的時候才初始化
一些物件可能在我們定義它的時候就初始化了。那麼,如果在呼叫這個物件之前出現任何問題,這個物件所佔的記憶體就被浪費了。
所以,真正當我們需要用到一個物件的前一步再去初始化。

8,不要再迴圈中定義變數
這是一個常見的錯誤。

public void test() {
        for (int i = 0; i < 20; i++) {
            String name = "";
            name += i;
            Log.i("test", name);
        }
    }

這樣你可以迴圈出所有的名字。但是每一次程式都會建立String name;然後回收;這樣做會額外的分配記憶體也會增加垃圾回收的器的負擔。

另外,在迴圈中String如果使用+來連結會是一個很大的開銷。這時候String會是一個很長的查詢或者產生一個toString,如果我們用+來連結,可想而知,是多麼大的記憶體開銷。
這時候儘量使用不變物件,如:StringBuffer來進行字串的連結。

如果要重引用物件,這時候將物件設定成null。這樣,記憶體能夠立即釋放它的記憶體。

9,使用finally塊
try/catch方法不管success還是final都會呼叫finally方法。我們可以將清理記憶體的語句放在finally中以保證記憶體被清理。

10,分散物件建立和刪除的時間
集中建立大量物件,特別是大物件的時候需要大量的記憶體。GC在這個時候會回收記憶體和正合記憶體碎片。這時候其實是增加了主GC的頻率,使得下一次建立新物件的時候強制主GC的機會大大增加。

好了,這就是android記憶體的基本知識。只要在程式設計中注意到了這些問題,那麼你的程式會大大的得到優化甚至便於維護。不要認為專案的優化是從框架到邏輯的一個過程,其實很多時候是從這些小細節出發的。