Android中常見(jiàn)的內(nèi)存泄漏和解決方案

什么是內(nèi)存泄漏?

簡(jiǎn)單點(diǎn)說(shuō),就是指一個(gè)對(duì)象不再使用,本應(yīng)該被回收,但由于某些原因?qū)е聦?duì)象無(wú)法回收,仍然占用著內(nèi)存,這就是內(nèi)存泄漏。

為什么會(huì)產(chǎn)生內(nèi)存泄漏,內(nèi)存泄漏會(huì)導(dǎo)致什么問(wèn)題?

相比C++需要手動(dòng)去管理對(duì)象的創(chuàng)建和回收,Java有著自己的一套垃圾回收機(jī)制,它能夠自動(dòng)回收內(nèi)存,但是它往往會(huì)因?yàn)槟承┰蚨兊谩安豢孔V”。

在Android開發(fā)中,一些不好的編碼習(xí)慣就很可能會(huì)導(dǎo)致內(nèi)存泄漏,而這些內(nèi)存泄漏會(huì)導(dǎo)致應(yīng)用內(nèi)存越占越大,使得應(yīng)用變得卡頓,甚至造成OOM(Out Of Memory)內(nèi)存溢出問(wèn)題,同時(shí)也使應(yīng)用變得極其不穩(wěn)定,因?yàn)楫?dāng)內(nèi)存不足的時(shí)候,系統(tǒng)會(huì)優(yōu)先回收那些“內(nèi)存占比”大的應(yīng)用。

Java的內(nèi)存分配機(jī)制

首先我們先來(lái)了解下Java的內(nèi)存分配機(jī)制,Java 程序運(yùn)行時(shí)的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)。

靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好,并且在程序整個(gè)運(yùn)行期間都存在。

棧區(qū) :當(dāng)方法被執(zhí)行時(shí),方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型、對(duì)象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放。因?yàn)闂?nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

堆區(qū) : 又稱動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接 new 出來(lái)的內(nèi)存,也就是對(duì)象的實(shí)例。這部分內(nèi)存在不使用時(shí)將會(huì)由 Java 垃圾回收器來(lái)負(fù)責(zé)回收。

那什么樣的對(duì)象會(huì)被回收呢?

Java內(nèi)存管理有向圖

為了更好理解 GC 的工作原理,我們可以將對(duì)象考慮為有向圖的頂點(diǎn),將引用關(guān)系考慮為圖的有向邊,有向邊從引用者指向被引對(duì)象。另外,每個(gè)線程對(duì)象可以作為一個(gè)圖的起始頂點(diǎn),例如大多程序從 main 進(jìn)程開始執(zhí)行,那么該圖就是以 main 進(jìn)程頂點(diǎn)開始的一棵根樹。在這個(gè)有向圖中,根頂點(diǎn)可達(dá)的對(duì)象都是有效對(duì)象,GC將不回收這些對(duì)象。如果某個(gè)對(duì)象 (連通子圖)與這個(gè)根頂點(diǎn)不可達(dá)(注意,該圖為有向圖),那么我們認(rèn)為這個(gè)(這些)對(duì)象不再被引用,可以被 GC 回收。

常見(jiàn)的內(nèi)存泄漏和解決方案

1、單例引起的內(nèi)存泄漏
由于單例的靜態(tài)特性導(dǎo)致它的生命周期和整個(gè)應(yīng)用的生命周期一樣長(zhǎng),如果有對(duì)象已經(jīng)不再使用了,但又卻被單例持有引用,那么就會(huì)導(dǎo)致這個(gè)對(duì)象就沒(méi)辦法被回收,從而導(dǎo)致內(nèi)存泄漏。

// 使用了單例模式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

問(wèn)題所在:
從上面的代碼我們可以看出,在創(chuàng)建單例對(duì)象的時(shí)候,引入了一個(gè)Context上下文對(duì)象,如果我們把Activity注入進(jìn)來(lái),會(huì)導(dǎo)致這個(gè)Activity一直被單例對(duì)象持有引用,當(dāng)這個(gè)Activity銷毀的時(shí)候,對(duì)象也是沒(méi)有辦法被回收的。

解決方案:
在這里我們只需要讓這個(gè)上下文對(duì)象指向應(yīng)用的上下文即可(this.context=context.getApplicationContext()),因?yàn)閼?yīng)用的上下文對(duì)象的生命周期和整個(gè)應(yīng)用一樣長(zhǎng)。

2、非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例引起的內(nèi)存泄漏
由于非靜態(tài)內(nèi)部類會(huì)默認(rèn)持有外部類的引用,如果我們?cè)谕獠款愔腥?chuàng)建這個(gè)內(nèi)部類對(duì)象,當(dāng)頻繁打開關(guān)閉Activity,會(huì)導(dǎo)致重復(fù)創(chuàng)建對(duì)象,造成資源的浪費(fèi),為了避免這個(gè)問(wèn)題我們一般會(huì)把這個(gè)實(shí)例設(shè)置為靜態(tài),這樣雖然解決了重復(fù)創(chuàng)建實(shí)例,但是會(huì)引發(fā)出另一個(gè)問(wèn)題,就是靜態(tài)成員變量它的生命周期是和應(yīng)用的生命周期一樣長(zhǎng)的,然而這個(gè)靜態(tài)成員變量又持有該Activity的引用,所以導(dǎo)致這個(gè)Activity銷毀的時(shí)候,對(duì)象也是無(wú)法被回收的。

public class MainActivity extends AppCompatActivity {

    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
   
    class TestResource {
    //...
    }
}

問(wèn)題所在:
其實(shí)這個(gè)和上面單例對(duì)象的內(nèi)容泄漏問(wèn)題是一樣的,由于靜態(tài)對(duì)象持有Activity的引用,導(dǎo)致Activity沒(méi)辦法被回收。

解決方案:
在這里我們只需要把非靜態(tài)內(nèi)部類改成靜態(tài)內(nèi)部類即可(static class TestResource)。

3、Handler引起的內(nèi)存泄漏
記得我們剛學(xué)習(xí)Handler的時(shí)候,網(wǎng)上資料甚至學(xué)校教材“教科書”式的寫法都是這樣的

    Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //to do something..
            switch (msg.what){
                case 0:
                    //to do something..
                    break;
            }    
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
            @Override
            public void run() {
                //to do something..
                mHandler.sendEmptyMessage(0);
            }
        }).start();
    }

問(wèn)題所在:
別看上面短短幾行代碼,其實(shí)涉及到了很多問(wèn)題,首先我們知道程序啟動(dòng)時(shí)在主線程中會(huì)創(chuàng)建一個(gè)Looper對(duì)象,這個(gè)Looper里維護(hù)著一個(gè)MessageQueue消息隊(duì)列,這個(gè)消息隊(duì)列里會(huì)按時(shí)間順序存放著Message,不清楚的朋友可以看下我之前寫的這篇文章《從源碼的角度徹底理解Android的消息處理機(jī)制》,然后上面的Handler是通過(guò)內(nèi)部類來(lái)創(chuàng)建的,內(nèi)部類會(huì)持有外部類的引用,也就是Handler持有Activity的引用,而消息隊(duì)列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是說(shuō)當(dāng)消息隊(duì)列中的消息如果還沒(méi)有處理完,這些未處理的消息(也可以理解成延遲操作)是持有Activity的引用的,此時(shí)如果關(guān)閉Activity,是沒(méi)辦法回收的,從而就會(huì)導(dǎo)致內(nèi)存泄露。

解決方案:
和上文一樣,我們需要先把非靜態(tài)內(nèi)部類改成靜態(tài)內(nèi)部類(如果是Runnable類也需要改成靜態(tài)),然后在Activity的onDestroy中移除對(duì)應(yīng)的消息,再來(lái)需要在Handler內(nèi)部用弱引用持有Activity,因?yàn)樽寖?nèi)部類不再持有外部類的引用時(shí),程序也就不允許Handler操作Activity對(duì)象了。

   MyHandler myHandler = new MyHandler(this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

         new Thread(new Runnable() {
            @Override
            public void run() {
                myHandler.sendMessage(Message.obtain());
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //移除對(duì)應(yīng)的Runnable或者是Message
        //mHandler.removeCallbacks(runnable);
        //mHandler.removeMessages(what);
        mHandler.removeCallbacksAndMessages(null);
    }

    private static class MyHandler extends Handler {

        private WeakReference<Activity> mActivity;

        public MyHandler(Activity activity) {
            mActivity = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if (mActivity.get() == null) {
                return;
            }
             //to do something..

        }
    };

4、WebView引起的內(nèi)存泄露
關(guān)于WebView的內(nèi)存泄漏,這是個(gè)絕對(duì)的大大大大大坑!不同版本都存在著不同版本的問(wèn)題,這里我只能給出我平時(shí)的處理方法,可能不同機(jī)型上存在的差異,只能靠積累了。
方法一:
首先不要在xml去定義<WebView/>,定義一個(gè)ViewGroup就行,然后動(dòng)態(tài)在代碼中new WebView(Context context)(傳入的Context采取弱引用),再通過(guò)addView添加到ViewGroup中,最后在頁(yè)面銷毀執(zhí)行onDestroy()的時(shí)候把WebView移除。
方法二:
簡(jiǎn)單粗暴,直接為WebView新開辟一個(gè)進(jìn)程,在結(jié)束操作的時(shí)候直接System.exit(0)結(jié)束掉進(jìn)程,這里需要注意進(jìn)程間的通訊,可以采取Aidl,Messager,Content Provider,Broadcast等方式。

5、Asynctask引起的內(nèi)存泄露
這部分和Handler比較像,其實(shí)也是因?yàn)閮?nèi)部類持有外部類引用,一樣的改成靜態(tài)內(nèi)部類,然后在onDestory方法中取消任務(wù)即可。

6、資源對(duì)象未關(guān)閉引起的內(nèi)存泄露
這塊就比較簡(jiǎn)單了,比如我們經(jīng)常使用的廣播接收者,數(shù)據(jù)庫(kù)的游標(biāo),多媒體,文檔,套接字等。

7、其他一些
還有一些需要注意的,比如注冊(cè)了EventBus沒(méi)注銷,添加Activity到棧中,銷毀的時(shí)候沒(méi)移除等。

好了,以上就是比較常見(jiàn)的內(nèi)存泄露原因和對(duì)應(yīng)的解決方案,當(dāng)然還有一些其他的,這里沒(méi)有辦法一一闡述,還是需要大家平時(shí)不斷去積累,總結(jié),這里提供一個(gè)可以檢查內(nèi)存泄露的工具LeakCanary,只需要幾行代碼就可以輕松在應(yīng)用內(nèi)集成內(nèi)存監(jiān)控功能了。

最后編輯于
?著作權(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ù)。

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