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:

          8 使用內存相關工具檢測

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


           

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