1. 程式人生 > >Android 效能優化之記憶體優化

Android 效能優化之記憶體優化

在移動作業系統上,通常實體記憶體有限,儘管 Android 的 Dalvik 虛擬機器扮演了常規的垃圾回收的角色,但這並不意味著我們可以忽略 APP 的記憶體分配與釋放,為了 GC 能夠從 APP 中及時回收記憶體,我們在日常的開發中就需要時刻注意記憶體洩露,並在合適的時候來釋放引用物件,對於大多數的 APP 來說,Dalvik 的 GC 會自動把離開活動執行緒的物件進行回收,接下來我們就來看看有關記憶體方面的具體優化思想,這裡我們從兩個方面來談,在具體寫程式碼的過程中我們需要注意的以及使用那些工具來檢測出我們在寫程式碼過程中所沒有注意到的記憶體洩露


一、如何管理記憶體

1)Service 資源使用

      如果需要在後臺使用 Service,當 Service 完成任務但停止失敗的情況下,會引起記憶體洩露,我們在處理 Service 的時候儘量使用 IntentService 它會在處理完給它的任務以後自動關閉,當一個 Service 已經不需要時還必須保留它,這對 Android 應用記憶體管理來說是很糟糕的錯誤之一

2)onTrimMemory 優化

      onTrimMemory 主要作用就是指導應用程式在不同的情況下進行自身的記憶體釋放,以避免系統被直接殺掉,提高應用程式的使用者體驗,接下來我們來看一下具體哪些元件可以實現 onTrimMemory 的回撥

  • Application.onTrimMemory()
  • Activity.onTrimMemory()
  • Fragment.onTrimMemory()
  • Service.onTrimMemory()
  • ContentProvider.onTrimMemory()

App 生命週期的任何階段,應該根據 onTrimMemory() 方法中的記憶體級別來進一步決定釋放哪些記憶體

  • TRIM_MEMORY_UI_HIDDEN: 表示應用程式的所有 UI 介面被隱藏了,即使用者點選了 Home 鍵或者 Back 鍵導致應用的UI 介面不可見.這時候應該釋放一些資源.
    TRIM_MEMORY_UI_HIDDEN:這個等級比較常用,和下面六個的關係不是很強,所以單獨說

下面三個等級是當我們的應用程式真正執行時的回撥:

  • TRIM_MEMORY_RUNNING_MODERATE:    表示應用程式正常執行,並且不會被殺掉。但是目前手機的記憶體已經有點低了,系統可能會開始根據LRU快取規則來去殺死程序了。
  • TRIM_MEMORY_RUNNING_LOW:    表示應用程式正常執行,並且不會被殺掉。但是目前手機的記憶體已經非常低了,我們應該去釋放掉一些不必要的資源以提升系統的效能,同時這也會直接影響到我們應用程式的效能。
  • TRIM_MEMORY_RUNNING_CRITICAL:    表示應用程式仍然正常執行,但是系統已經根據LRU快取規則殺掉了大部分快取的程序了。這個時候我們應當儘可能地去釋放任何不必要的資源,不然的話系統可能會繼續殺掉所有快取中的程序,並且開始殺掉一些本來應當保持執行的程序,比如說後臺執行的服務。

當應用程式是快取的,則會收到以下幾種型別的回撥:

  • TRIM_MEMORY_BACKGROUND:    表示手機目前記憶體已經很低了,系統準備開始根據 LRU 快取來清理程序。這個時候我們的程式在 LRU 快取列表的最近位置,是不太可能被清理掉的,但這時去釋放掉一些比較容易恢復的資源能夠讓手機的記憶體變得比較充足,從而讓我們的程式更長時間地保留在快取當中,這樣當用戶返回我們的程式時會感覺非常順暢,而不是經歷了一次重新啟動的過程。
  • TRIM_MEMORY_MODERATE:    表示手機目前記憶體已經很低了,並且我們的程式處於 LRU 快取列表的中間位置,如果手機記憶體還得不到進一步釋放的話,那麼我們的程式就有被系統殺掉的風險了。
  • TRIM_MEMORY_COMPLETE:    表示手機目前記憶體已經很低了,並且我們的程式處於 LRU 快取列表的最邊緣位置,系統會最優先考慮殺掉我們的應用程式,在這個時候應當儘可能地把一切可以釋放的東西都進行釋放。
3)使用 BitMap 優化
    在 Android 應用裡,最耗費記憶體的就是圖片資源。而且在 Android 系統中,讀取點陣圖 Bitmap 時,分給虛擬機器中的圖片的堆疊大小隻有 8 M,如果超出了,就會出現 OutOfMemory 異常。所以,對於圖片的記憶體優化,是 Android 應用開發中比較重要的內容

3.1 要及時回收 Bitmap 的記憶體

      Bitmap 類有一個方法 recycle(),從方法名可以看出意思是回收。這裡就有疑問了,Android 系統有自己的垃圾回收機制,可以不定期的回收掉不使用的記憶體空間,當然也包括 Bitmap 的空間。

      Bitmap 類的構造方法都是私有的,所以開發者不能直接 new 出一個 Bitmap 物件,只能通過 BitmapFactory 類的各種靜態方法來例項化一個 Bitmap。仔細檢視 BitmapFactory 的原始碼可以看到,生成 Bitmap 物件最終都是通過 JNI 呼叫方式實現的。所以,載入 Bitmap 到記憶體裡以後,是包含兩部分記憶體區域的。簡單的說,一部分是 Java 部分的,一部分是 C 部分的。這個 Bitmap 物件是由 Java 部分分配的,不用的時候系統就會自動回收了,但是那個對應的 C 可用的記憶體區域,虛擬機器是不能直接回收的,這個只能呼叫底層的功能釋放。所以需要呼叫 recycle() 方法來釋放 C 部分的記憶體。從 Bitmap 類的原始碼也可以看到,recycle() 方法裡也的確是呼叫了 JNI 方法了的。

      那如果不呼叫 recycle(),是否就一定存在記憶體洩露呢?也不是的。Android 的每個應用都執行在獨立的程序裡,有著獨立的記憶體,如果整個程序被應用本身或者系統殺死了,記憶體也就都被釋放掉了,當然也包括  C 部分的記憶體。

       Android 對於程序的管理是非常複雜的。簡單的說,Android 系統的程序分為幾個級別,系統會在記憶體不足的情況下殺死一些低優先順序的程序,以提供給其它程序充足的記憶體空間。在實際專案開發過程中,有的開發者會在退出程式的時候使用Process.killProcess(Process.myPid()) 的方式將自己的程序殺死,但是有的應用僅僅會使用呼叫 Activity.finish() 方法的方式關閉掉所有的 Activity。

      一般來說,如果能夠獲得 Bitmap 物件的引用,就需要及時的呼叫 Bitmap 的 recycle() 方法來釋放 Bitmap 佔用的記憶體空間,而不要等 Android 系統來進行釋放。

// 先判斷是否已經回收
if(bitmap != null && !bitmap.isRecycled()){ 
        // 回收並且置為null
        bitmap.recycle(); 
        bitmap = null; 
} 
System.gc();
     從上面的程式碼可以看到,bitmap.recycle() 方法用於回收該 Bitmap 所佔用的記憶體,接著將 bitmap置空,最後使用System.gc() 呼叫一下系統的垃圾回收器進行回收,可以通知垃圾回收器儘快進行回收。這裡需要注意的是,呼叫 System.gc() 並不能保證立即開始進行回收過程,而只是為了加快回收的到來

3.2 異常捕獲

      因為 Bitmap 非常消耗記憶體,為了避免應用在分配 Bitmap 記憶體的時候出現 OutOfMemory 異常以後 Crash 掉,需要特別注意例項化 Bitmap 部分的程式碼。通常,在例項化 Bitmap 的程式碼中,一定要對 OutOfMemory 異常進行捕獲。

Bitmap bitmap = null;
try {
    // 例項化Bitmap
    bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
   //
}
if (bitmap == null) {
    // 如果例項化失敗 返回預設的Bitmap物件
    return defaultBitmapMap;
}

3.3 壓縮圖片

     如果圖片畫素過大,使用 BitmapFactory 類的方法例項化 Bitmap 的過程中,需要大於 8M 的記憶體空間,就必定會發生OutOfMemory 異常。這個時候該如何處理呢?如果有這種情況,則可以將圖片縮小,以減少載入圖片過程中的記憶體的使用,避免異常發生。

      使用 BitmapFactory.Options 設定 inSampleSize 就可以縮小圖片。屬性值 inSampleSize 表示縮圖大小為原始圖片大小的幾分之一。即如果這個值為 2,則取出的縮圖的寬和高都是原始圖片的 1/2,圖片的大小就為原始大小的 1/4。

如果知道圖片的畫素過大,就可以對其進行縮小。那麼如何才知道圖片過大呢?

      使用 BitmapFactory.Options 設定 inJustDecodeBounds 為 true 後,再使用 decodeFile() 等方法,並不會真正的分配空間,即解碼出來的 Bitmap 為 null,但是可計算出原始圖片的寬度和高度,即 options.outWidth 和 options.outHeight。通過這兩個值,就可以知道圖片是否過大了。

BitmapFactory.Options opts = new BitmapFactory.Options();
    // 設定inJustDecodeBounds為true
    opts.inJustDecodeBounds = true;
    // 使用decodeFile方法得到圖片的寬和高
    BitmapFactory.decodeFile(path, opts);
    // 打印出圖片的寬和高
    Log.d("example", opts.outWidth + "," + opts.outHeight);

      在實際專案中,可以利用上面的程式碼,先獲取圖片真實的寬度和高度,然後判斷是否需要跑縮小。如果不需要縮小,設定inSampleSize 的值為 1。如果需要縮小,則動態計算並設定 inSampleSize 的值,對圖片進行縮小。需要注意的是,在下次使用BitmapFactory 的 decodeFile() 等方法例項化 Bitmap 物件前,別忘記將 opts.inJustDecodeBound 設定回 false。否則獲取的 bitmap 物件還是 null。

3.4 快取通用的 Bitmap 物件

      有時候,可能需要在一個 Activity 裡多次用到同一張圖片。比如一個 Activity 會展示一些使用者的頭像列表,而如果使用者沒有設定頭像的話,則會顯示一個預設頭像,而這個頭像是位於應用程式本身的資原始檔中的。

     如果有類似上面的場景,就可以對同一 Bitmap 進行快取。如果不進行快取,儘管看到的是同一張圖片檔案,但是使用BitmapFactory 類的方法來例項化出來的 Bitmap,是不同的 Bitmap 物件。快取可以避免新建多個 Bitmap 物件,避免記憶體的浪費,如在專案中使用,建議使用成熟的圖片載入框架,如 Glide、Picasso、Fraesco 等,這樣能避免很多圖片載入時出現的問題

4)使用優化的資料容器

    利用 Android FrameWork 裡面優化過的容器類,例如 SparseArray、SparseBooleanArray、LongSparseArray,通常的HashMap 實現方式更加消耗記憶體,因為它需要一個額外的實現物件來記錄 Mapping 的操作,另外 SparseArray 更加高效在於它們避免了對 key 與 value 的 auotbox 自動裝箱,並且避免了自動裝箱後的解箱

5)使用 ProGuard 來剔除不需要的程式碼

      ProGuard 能夠通過移除不需要的程式碼,重新命名類,和方法等方式對程式碼進行壓縮,優化與混淆,使用 ProGuard 可以使你的程式碼更加緊湊

6)對最終的 APK 使用 zipalign

     在編寫玩所有的程式碼,並通過編譯系統生成 APK 之後,需要使用 zipalign 對 APK 進行重新校準,它會對齊升級包資源,Google Play 不接受沒有 zipalign 的 APK

二、記憶體洩露和記憶體溢位優化

記憶體洩露:物件在記憶體 heap 堆中分配空間,當不再使用或沒有引用指引的情況下,仍不能被 GC 正常回收的情況,多數出現在不合理的編碼情況下,例如在 Activity 中註冊了一個廣播就收器,但是在頁面關閉的時候未進行 unRegister,就會出現記憶體洩露現象,通常情況下,大量的記憶體洩露會導致 OOM

OOM:OutOfMemoery,記憶體溢位是指 APP 向系統申請超過最大閾值的記憶體請求,系統不會在分配多餘空間,就會造成 OOM,在 Android 平臺下,多數情況是出現在圖片處理不當時,造成記憶體洩露

1)靜態變數導致的記憶體洩露

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sContext = this;
    }
}

      上面這種情形是一種最簡單的記憶體洩露,相信大家都不會這麼幹,上面程式碼將導致 Activity 無法正常銷燬,因為靜態變數引用了它,上面的程式碼也可以改造一下,如下所示,sView 是一個靜態變數,它內部持有了當前 Activity,所以 Activity 仍然無法釋放,估計大家也明白

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static View sView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sView = new View(this);
    }
}

2)單例模式導致的記憶體洩露

public class CommUtils {

    private static CommUtils instance;
    private Context context;
    public CommUtils(Context context) {
        this.context = context;
    }

    public static CommUtils getInstance(Context context) {

        if (instance == null) {
            synchronized (CommUtils.class) {
                if (instance == null) {
                    instance = new CommUtils(context);
                }
            }
        }
        return instance;
    }
}

在 MainActivity 中的使用如下:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CommUtils commUtils = CommUtils.getInstance(this);
    }
}

      上面的單例是我們經常寫的,但是是存在記憶體洩露風險的,這樣寫的問題就是,在 MainActivity 裡使用 CommUtils 類,傳入的context 是 MainActivity 的 Context,試想一下,當 MainActivity 宣告週期結束,但 CommUtils 類裡面還存在 MainActiviy 的引用(context),這樣 MainActivity 佔用的記憶體就一直不能回收,而 MainActivity 的物件也不會再被使用,那麼我們如何來解決這個問題呢?在MainActivity中,可以用CommUtils.getInstance(getApplicationContext())或CommUtils.getInstance(getApplication()) 代替,因為 Application 的生命週期是貫穿整個程式的,所以 CommUtils 類持有它的引用,也不會造成記憶體洩露問題

3)非靜態內部類導致的記憶體洩露

非靜態內部類預設持有外部類的引用,使外部類不能被回收,具體解決方法如下:

  1. 去除隱式引用(通過靜態內部類來去除隱式引用)
  2. 手動管理物件引用(修改靜態內部類的構造方式,手動引入其外部類引用)
  3. 當記憶體不可用時,不執行不可控程式碼(Android 可以結合智慧指標,WeakReference 包裹外部類例項)

4)資源未關閉導致的記憶體洩露

      對於使用了 BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap 等資源的程式碼,應該在 Activity 銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩露

5)AsyncTask、Handler 導致的記憶體洩露

基本也是由於非靜態內部類持有外部類的應用,導致外部類在銷燬時無法回收,造成記憶體洩露

6)WebView 導致的記憶體洩

  • 不在 xml 中定義 WebView 節點,而是需要的時候在 Activity 中建立,並且 Context 使用 getApplicationContext()
  • 在 Activity 銷燬 WebView 的時候,先讓 WebView 載入 null 內容,然後移除 WebView,再銷燬 WebView
@Override
protected void onDestroy() {
    if( mWebView!=null) {

        // 如果先呼叫destroy()方法,則會命中if (isDestroyed()) return;這一行程式碼,需要先onDetachedFromWindow(),再
        // destory()
        ViewParent parent = mWebView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mWebView);
        }

        mWebView.stopLoading();
        // 退出時呼叫此方法,移除繫結的服務,否則某些特定系統會報錯
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView.destroy();
    }
    super.on Destroy();
}

關於記憶體優化的內容,今天就先介紹到這裡,如有錯誤,請指正