Android 內存泄漏分析總結

GitHub地址

https://github.com/realxz/MemoryLeak
GitHub 代碼只包含泄漏情況,不包括修改后的代碼,大家可以下載下來后,自行修改。

什么是內存

Android 系統我們的 APP 分配的內存大小是有限的,我現在用的手機小米 4c 為我自己開發的應用 分配的256MB的內存大小,不同的手機型號,不同的 ROM 分配的內存大小不一定一樣,這里面所提 到的內存一般是指 Android 手機的 RAM。

RAM 包含寄存器,棧,堆,靜態存儲區域,常量池。通常我們所說的 Android 內存泄漏中的內存, 指的是其中的堆內存。一般來來說,我們 new 出來的對 象都會存儲在堆內存中,這部分的內存由 GC 進行回收管理。

GC 是什么,它如何進行內存管理

GC 指垃圾回收器 「Garbage Collection」。Java 使用 GC 進行內存回收理,不用我們手動釋放內 存,提升了我們的開發效率。那GC回收對象的依據是什么呢 ?簡單的說,對于一個對象,若果不存 在從 GC 根節點到該對象的引用鏈 (從根節點不可到達的 (從根節點不可到達的),那么對于 GC 來說這個對象就是需要被回收的,反之該對象是從根節點可到達的,那么這個對象就不會被 GC 回 收。

根節點:在 Java 中可以作為根節點的對象有很多,這塊內容我理解的不是很到位。我很簡單的把它理解為 Android 應用的主線程,存活的子線程,棧中的對象以及靜態屬性引用的對象。
注意:這里的引用是指強引用,在 Java 當中存在4種引用類型分別是「強引用」、「軟引用」、「弱引用」、「虛引用」。如果沒有特別指定,我們所說的引用都是指強引用,GC 不會回收具有 強引用的對象。

什么是內存泄漏

我們已經知道了,如果某個對象,從根節點可到達,也就是存在從根節點到該對象的引用鏈,那么該對象是不會被 GC 回收的。如果說這個對象已經不會再被使用到了,是無用的,我們依然持有他的引用的話,就會造成內存泄漏,例如 一個長期在后臺運行的線程持有 Activity 的引用,這個時 候 Activity 執行了 onDestroy 方法,那么這個 Activity 就是從根節點可到達并且無用的對象, 這個 Activity 對象就是泄漏的對象,給這個對象分配的內存將無法被回收。

內存泄漏的影響

  • 內存很寶貴,即使從效率,責任的角度上,我們也應該降低內存的使用,減少內存的浪費。
  • 內存泄漏導致可用內存越來越少,最終導致OOM。
  • 可用內存減少,GC 被觸發,雖然 GC 可以幫助我們回收無用內存,但是頻繁的觸發 GC 也會影響性能,可能造成程序卡頓。

如何查找、定位內存泄漏

  • MAT「Memory AnalysisTools」,網上有很多的使用教程,個人感覺使用較繁瑣,需要耐心分析和一定的經驗才能定位出內存泄漏。
  • LeakCanary,Square 公司開源作品,使用方便,可以直接定位到泄漏的對象,并且給出調用鏈。

內存泄漏事例

非靜態內部類

package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class InnerThreadActivity extends AppCompatActivity {

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

        RunningThread runningThread = new RunningThread();
        runningThread.start();
    }

    class RunningThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000*5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void closeActivity(View view) {
        this.finish();
    }

}

當我們運行這段代碼的時候,LeakCanary 會幫我們檢測出來內存泄漏,如圖:

![image_1bbluquud1ndr12k41mdnpbmiv9.png-59.9kB][1]

在 Java 中,內部類會隱式的持有外部類的引用。我們可以很清楚的看見 RunningThread 對象持有 了 InnerThreadActivity 的引用,由于 RunningThread 線程會一直運行下去,我 finish 掉當前 的 Activity 就會導致 InnerThreadActivity 實例發生泄漏。我們可以采用靜態內部類的方式來解 除這種內存泄漏的隱患,代碼如下:

private static class RunningThread extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意:盡量使用靜態內部類來替代內部類,同時避免讓長期運行的任務( 線程 )持有 Activity的引用。

匿名內部類

package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class AnonymousThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_anonymous_thread);
        Thread anonymousThread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    //do something
                    try {
                        Thread.sleep(60 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return;
                }
            }
        };
        anonymousThread.start();
    }

    public void closeActivity(View view) {
        this.finish();
    }
}

LeakCanary 同樣會檢測出內存泄漏,如圖:

![image_1bbluvkf51um53c04fucmr1g8cm.png-69.7kB][2]

在 Java 中,匿名內部類和非靜態內部類一樣,都會持有外部類的引用。上面的代碼正式由于 Thread 的匿名類持有了 AnonymousThreadActivity 的引用,并且匿名類的運行時間長達 1 分鐘, 在這段時間內,我 finish 掉了 Activity 導致了內存泄漏,解決方式和非靜態內部類的方法一樣,使用靜態內部類來代替匿名內部類,這里就不貼代碼了。

Handler 內存泄漏

package com.example.xiezhen.memoryleak;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class HandlerActivity extends AppCompatActivity {
    private TextView tvShowMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
        MemoryLeakHandler handler = new MemoryLeakHandler();
        handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
    }

    class MemoryLeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            tvShowMessage.setText("MemoryLeak");
            Toast.makeText(HandlerActivity.this, "memory leak", Toast.LENGTH_SHORT).show();
        }
    }

    public void closeActivity(View view) {
        this.finish();
    }
}

![image_1bblv16hj148t1bvmjge13jd5if13.png-79.1kB][3]

LeakCanary 為我們展示了內存泄漏的引用鏈,這段代碼泄漏的原因也是因為非靜態內部類持有了外部類的引用。圖中的引用鏈涉及到 Android 中的消息機制 「Handler」、「MessageQueue」、 「Looper」。大致敘述一下,我們的 MemoryLeakHandler 因為內部類的關系會持有 HandlerActivity 實例的引用,我們使用 Handler 來發送消息,這個Handler 會被消息中 target 屬性引用,這個 Message 會在我們主線程的消息隊 列中存活 10 秒鐘,在這段時間內,我 finish 掉當前 Activity 就會造成內存泄漏,并且依然會彈出 Toast 盡管我們已經開不見這個 Activity了。

解決方案依然是采用靜態內部類來替代非靜態內部類,并且使用 WeakReference 來引用 Activity,如果對象只存在弱引用的話,GC 是會回收這部分內存的。

package com.example.xiezhen.memoryleak;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.lang.ref.WeakReference;

public class HandlerActivity extends AppCompatActivity {
    private TextView tvShowMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
        MemoryLeakHandler handler = new MemoryLeakHandler(this);
        handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
    }

    private static class MemoryLeakHandler extends Handler {
        private WeakReference<HandlerActivity> weakReference;

        public MemoryLeakHandler(HandlerActivity activity) {
            this.weakReference = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity activity = weakReference.get();
            if (activity != null) {
                activity.tvShowMessage.setText("MemoryLeak");
                Toast.makeText(activity, "memory leak", Toast.LENGTH_SHORT).show();
            }
        }
    }


    public void closeActivity(View view) {
        this.finish();
    }
}

單例/靜態引用

static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}
package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

public class EventBusActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_bus);
        EventBus.getDefault().register(this);
        EventBusThread thread=new EventBusThread();
        thread.start();

    }

    public void closeActivity(View view) {
        this.finish();
    }

    private static class EventBusThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            EventBus.getDefault().post("eventbus");
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void receiveMessage(String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }

}

EventBus 我相信大家都不陌生,我們一般在使用 EventBus 的時候,都會使用EventBus.getDefault( ) 方法來獲取一個 EventBus 單例,這個單例是靜態的,全局可訪問的。上面的代碼我們在獲取 EventBus 單例后調用 register 方法,將 EventBusActivity 注冊到 EventBus 中,這個時候 EventBus 就會持有 Activity 的引用,由于單例是靜態的,生命周期和整個 App 生命周期 一致,如果我們不調用 unRegister 方法的話,EventBusActivity 實例就會泄漏。上述代碼中,我特意沒有去調用 unRegister 方法,我們來看看 LeakCanary 的結果:

![image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png-88kB][4]

  • 不要讓我們的對象被靜態屬性所引用,這很容易造成內存泄漏。
  • 一般來說我們在使用注冊方法的時候,library 都會提供相對應的解除注冊方法,不要忘了調用!

Activity Context & Application Context

package com.example.xiezhen.memoryleak;

import android.content.Context;
import android.content.pm.PackageInfo;

/**
* Created by xiezhen on 2017/3/16.
*/

public class CommonHelper {
    private Context context;
    private static CommonHelper commonHelper = null;

    private CommonHelper(Context context) {
        this.context = context;
    }

    public static CommonHelper getCommonHelper(Context context) {
        if (commonHelper == null) {
            commonHelper = new CommonHelper(context);
        }
        return commonHelper;
    }

    public int getVersionCode() {
        PackageInfo packInfo = getPackageInfo(context);
        if (packInfo != null) {
            return packInfo.versionCode;
        } else {
            return -1;
        }
    }

    public  String getVersionName() {
        PackageInfo packInfo = getPackageInfo(context);
        if (packInfo != null) {
            return packInfo.versionName;
        } else {
            return "";
        }
    }


    private PackageInfo getPackageInfo(Context context) {
        try {
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        } catch (Exception e) {
            return null;
        }
    }


}
package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class ContextActivity extends AppCompatActivity {

    private TextView tvVersionName;
    private TextView tvVersionCode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_context);
        tvVersionName = (TextView) findViewById(R.id.tv_version_name);
        tvVersionCode = (TextView) findViewById(R.id.tv_version_code);
    }

    @Override
    protected void onResume() {
        super.onResume();
        setVersionCode(CommonHelper.getCommonHelper(this).getVersionCode());
        setVersionName(CommonHelper.getCommonHelper(this).getVersionName());
    }

    private void setVersionName(String versionName) {
        tvVersionName.setText(versionName);
    }

    private void setVersionCode(int versionCode) {
        tvVersionCode.setText(String.valueOf(versionCode));
    }

    public void closeActivity(View view) {
        this.finish();
    }

}

我寫了一個工具類,來獲取 App 的 Version Code 和 Version Name,這段代碼同樣會導致內存泄漏,下面是 LeakCanary 的泄露圖。

![image_1bblvctrqieh39h9asvsm9p91t.png-60.1kB][5]

發現泄露了一個 Context 實例,在調用 CommonHelper 中的方法時候的時候,我們將 ContextActivity 作為一個 Context 對象傳遞了進去,Context 對象的引用被長期持有導致內存泄漏。處理這種泄漏的方法很簡單,使用 Application Context 來代替 Activity Context 即可,Application Context 在整個 App 生命周期內適用。

結論

一般來說,內存泄漏都是因為泄漏對象的引用被傳遞到該對象的范圍之外,或者說內存泄漏是因為持有對象的長期引用,導致對象無法被 GC 回收。為了避免這種情況,我們可以選擇在對象生命周期結束的時候,解除綁定,將引用置為空,或者使用弱引用。

  1. 由于 Context 導致內存泄漏。使用 Application Context 代替 Activity Context,避免長期持有 Context 的引用,引用應該和 Context 自身的生命周期保持一致。
  2. 由于非靜態內部類、匿名內部類導致內存泄。它們會隱式的持有外部類的引用,一不小心長期持有該引用就會導致內存泄漏,使用靜態內部類來代替它們。
  3. Handler 導致內存泄漏。原因和第二點一樣,同樣使用靜態內部類的實現方式,同時對需要引用的對象/資源采用弱引用的方式。
  4. EventBus導致內存泄漏。EventBus 的單例特性,會長期持有注冊對象的引用,一定要在對象生命周期結束的時候,接觸注冊,釋放引用。同樣對于系統提供的一些成對出現的方法,我們也需要成對的調用,例如 BroadcastReceiver 的 registerReceiver( ) 方法和 unRegisterReceiver( ) 方法。
  5. 線程導致內存泄漏。我們經常會執行一些長期運行的任務,避免在這些任務中持有 Activity 對象的引用,如果持有了引用的話,我們應該在對象生命周期結束的時候,釋放引用。

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,648評論 0 8
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,383評論 0 12
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,232評論 2 7
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 798評論 0 5
  • 每日打卡。 來評論區記錄下今天的收獲和成長吧!
    樹洞君閱讀 733評論 20 1