1. 程式人生 > >Android記憶體管理機制和記憶體洩漏分析及優化

Android記憶體管理機制和記憶體洩漏分析及優化

Android中的記憶體管理機制

分配機制

Android為每個程序分配記憶體的時候,採用了彈性的分配方式,也就是剛開始並不會一下分配很多記憶體給每個程序,而是給每一個程序分配一個“夠用”的量。這個量是根據每一個裝置實際的實體記憶體大小來決定的。隨著應用的執行,可能會發現當前的記憶體可能不夠使用了,這時候Android又會為每個程序分配一些額外的記憶體大小。但是這些額外的大小並不是隨意的,也是有限度的,系統不可能為每一個App分配無限大小的記憶體。

Android系統的宗旨是最大限度的讓更多的程序存活在記憶體中,因為這樣的話,下一次使用者再啟動應用,不需要重新建立程序,只需要恢復已有的程序就可以了,減少了應用的啟動時間,提高了使用者體驗。

回收機制

Android對記憶體的使用方式是“盡最大限度的使用”,這一點繼承了Linux的優點。Android會在記憶體中儲存儘可能多的資料,即使有些程序不再使用了,但是它的資料還被儲存在記憶體中,所以Android現在不推薦顯式的“退出”應用。因為這樣,當用戶下次再啟動應用的時候,只需要恢復當前程序就可以了,不需要重新建立程序,這樣就可以減少應用的啟動時間。只有當Android系統發現記憶體不夠使用,需要回收記憶體的時候,Android系統就會需要殺死其他程序,來回收足夠的記憶體。但是Android也不是隨便殺死一個程序,比如說一個正在與使用者互動的程序,這種後果是可怕的。所以Android會有限清理那些已經不再使用的程序,以保證最小的副作用。

Android殺死程序有兩個參考條件:
程序優先順序:

Android為每一個程序分配了優先順序的概念,優先順序越低的程序,被殺死的概率就更大。Android中總共有5個程序優先順序。具體含義這裡不再給出。
- 前臺程序:正常不會被殺死
- 可見程序:正常不會被殺死
- 服務程序:正常不會被殺死
- 後臺程序:存放於一個LRU快取列表中,先殺死處於列表尾部的程序
- 空程序:正常情況下,為了平衡系統整體效能,Android不儲存這些程序

回收收益:

當Android系統開始殺死LRU快取中的程序時,系統會判斷每個程序殺死後帶來的回收收益。因為Android總是傾向於殺死一個能回收更多記憶體的程序,從而可以殺死更少的程序,來獲取更多的記憶體。殺死的程序越少,對使用者體驗的影響就越小。

官方推薦的App記憶體使用方式
  • 當Service完成任務後,儘量停止它。因為有Service元件的程序,優先順序最低也是服務程序,這會影響到系統的記憶體回收。IntentService可以很好地完成這個任務。
  • 在UI不可見的時候,釋放掉一些只有UI使用的資源。系統會根據onTrimMemory()回撥方法d的TRIM_MEMORY_UI_HIDDEN等級的事件,來通知App UI已經隱藏了。
  • 在系統記憶體緊張的時候,儘可能多的釋放掉一些非重要資源。系統會根據onTrimMemory()回撥方法來通知記憶體緊張的狀態,App應該根據不同的記憶體緊張等級,來合理的釋放資源,以保證系統能夠回收更多記憶體。當系統回收到足夠多的記憶體時,就不用殺死程序了。
  • 檢查自己最大可用的記憶體大小。這對一些快取框架很有用,因為正常情況下,快取框架的快取池大小應當指定為最大記憶體的百分比,這樣才能更好地適配更多的裝置。通過getMemoryClass()和getLargeMemoryClass()來獲取可用記憶體大小的資訊。
  • 避免濫用Bitmap導致的記憶體浪費。
    根據當前裝置的解析度來壓縮Bitmap是一個不錯的選擇,在使用完Bitmap後,記得要使用recycle()來釋放掉Bitmap。使用軟引用或者弱引用來引用一個Bitmap,使用LRU快取來對Bitmap進行快取。
  • 使用針對記憶體優化過的資料容器。針對移動裝置記憶體有限的問題,Android提供了一套針對記憶體優化過的資料容器,來替代JDK原生提供的資料容器。但是缺點就是,時間複雜度被提高了。比如SparseArray、SparseBooleanArray、LongSparseArray、
  • 意識到記憶體的過度消耗。Enum型別佔用的記憶體是常量的兩倍多,所以避免使用enum,直接使用常量。
    每一個Java的類(包括匿名內部類)都需要500Byte的程式碼。每一個類的例項都有12-16 Byte的額外記憶體消耗。注意類似於HashMap這種,內部還需要生成Class的資料容器,這會消耗更多記憶體。
  • 抽象程式碼也會帶來更多的記憶體消耗。如果你的“抽象”設計實際上並沒有帶來多大好處,那麼就不要使用它。
  • 使用nano protobufs 來序列化資料。Google設計的一個語言和平臺中立打的序列化協議,比XML更快、更小、更簡單。
  • 避免使用依賴注入的框架。依賴注入的框架需要開啟額外的服務,來掃描App中程式碼的Annotation,所以需要額外的系統資源。
  • 使用ZIP對齊的APK。對APK做Zip對齊,會壓縮其內部的資源,執行時會佔用更少的記憶體。
  • 合理使用多程序。

Android記憶體洩漏分析及優化

記憶體洩漏的根本原因

記憶體洩漏分析1.png

如上圖所示,GC會選擇一些它瞭解還存活的物件作為記憶體遍歷的根節點(GC Roots),比方說thread stack中的變數,JNI中的全域性變數,zygote中的物件(class loader載入)等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。如下圖藍色部分。

記憶體洩漏分析2.png
記憶體洩漏指的是程序中某些物件(垃圾物件)已經沒有使用價值了,但是它們卻可以直接或間接地引用到gc roots導致無法被GC回收。無用的物件佔據著記憶體空間,使得實際可使用記憶體變小,形象地說法就是記憶體洩漏了。下面分析一些可能導致記憶體洩漏的情景。

Android中常見的記憶體洩漏原因
1.使用static變數引起的記憶體洩漏

因為static變數的生命週期是在類載入時開始 類解除安裝時結束,也就是說static變數是在程式程序死亡時才釋放,如果在static變數中引用了Activity那麼這個Activity由於被引用,便會隨static變數的生命週期一樣,一直無法被釋放,造成記憶體洩漏。

一般解決辦法:
想要避免context相關的記憶體洩漏,需要注意以下幾點:
- 不要對activity的context長期引用(一個activity的引用的生存週期應該和activity的生命週期相同)
- 如果可以的話,儘量使用關於application的context來替代和activity相關的context
- 如果一個acitivity的非靜態內部類的生命週期不受控制,那麼避免使用它;正確的方法是使用一個靜態的內部類,並且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所做的那樣,使用弱引用private final WeakReference mViewAncestor;。

下面的程式碼存在記憶體洩漏的問題,非靜態內部類的靜態例項導致記憶體洩漏。

/**
mDemo會獲得並一直持有MemoryLeakActivity的引用。當MemoryLeakActivity銷燬後重建,因為mDemo持有引用,無法被GC回收的,程序中會存在2個MemoryLeakActivity例項。所以,對於lauchMode不是singleInstance的Activity, 應該避免在activity裡面例項化其非靜態內部類的靜態例項。
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    static Demo mDemo;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("啟動一個持有本物件的執行緒");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mDemo = new Demo();
        mDemo.run();
    }
    class Demo{
        void run(){
            Log.i(TAG, "run: ");
        }
    }
}

解決方法:將Demo改成靜態內部類
因為普通的內部類物件隱含地儲存了一個引用,指向建立它的外圍類物件。然而,當內部類是static的時,就不是這樣了。巢狀類意味著: 1. 巢狀類的物件,並不需要其外圍類的物件。 2. 不能從巢狀類的物件中訪問非靜態的外圍類物件。

2.執行緒引起的記憶體洩漏

下面的程式碼存在記憶體洩漏的問題,啟動執行緒的匿名內部類會持有MemoryLeakActivity.this的引用。如果執行緒還沒有結束,Activity已經銷燬那就會造成記憶體洩漏。

public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("啟動一個持有本物件的執行緒");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(8000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}

解決辦法:
1.合理安排執行緒執行的時間,控制執行緒在Activity結束前結束。
2.將內部類改為靜態內部類,並使用弱引用WeakReference來儲存Activity例項 因為弱引用 只要GC發現了 就會回收它 ,因此可儘快回收。

將匿名內部類改成靜態類,避免了Activity context的記憶體洩漏問題

/**
 * 示例通過將執行緒類宣告為私有的靜態內部類避免了 Activity context 的記憶體洩漏問題,但
 * 在配置發生改變後,執行緒仍然會執行。原因在於,DVM 虛擬機器持有所有執行執行緒的引用,無論
 * 這些執行緒是否被回收,都與 Activity 的生命週期無關。執行中的執行緒只會繼續執行,直到
 * Android 系統將整個應用程序殺死
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("啟動一個持有本物件的執行緒");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        new MyThread().start();
    }

    private static class MyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(8000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } 
        }
    }
}

在Activity生命週期的onDestory中結束執行緒執行

/**
* 除了我們需要實現銷燬邏輯以保證執行緒不會發生記憶體洩漏。在退出當前
* Activity 前使用 onDestroy() 方法結束你的執行中執行緒。
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    private static boolean mRunnale = false;
    private MyThread mThread; 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("啟動一個持有本物件的執行緒");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        new Thread(runnable).start();
        new MyThread().start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mThread.closeThread();
    }

    private static class MyThread extends Thread{
        @Override
        public void run() {
            mRunnale = true;
            while (true){
                //TODO
                Log.i(TAG, "run: do something");
            }
        }

        public void closeThread(){
            mRunnale = false;
        }
    }
}
3.Handler的使用造成的記憶體洩漏

由於在Handler的使用中,handler會發送message物件到 MessageQueue中 然後 Looper會輪詢MessageQueue 然後取出Message執行,但是如果一個Message長時間沒被取出執行,那麼由於 Message中有 Handler的引用,而 Handler 一般來說也是內部類物件,Message引用 Handler ,Handler引用 Activity 這樣 使得 Activity無法回收。或者說Handler在Activity退出時依然還有訊息需要處理,那麼這個Activity就不會被回收。

解決辦法:
依舊使用 靜態內部類+弱引用的方式 可解決
例如下面的程式碼

public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    private MyHandler mHandler;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("啟動一個持有本物件的執行緒");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mHandler = new MyHandler(this);
        mHandler.sendEmptyMessage(0);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //第三步,在Activity退出的時候移除回撥
        mHandler.removeCallbacksAndMessages(null);
    }

    //第一步,將Handler改成靜態內部類。
    static class MyHandler extends Handler{
        //第二步,將需要引用Activity的地方,改成弱引用。
        private WeakReference<MemoryLeakActivity> mActivityRef;
        public MyHandler(MemoryLeakActivity activity){
            mActivityRef = new WeakReference<MemoryLeakActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MemoryLeakActivity mla = mActivityRef == null ? null : mActivityRef.get();
            if(mla == null || mla.isFinishing()){
                return;
            }
            //TODO
            mla.view.setText("do something");

        }
    }
}
4.資源未被及時關閉造成的記憶體洩漏

比如一些Cursor 沒有及時close 會儲存有Activity的引用,導致記憶體洩漏

解決辦法:
在onDestory方法中及時 close即可

5.BitMap佔用過多記憶體

bitmap的解析需要佔用記憶體,但是記憶體只提供8M的空間給BitMap,如果圖片過多,並且沒有及時 recycle bitmap 那麼就會造成記憶體溢位。

解決辦法:
及時recycle 壓縮圖片之後載入圖片

其中還有一些關於 集合物件沒移除,註冊的物件沒反註冊,程式碼壓力的問題也可能產生記憶體洩漏,但是使用上述的幾種解決辦法一般都是可以解決的。