Android內(nèi)存管理機(jī)制和內(nèi)存泄漏分析及優(yōu)化

Android中的內(nèi)存管理機(jī)制

分配機(jī)制

Android為每個(gè)進(jìn)程分配內(nèi)存的時(shí)候,采用了彈性的分配方式,也就是剛開(kāi)始并不會(huì)一下分配很多內(nèi)存給每個(gè)進(jìn)程,而是給每一個(gè)進(jìn)程分配一個(gè)“夠用”的量。這個(gè)量是根據(jù)每一個(gè)設(shè)備實(shí)際的物理內(nèi)存大小來(lái)決定的。隨著應(yīng)用的運(yùn)行,可能會(huì)發(fā)現(xiàn)當(dāng)前的內(nèi)存可能不夠使用了,這時(shí)候Android又會(huì)為每個(gè)進(jìn)程分配一些額外的內(nèi)存大小。但是這些額外的大小并不是隨意的,也是有限度的,系統(tǒng)不可能為每一個(gè)App分配無(wú)限大小的內(nèi)存。

Android系統(tǒng)的宗旨是最大限度的讓更多的進(jìn)程存活在內(nèi)存中,因?yàn)檫@樣的話,下一次用戶再啟動(dòng)應(yīng)用,不需要重新創(chuàng)建進(jìn)程,只需要恢復(fù)已有的進(jìn)程就可以了,減少了應(yīng)用的啟動(dòng)時(shí)間,提高了用戶體驗(yàn)。

回收機(jī)制

Android對(duì)內(nèi)存的使用方式是“盡最大限度的使用”,這一點(diǎn)繼承了Linux的優(yōu)點(diǎn)。Android會(huì)在內(nèi)存中保存盡可能多的數(shù)據(jù),即使有些進(jìn)程不再使用了,但是它的數(shù)據(jù)還被存儲(chǔ)在內(nèi)存中,所以Android現(xiàn)在不推薦顯式的“退出”應(yīng)用。因?yàn)檫@樣,當(dāng)用戶下次再啟動(dòng)應(yīng)用的時(shí)候,只需要恢復(fù)當(dāng)前進(jìn)程就可以了,不需要重新創(chuàng)建進(jìn)程,這樣就可以減少應(yīng)用的啟動(dòng)時(shí)間。只有當(dāng)Android系統(tǒng)發(fā)現(xiàn)內(nèi)存不夠使用,需要回收內(nèi)存的時(shí)候,Android系統(tǒng)就會(huì)需要?dú)⑺榔渌M(jìn)程,來(lái)回收足夠的內(nèi)存。但是Android也不是隨便殺死一個(gè)進(jìn)程,比如說(shuō)一個(gè)正在與用戶交互的進(jìn)程,這種后果是可怕的。所以Android會(huì)有限清理那些已經(jīng)不再使用的進(jìn)程,以保證最小的副作用。

Android殺死進(jìn)程有兩個(gè)參考條件:
進(jìn)程優(yōu)先級(jí):

Android為每一個(gè)進(jìn)程分配了優(yōu)先級(jí)的概念,優(yōu)先級(jí)越低的進(jìn)程,被殺死的概率就更大。Android中總共有5個(gè)進(jìn)程優(yōu)先級(jí)。具體含義這里不再給出。

  • 前臺(tái)進(jìn)程:正常不會(huì)被殺死
  • 可見(jiàn)進(jìn)程:正常不會(huì)被殺死
  • 服務(wù)進(jìn)程:正常不會(huì)被殺死
  • 后臺(tái)進(jìn)程:存放于一個(gè)LRU緩存列表中,先殺死處于列表尾部的進(jìn)程
  • 空進(jìn)程:正常情況下,為了平衡系統(tǒng)整體性能,Android不保存這些進(jìn)程
回收收益:

當(dāng)Android系統(tǒng)開(kāi)始?xì)⑺繪RU緩存中的進(jìn)程時(shí),系統(tǒng)會(huì)判斷每個(gè)進(jìn)程殺死后帶來(lái)的回收收益。因?yàn)锳ndroid總是傾向于殺死一個(gè)能回收更多內(nèi)存的進(jìn)程,從而可以殺死更少的進(jìn)程,來(lái)獲取更多的內(nèi)存。殺死的進(jìn)程越少,對(duì)用戶體驗(yàn)的影響就越小。

官方推薦的App內(nèi)存使用方式
  • 當(dāng)Service完成任務(wù)后,盡量停止它。因?yàn)橛蠸ervice組件的進(jìn)程,優(yōu)先級(jí)最低也是服務(wù)進(jìn)程,這會(huì)影響到系統(tǒng)的內(nèi)存回收。IntentService可以很好地完成這個(gè)任務(wù)。
  • 在UI不可見(jiàn)的時(shí)候,釋放掉一些只有UI使用的資源。系統(tǒng)會(huì)根據(jù)onTrimMemory()回調(diào)方法的TRIM_MEMORY_UI_HIDDEN等級(jí)的事件,來(lái)通知App UI已經(jīng)隱藏了。這和onStop()方法還是有很大區(qū)別的,因?yàn)閛nStop()方法只是當(dāng)一個(gè)Activity完全不可見(jiàn)的時(shí)候就會(huì)調(diào)用,比如說(shuō)用戶打開(kāi)了我們程序中的另一個(gè)Activity。因此,我們可以在onStop()方法中去釋放一些Activity相關(guān)的資源,比如說(shuō)取消網(wǎng)絡(luò)連接或者注銷廣播接收器等,但是UI相關(guān)的資源等onTrimMemory(TRIM_MEMORY_UI_HIDDEN)這個(gè)回調(diào)之后才去釋放,這樣可以保證如果用戶只是從我們程序的一個(gè)Activity回到了另外一個(gè)Activity,界面相關(guān)的資源都不需要重新加載,從而提升響應(yīng)速度。
@Override  
public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 進(jìn)行資源釋放操作  
        break;  
    }  
}  
  • 在系統(tǒng)內(nèi)存緊張的時(shí)候,盡可能多的釋放掉一些非重要資源。系統(tǒng)會(huì)根據(jù)onTrimMemory()回調(diào)方法來(lái)通知內(nèi)存緊張的狀態(tài),App應(yīng)該根據(jù)不同的內(nèi)存緊張等級(jí),來(lái)合理的釋放資源,以保證系統(tǒng)能夠回收更多內(nèi)存。當(dāng)系統(tǒng)回收到足夠多的內(nèi)存時(shí),就不用殺死進(jìn)程了。
  • 檢查自己最大可用的內(nèi)存大小。這對(duì)一些緩存框架很有用,因?yàn)檎G闆r下,緩存框架的緩存池大小應(yīng)當(dāng)指定為最大內(nèi)存的百分比,這樣才能更好地適配更多的設(shè)備。通過(guò)getMemoryClass()和getLargeMemoryClass()來(lái)獲取可用內(nèi)存大小的信息。
  • 避免濫用Bitmap導(dǎo)致的內(nèi)存浪費(fèi)。
    根據(jù)當(dāng)前設(shè)備的分辨率來(lái)壓縮Bitmap是一個(gè)不錯(cuò)的選擇,在使用完Bitmap后,記得要使用recycle()來(lái)釋放掉Bitmap。使用軟引用或者弱引用來(lái)引用一個(gè)Bitmap,使用LRU緩存來(lái)對(duì)Bitmap進(jìn)行緩存。
  • 使用針對(duì)內(nèi)存優(yōu)化過(guò)的數(shù)據(jù)容器。針對(duì)移動(dòng)設(shè)備內(nèi)存有限的問(wèn)題,Android提供了一套針對(duì)內(nèi)存優(yōu)化過(guò)的數(shù)據(jù)容器,來(lái)替代JDK原生提供的數(shù)據(jù)容器。但是缺點(diǎn)就是,時(shí)間復(fù)雜度被提高了。比如SparseArray、SparseBooleanArray、LongSparseArray、
  • 意識(shí)到內(nèi)存的過(guò)度消耗。Enum類型占用的內(nèi)存是常量的兩倍多,所以避免使用enum,直接使用常量。
    每一個(gè)Java的類(包括匿名內(nèi)部類)都需要500Byte的代碼。每一個(gè)類的實(shí)例都有12-16 Byte的額外內(nèi)存消耗。注意類似于HashMap這種,內(nèi)部還需要生成Class的數(shù)據(jù)容器,這會(huì)消耗更多內(nèi)存。
  • 抽象代碼也會(huì)帶來(lái)更多的內(nèi)存消耗。如果你的“抽象”設(shè)計(jì)實(shí)際上并沒(méi)有帶來(lái)多大好處,那么就不要使用它。
  • 使用nano protobufs 來(lái)序列化數(shù)據(jù)。Google設(shè)計(jì)的一個(gè)語(yǔ)言和平臺(tái)中立打的序列化協(xié)議,比XML更快、更小、更簡(jiǎn)單。
  • 避免使用依賴注入的框架。依賴注入的框架需要開(kāi)啟額外的服務(wù),來(lái)掃描App中代碼的Annotation,所以需要額外的系統(tǒng)資源。
  • 使用ZIP對(duì)齊的APK。對(duì)APK做Zip對(duì)齊,會(huì)壓縮其內(nèi)部的資源,運(yùn)行時(shí)會(huì)占用更少的內(nèi)存。
  • 合理使用多進(jìn)程。

Android內(nèi)存泄漏分析及優(yōu)化

內(nèi)存泄漏的根本原因
內(nèi)存泄漏分析1.png

如上圖所示,GC會(huì)選擇一些它了解還存活的對(duì)象作為內(nèi)存遍歷的根節(jié)點(diǎn)(GC Roots),比方說(shuō)thread stack中的變量,JNI中的全局變量,zygote中的對(duì)象(class loader加載)等,然后開(kāi)始對(duì)heap進(jìn)行遍歷。到最后,部分沒(méi)有直接或者間接引用到GC Roots的就是需要回收的垃圾,會(huì)被GC回收掉。如下圖藍(lán)色部分。

內(nèi)存泄漏分析2.png

內(nèi)存泄漏指的是進(jìn)程中某些對(duì)象(垃圾對(duì)象)已經(jīng)沒(méi)有使用價(jià)值了,但是它們卻可以直接或間接地引用到gc roots導(dǎo)致無(wú)法被GC回收。無(wú)用的對(duì)象占據(jù)著內(nèi)存空間,使得實(shí)際可使用內(nèi)存變小,形象地說(shuō)法就是內(nèi)存泄漏了。下面分析一些可能導(dǎo)致內(nèi)存泄漏的情景。

Android中常見(jiàn)的內(nèi)存泄漏原因

1.使用static變量引起的內(nèi)存泄漏

因?yàn)閟tatic變量的生命周期是在類加載時(shí)開(kāi)始 類卸載時(shí)結(jié)束,也就是說(shuō)static變量是在程序進(jìn)程死亡時(shí)才釋放,如果在static變量中引用了Activity那么這個(gè)Activity由于被引用,便會(huì)隨static變量的生命周期一樣,一直無(wú)法被釋放,造成內(nèi)存泄漏。

一般解決辦法:
想要避免context相關(guān)的內(nèi)存泄漏,需要注意以下幾點(diǎn):

  • 不要對(duì)activity的context長(zhǎng)期引用(一個(gè)activity的引用的生存周期應(yīng)該和activity的生命周期相同)
  • 如果可以的話,盡量使用關(guān)于application的context來(lái)替代和activity相關(guān)的context
  • 如果一個(gè)acitivity的非靜態(tài)內(nèi)部類的生命周期不受控制,那么避免使用它;正確的方法是使用一個(gè)靜態(tài)的內(nèi)部類,并且對(duì)它的外部類有一WeakReference,就像在ViewRootImpl中內(nèi)部類W所做的那樣,使用弱引用private final WeakReference<ViewRootImpl> mViewAncestor;。

下面的代碼存在內(nèi)存泄漏的問(wèn)題,非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例導(dǎo)致內(nèi)存泄漏。

/**
mDemo會(huì)獲得并一直持有MemoryLeakActivity的引用。當(dāng)MemoryLeakActivity銷毀后重建,因?yàn)閙Demo持有引用,無(wú)法被GC回收的,進(jìn)程中會(huì)存在2個(gè)MemoryLeakActivity實(shí)例。所以,對(duì)于lauchMode不是singleInstance的Activity, 應(yīng)該避免在activity里面實(shí)例化其非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例。
*/
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("啟動(dòng)一個(gè)持有本對(duì)象的線程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mDemo = new Demo();
        mDemo.run();
    }
    class Demo{
        void run(){
            Log.i(TAG, "run: ");
        }
    }
}

解決方法:將Demo改成靜態(tài)內(nèi)部類

因?yàn)槠胀ǖ膬?nèi)部類對(duì)象隱含地保存了一個(gè)引用,指向創(chuàng)建它的外圍類對(duì)象。然而,當(dāng)內(nèi)部類是static的時(shí),就不是這樣了。嵌套類意味著: 1. 嵌套類的對(duì)象,并不需要其外圍類的對(duì)象。 2. 不能從嵌套類的對(duì)象中訪問(wèn)非靜態(tài)的外圍類對(duì)象。

2.線程引起的內(nèi)存泄漏

下面的代碼存在內(nèi)存泄漏的問(wèn)題,啟動(dòng)線程的匿名內(nèi)部類會(huì)持有MemoryLeakActivity.this的引用。如果線程還沒(méi)有結(jié)束,Activity已經(jīng)銷毀那就會(huì)造成內(nèi)存泄漏。

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("啟動(dòng)一個(gè)持有本對(duì)象的線程");
        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.合理安排線程執(zhí)行的時(shí)間,控制線程在Activity結(jié)束前結(jié)束。
2.將內(nèi)部類改為靜態(tài)內(nèi)部類,并使用弱引用WeakReference來(lái)保存Activity實(shí)例 因?yàn)槿跻?只要GC發(fā)現(xiàn)了 就會(huì)回收它 ,因此可盡快回收。

將匿名內(nèi)部類改成靜態(tài)類,避免了Activity context的內(nèi)存泄漏問(wèn)題

/**
 * 示例通過(guò)將線程類聲明為私有的靜態(tài)內(nèi)部類避免了 Activity context 的內(nèi)存泄漏問(wèn)題,但
 * 在配置發(fā)生改變后,線程仍然會(huì)執(zhí)行。原因在于,DVM 虛擬機(jī)持有所有運(yùn)行線程的引用,無(wú)論
 * 這些線程是否被回收,都與 Activity 的生命周期無(wú)關(guān)。運(yùn)行中的線程只會(huì)繼續(xù)運(yùn)行,直到
 * Android 系統(tǒng)將整個(gè)應(yīng)用進(jìn)程殺死
*/
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("啟動(dòng)一個(gè)持有本對(duì)象的線程");
        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中結(jié)束線程運(yùn)行

/**
* 除了我們需要實(shí)現(xiàn)銷毀邏輯以保證線程不會(huì)發(fā)生內(nèi)存泄漏。在退出當(dāng)前
* Activity 前使用 onDestroy() 方法結(jié)束你的運(yùn)行中線程。
*/
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("啟動(dòng)一個(gè)持有本對(duì)象的線程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        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的使用造成的內(nèi)存泄漏

由于在Handler的使用中,handler會(huì)發(fā)送message對(duì)象到 MessageQueue中 然后 Looper會(huì)輪詢MessageQueue 然后取出Message執(zhí)行,但是如果一個(gè)Message長(zhǎng)時(shí)間沒(méi)被取出執(zhí)行,那么由于 Message中有 Handler的引用,而 Handler 一般來(lái)說(shuō)也是內(nèi)部類對(duì)象,Message引用 Handler ,Handler引用 Activity 這樣 使得 Activity無(wú)法回收?;蛘哒f(shuō)Handler在Activity退出時(shí)依然還有消息需要處理,那么這個(gè)Activity就不會(huì)被回收。

解決辦法:
依舊使用 靜態(tài)內(nèi)部類+弱引用的方式 可解決
例如下面的代碼

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("啟動(dòng)一個(gè)持有本對(duì)象的線程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mHandler = new MyHandler(this);
        mHandler.sendEmptyMessage(0);
    }

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

    //第一步,將Handler改成靜態(tài)內(nèi)部類。
    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.資源未被及時(shí)關(guān)閉造成的內(nèi)存泄漏

比如一些Cursor 沒(méi)有及時(shí)close 會(huì)保存有Activity的引用,導(dǎo)致內(nèi)存泄漏

解決辦法:
在onDestory方法中及時(shí) close即可

5.BitMap占用過(guò)多內(nèi)存

bitmap的解析需要占用內(nèi)存,但是內(nèi)存只提供8M的空間給BitMap,如果圖片過(guò)多,并且沒(méi)有及時(shí) recycle bitmap 那么就會(huì)造成內(nèi)存溢出。

解決辦法:
及時(shí)recycle 壓縮圖片之后加載圖片

其中還有一些關(guān)于 集合對(duì)象沒(méi)移除,注冊(cè)的對(duì)象沒(méi)反注冊(cè),代碼壓力的問(wèn)題也可能產(chǎn)生內(nèi)存泄漏,但是使用上述的幾種解決辦法一般都是可以解決的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容