從Android資源角度談Android程式碼記憶體優化

codeceo發表於2015-07-21

這篇文章主要介紹在實際Android應用程式的開發中,容易導致記憶體洩露的一些情況。開發人員如果在進行程式碼編寫之前就有記憶體洩露方面的基礎知 識,那麼寫出來的程式碼會強壯許多,寫這篇文章也是這個初衷。本文從Android開發中的資源使用情況入手,介紹瞭如何在Bitmap、資料庫查詢、9- patch、過渡繪製等方面優化記憶體的使用。

Android資源優化

1. Bitmap優化

Android中的大部分記憶體問題歸根結底都是Bitmap的問題,如果開啟MAT(Memory analyzer tool)來看,實際佔用記憶體大的都是一些Bitmap(以byte陣列的形式儲存)。所以Bitmap的優化應該是我們著重去解決的。Google在其 官方有針對Bitmap的使用專門寫了一個專題 : Displaying Bitmaps Efficiently , 對應的中文翻譯在 : displaying-bitmaps , 在優化Bitmap資源之前,請先看看這個系列的文件,以確保自己正確地使用了Bitmap。

Bitmap如果沒有被釋放,那麼一般只有兩個問題:

  • 使用者在使用完這個Bitmap之後,沒有主動去釋放Bitmap資源。
  • 這個Bitmap資源被引用所以無法被釋放 。

1.1 主動釋放Bitmap資源

當你確定這個Bitmap資源不會再被使用的時候(當然這個Bitmap不釋放可能會讓程式下一次啟動或者resume快一些,但是其佔用的記憶體 資源太大,可能導致程式在後臺的時候被殺掉,反而得不償失),我們建議手動呼叫recycle()方法,釋放其Native記憶體:

if(bitmap != null && !bitmap.isRecycled()){  
    bitmap.recycle(); 
    bitmap = null; 
}

我們也可以看一下Bitmap.java中recycle()方法的說明:

    /**
     * Free the native object associated with this bitmap, and clear the
     * reference to the pixel data. This will not free the pixel data synchronously;
     * it simply allows it to be garbage collected if there are no other references.
     * The bitmap is marked as "dead", meaning it will throw an exception if
     * getPixels() or setPixels() is called, and will draw nothing. This operation
     * cannot be reversed, so it should only be called if you are sure there are no
     * further uses for the bitmap. This is an advanced call, and normally need
     * not be called, since the normal GC process will free up this memory when
     * there are no more references to this bitmap.
     */
    public void recycle() {
        if (!mRecycled) {
            if (nativeRecycle(mNativeBitmap)) {
                // return value indicates whether native pixel object was actually recycled.
                // false indicates that it is still in use at the native level and these
                // objects should not be collected now. They will be collected later when the
                // Bitmap itself is collected.
                mBuffer = null;
                mNinePatchChunk = null;
            }
            mRecycled = true;
        }
    }

......
//如果使用過程中丟擲異常的判斷
if (bitmap.isRecycled()) {
    throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap);
}

呼叫bitmap.recycle之後,這個Bitmap如果沒有被引用到,那麼就會被垃圾回收器回收。如果不主動呼叫這個方法,垃圾回收器也會進 行回收工作,只不過垃圾回收器的不確定性太大,依賴其自動回收不靠譜(比如垃圾回收器一次性要回收好多Bitmap,那麼需要的時間就會很多,導致回收的 時候會卡頓)。所以我們需要主動呼叫recycle。

1.2 主動釋放ImageView的圖片資源

由於我們在實際開發中,很多情況是在xml佈局檔案中設定ImageView的src或者在程式碼中呼叫 ImageView.setImageResource/setImageURI/setImageDrawable等方法設定影像,下面程式碼可以回收這 個ImageView所對應的資源:

private static void recycleImageViewBitMap(ImageView imageView) {
    if (imageView != null) {
        BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
        rceycleBitmapDrawable(bd);
    }
}

private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
    if (bitmapDrawable != null) {
        Bitmap bitmap = bitmapDrawable.getBitmap();
        rceycleBitmap(bitmap);
    }
    bitmapDrawable = null;
}

private static void rceycleBitmap(Bitmap bitmap) {
    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
        bitmap = null;
    }
}

1.3 主動釋放ImageView的背景資源

如果你的ImageView是有Background,那麼下面的程式碼可以釋放他:

public static void recycleBackgroundBitMap(ImageView view) {
    if (view != null) {
        BitmapDrawable bd = (BitmapDrawable) view.getBackground();
        rceycleBitmapDrawable(bd);
    }
}

public static void recycleImageViewBitMap(ImageView imageView) {
    if (imageView != null) {
        BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
        rceycleBitmapDrawable(bd);
    }
}

private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
    if (bitmapDrawable != null) {
        Bitmap bitmap = bitmapDrawable.getBitmap();
        rceycleBitmap(bitmap);
    }
    bitmapDrawable = null;
}

1.4 儘量少用Png圖,多用NinePatch的圖

現在手機的解析度越來越高,圖片資源在被載入後所佔用的記憶體也越來越大,所以要儘量避免使用大的PNG圖,在產品設計的時候就要儘量避免用一張大圖來進行展示,儘量多用NinePatch資源。

Android中的NinePatch指的是一種拉伸後不會變形的特殊png圖,NinePatch的拉伸區域可以自己定義。這種圖的優點是體積 小,拉伸不變形,可以適配多機型。Android SDK中有自帶NinePatch資源製作工具,Android-Studio中在普通png圖片點選右鍵可以將其轉換為NinePatch資源,使用起 來非常方便。

Android程式碼記憶體優化建議-Android資源篇

1.5 使用大圖之前,儘量先對其進行壓縮

圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現出來的要大很多。例如,系統的Gallery程式會顯示那些你使用裝置camera拍攝的圖片,但是那些圖片的解析度通常都比你的裝置螢幕解析度要高很多。

考慮到程式是在有限的記憶體下工作,理想情況是你只需要在記憶體中載入一個低解析度的版本即可。這個低解析度的版本應該是與你的UI大小所匹配的,這 樣才便於顯示。一個高解析度的圖片不會提供任何可見的好處,卻會佔用寶貴的(precious)的記憶體資源,並且會在快速滑動圖片時導致(incurs) 附加的效率問題。

Google官網的Training中,有一篇文章專門介紹如何有效地載入大圖,裡面提到了兩個比較重要的技術:

  • 在圖片載入前獲取其寬高和型別
  • 載入一個按比例縮小的版本到記憶體中

原文地址: Loading Large Bitmaps Efficiently ,中文翻譯地址: 有效地載入大尺寸點陣圖 ,強烈建議每一位Android開發者都去看一下,並在自己的實際專案中使用到。

更多關於Bitmap的使用和優化,可以參考Android官方Training專題的 displaying-bitmaps

2 查詢資料庫沒有關閉遊標

程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。示例程式碼:

Cursor cursor = getContentResolver().query(uri ...);
  if (cursor.moveToNext()) {
 	... ... 
}

修正示例程式碼:

Cursor cursor = null;
try {
  	cursor = getContentResolver().query(uri ...);
  if (cursor != null && cursor.moveToNext()) {
  ... ... 
  }
  } finally {
    if (cursor != null) {
  try { 
    cursor.close();
  } catch (Exception e) {
    //ignore this
    }
  }
}

3 構造Adapter時,沒有使用快取的convertView

以構造ListView的BaseAdapter為例,在BaseAdapter中提供了方法:

public View getView(int position, View convertView, ViewGroup parent)

來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的螢幕佈局例項化一 定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。由此可以看出,如果我們不去使用 convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費資源也浪費時間,也會使得記憶體佔用越來越大。 ListView回收list item的view物件的過程可以檢視:android.widget.AbsListView.java —> void addScrapView(View scrap) 方法。

Android程式碼記憶體優化建議-Android資源篇

示例程式碼:

public View getView(int position, View convertView, ViewGroup parent) {
 View view = new Xxx(...);
 ... ...
 return view;
}

`示例修正程式碼:

public View getView(int position, View convertView, ViewGroup parent) {
 View view = null;
 if (convertView != null) {
 view = convertView;
 populate(view, getItem(position));
 ...
 } else {
 view = new Xxx(...);
 ...
 }
 return view;
}

關於ListView的使用和優化,可以參考這兩篇文章:

4 釋放物件的引用

前面有說過,一個物件的記憶體沒有被釋放是因為他被其他的物件所引用,系統不回去釋放這些有GC Root的物件。

示例A:假設有如下操作

public class DemoActivity extends Activity {
  ... ...
  private Handler mHandler = ...
  private Object obj;
  public void operation() {
   obj = initObj();
   ...
   [Mark]
   mHandler.post(new Runnable() {
          public void run() {
           useObj(obj);
          }
   });
  }
}

我們有一個成員變數 obj,在operation()中我們希望能夠將處理obj例項的操作post到某個執行緒的MessageQueue中。在以上的程式碼中,即便是 mHandler所在的執行緒使用完了obj所引用的物件,但這個物件仍然不會被垃圾回收掉,因為DemoActivity.obj還保有這個物件的引用。 所以如果在DemoActivity中不再使用這個物件了,可以在[Mark]的位置釋放物件的引用,而程式碼可以修改為:

public void operation() {
  obj = initObj();
  ...
  final Object o = obj;
  obj = null;
  mHandler.post(new Runnable() {
      public void run() {
          useObj(o);
      }
  }
}

示例B:假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen 中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要 顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。

但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen 無法被垃圾回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得 system_ui程式掛掉。

總之當一個生命週期較短的物件A,被一個生命週期較長的物件B保有其引用的情況下,在A的生命週期結束時,要在B中清除掉對A的引用。

使用MAT可以很方便地檢視物件之間的引用,

5 在Activity的生命週期中釋放資源

Android應用程式中最典型的需要注意釋放資源的情況是在Activity的生命週期中,在onPause()、onStop()、 onDestroy()方法中需要適當的釋放資源的情況。由於此情況很基礎,在此不詳細說明,具體可以檢視官方文件對Activity生命週期的介紹,以 明確何時應該釋放哪些資源。

6 消除過渡繪製

過渡繪製指的是在螢幕一個畫素上繪製多次(超過一次),比如一個TextView後有背景,那麼顯示文字的畫素至少繪了兩次,一次是背景,一次是 文字。GPU過度繪製或多或少對效能有些影響,裝置的記憶體頻寬是有限的,當過度繪製導致應用需要更多的頻寬(超過了可用頻寬)的時候效能就會降低。頻寬的 限制每個裝置都可能是不一樣的。

過渡繪製的原因:

  1. 同一層級的View疊加
  2. 複雜的層級疊加

減少過渡繪製能去掉一些無用的View,能有效減少GPU的負載,也可以減輕一部分記憶體壓力。關於過渡繪製我專門寫了一篇文章來介紹:過渡繪製及其優化

7 使用Android系統自帶的資源

在Android應用開發過程中,螢幕上控制元件的佈局程式碼和程式的邏輯程式碼通常是分開的。介面的佈局程式碼是放在一個獨立的xml檔案中的,這個檔案 裡面是樹型組織的,控制著頁面的佈局。通常,在這個頁面中會用到很多控制元件,控制元件會用到很多的資源。Android系統本身有很多的資源,包括各種各樣的字 符串、圖片、動畫、樣式和佈局等等,這些都可以在應用程式中直接使用。這樣做的好處很多,既可以減少記憶體的使用,又可以減少部分工作量,也可以縮減程式安 裝包的大小。

比如下面的程式碼就是使用系統的ListView:

<ListView 
    android:id="@android:id/list"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"/>

8 使用記憶體相關工具檢測

在開發中,不可能保證一次就開發出一個記憶體管理非常棒的應用,所以在開發的每一個階段,都要有意識地去針對記憶體進行專門的檢查。目前Android提供了許多佈局、記憶體相關的工具,比如Lint、MAT等。學會這些工具的使用是一個Android開發者必不可少的技能。

相關文章