記憶體使用總結篇 -- Android 記憶體優化第五彈

anly_jun發表於2016-11-04

前面幾彈從Android記憶體管理, GC機制理論, 到記憶體分析工具, 記憶體洩露例項分析等幾個方面聊了下Android App中關於記憶體優化的一些個知識.

本篇作為Android App記憶體優化的第五彈, 也是最後一彈, 將對Andorid中的記憶體優化做一個簡單的總結.

1, 回顧

系列文連結:

1.GC那些事兒
2.Android的記憶體管理
3.記憶體分析工具
4.記憶體洩露例項分析

幾個要點:

  • Android的App執行在Dalvik/ART這種類JVM環境的, 使用的是自動記憶體管理方式, 也就是通常說的GC機制.
  • 每個App預設單獨執行在一個VM程式內, 其記憶體使用是有上限的.
  • 所謂GC就是回收垃圾物件.
  • 所謂垃圾, 就是GC Roots不可達的物件, 也就是死物件(相對於活物件).
  • 物件佔用記憶體(Retained Size)是其所支配(Dominate)的所有子物件的佔用記憶體之和. 故而我們找記憶體消耗點, 和記憶體洩露的時候都是關注物件的Retained Size.
  • 所謂記憶體分析, 最多是就是使用工具定位是哪個物件支配著某個Retained Size很大的物件, 進而定位出記憶體消耗或記憶體洩露點.

回顧之後, 我們再來看下記憶體問題.

2, 記憶體問題

從大的分類上來說, Android App中關於記憶體的問題大致可以分成如下三類:

  • 記憶體洩露
  • 記憶體消耗過大
  • 記憶體抖動

前二者, 記憶體洩露和記憶體消耗過大, 最終的結果就是我們常見的OutOfMemoryException, 今天我們的記憶體使用總結也主要是針對這二者.

關於記憶體抖動我們在App優化之消除卡頓一文中有描述.

3, 常見的記憶體洩露及其解決方案

以下關於洩露的名字, 個人根據自己的習慣起的, 並非哪兒的官方稱呼, 希望沒有誤導到吃瓜群眾.

3.1 Context洩露

Context使用不當導致的記憶體洩露.
一般來說是因為某些全部的物件, 理當使用Application級別的Context, 而使用了指定Activity的Context, 導致該Activity無法釋放.

例如, 某個單例中需要一個Context, 傳入了一個Activity的Context, 導致其被這個單例持續引用而無法回收.

這類洩露的解決方案, 就是根據元件的生命週期來正確使用Context, 全域性引用使用Application Context.

關於各種Context的說明和使用請參看這篇譯文.
Context洩露的例項還可以看下android dev blog中的這篇, 需翻牆.

3.2 內部類洩露

由於(匿名)內部類隱式地持有一個外部類的引用, 故而當內部類中執行的事情長於外部類的生命週期時, 就會導致外部類的洩露.

常見的此類洩露包括Handler洩露, Thread洩露..., 這些也是我們經常會作為(匿名)內部類在Activity中使用的.

下面以HandlerLeak為例:

如下:

public class HandlerLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        mHandler.sendEmptyMessageDelayed(1, 60 * 1000);

    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}複製程式碼

這段程式碼我們實際上非常多的使用, 然而如果我們用android lint工具檢測的話, 會有一段這樣的提示:

記憶體使用總結篇 -- Android 記憶體優化第五彈

也就是說這個Handler類可能會導致記憶體洩露, 建議我們使用static方式. 點開"more", 我們來看下官方的建議解決方案:

Since this Handler is declared as an inner class, it may prevent the outer
class from being garbage collected. If the Handler is using a Looper or
MessageQueue for a thread other than the main thread, then there is no issue.
If the Handler is using the Looper or MessageQueue of the main thread, you
need to fix your Handler declaration, as follows: Declare the Handler as a
static class; In the outer class, instantiate a WeakReference to the outer
class and pass this object to your Handler when you instantiate the Handler;
Make all references to members of the outer class using the WeakReference
object.複製程式碼

閱讀這段"more"的前半段, 我們分析下洩露是怎麼產生的:

因為這個Handler是一個內部類(預設持有一個外部類也就是我們的HandlerLeakActivity的引用), 如果這個Handler的Looper/MQ所在的Thread與Main Thread不同, 則沒有問題. 但是如果Handler的Looper/MQ就是Main Thread(本例中就是), 那麼問題就來了:

這個Handler傳送的message會放到MQ中, 這個message會對Handler有一個引用, 而Handler有HandlerLeakActivity的引用. 當我們進入這個Activity, 然後退出, 理當銷燬這個Activity並回收了. 但是因為這個message會延時60s, 故而導致這個mHandler被引用, 從而activity被引用著, 而無法回收釋放記憶體.

GC那些事兒中, 我們就講到, 執行中的Thread就是GC Root之一, 根據上面的分析, 得出: HandlerLeakActivity到GC Roots可達, 故而無法回收.

我們用LeakCanary來驗證下我們的分析:

記憶體使用總結篇 -- Android 記憶體優化第五彈

可以看到, 果然如我們分析的.

那麼此類問題怎麼解決呢, 可能很多同學也直接使用加上@SuppressLint("HandlerLeak")的方式來避免lint提示了, 如下:

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
   @Override
   public void handleMessage(Message msg) {
       super.handleMessage(msg);
   }
};複製程式碼

然而這並非解決之道, 其實這段"more"的後半段也給了我們解決方案 --- 使用Static + WeakReference的方式, 具體如下:

public class HandlerLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        new DemoHandler(this).sendEmptyMessageDelayed(1, 60 * 1000);

    }

    private static class DemoHandler extends Handler {

        private final WeakReference<HandlerLeakActivity> mActivity;


        private DemoHandler(HandlerLeakActivity activity) {
            this.mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            HandlerLeakActivity activity = mActivity.get();
            if (activity != null) {
                activity.doSomething();
            }
        }
    }

    private void doSomething() {

    }
}複製程式碼

留下一個問題, 為什麼說這個Handler不在Main Thread的時候不會有問題, 大家可以自行研究下, 有機會就HandlerLeak這個話題我們再深入研究下.

3.3 Register洩露

對於觀察者, 廣播, Listener等, 註冊和登出沒有成對出現而導致的記憶體洩露.

記憶體洩露例項中那個例子, 就是這種洩露, 在此不在細述了.

解決方案就是編碼的時候多注意吧, add/remove, register/unregister, bind/unbind什麼的~.

3.4 資源洩露

常見的資料庫查詢Cursor, 檔案讀寫流等, 用完沒有關閉導致的記憶體洩露.

例如:

public class CursorLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

        if (cursor != null) {
            cursor.moveToFirst();

            // do something.
        }
    }
}複製程式碼

這個cursor就可能洩露, 實際上android lint也給了我們提示:

記憶體使用總結篇 -- Android 記憶體優化第五彈

此類問題的解決方案, 一般我們使用try-catch-finally的結構, 在finally中關閉並釋放資源.
如下:

public class CursorLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

        try {
            if (cursor != null) {
                cursor.moveToFirst();

                // do something.
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if (cursor != null) {
                cursor.close();
                cursor = null;
            }
        }
    }
}複製程式碼

3.5 Bitmap洩露

Bitmap沒有及時呼叫recycle()回收導致的洩露.

對於Bitmap是使用, 一直就是Android開發者的痛, 特別是對大圖片的處理. 可以說我們大多數的報出來的OutOfMemory異常基本都是因為要給某個Bitmap分配記憶體, 而可用記憶體不夠導致的.

3.6 記憶體洩露小結

對於記憶體洩露, 我們儘量是以防為主. 根據上面的常見記憶體洩露, 我們需要注意以下幾點:

  • Context的(根據元件生命週期)合理使用.
  • 避免在Activity中使用非靜態內部類, 可以靜態內部類+WeakReference達成目的.
  • 注意add/remove, register/unregister, bind/unbind的成對使用.
  • 資源及時關閉, 釋放.

4, 有效使用記憶體的建議

本節大部分內容來自官方開發文件.

  • 合理使用Service
    Service的及時關閉可以讓我們節省記憶體消耗, 對於一次性的任務, 建議使用IntentService.

  • 使用優化後的資料容器
    使用Android提供的SparseArray, SparseBooleanArray, LongSparseArray來代替HashMap的使用.
    關於HashMap,ArrayMap,SparseArray, 這篇文章有個比較直觀的比較, 可以看下.

  • 少用列舉enum結構
    相比於靜態常量(static final), enum會耗費雙倍的記憶體.

  • 避免建立不必要的物件
    諸如一些臨時物件, 特別是迴圈中的.

  • 考慮實現onTrimMemory(), 在此根據當前的記憶體狀態做些處理.

  • Bitmap的合理有效使用.
    對於Bitmap的使用, 建議直接檢視官方開發文件中的高效顯示Bitmap(需翻牆).

結語

至此, Android App記憶體優化的5發子彈就打完了, 關於App記憶體優化的部分, 我們就先到這裡了, 可能還有很多沒有覆蓋到的內容.

順便, 再次表明下我寫文的思想: 一個是想記錄下自己的一個解決問題的思路和經驗, 再一個是想傳達如何去解決問題的思想. 故而, 文章並不是一開始就說有哪些記憶體問題, 怎麼解. 而是從理論基礎到分析工具的使用, 案例的分析去一步步的學會怎麼處理這類問題.

希望大家能從中得到一些關於解決問題的啟發, 而非被灌輸一些強記下的知識.

感謝相隨...

相關文章