Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 從Android資源角度談Android代碼內存優化

從Android資源角度談Android代碼內存優化

編輯:Android資訊

這篇文章主要介紹在實際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的使用和優化,可以參考這兩篇文章:

  • Using lists in Android (ListView) – Tutorial
  • Making ListView Scrolling Smooth

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開發者必不可少的技能。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved