Android中常見的內存泄漏和解決方案

什么是內存泄漏?

簡單點說囤锉,就是指一個對象不再使用坦弟,本應該被回收,但由于某些原因導致對象無法回收官地,仍然占用著內存酿傍,這就是內存泄漏。

為什么會產生內存泄漏驱入,內存泄漏會導致什么問題赤炒?

相比C++需要手動去管理對象的創(chuàng)建和回收,Java有著自己的一套垃圾回收機制,它能夠自動回收內存,但是它往往會因為某些原因而變得“不靠譜”割卖。

在Android開發(fā)中,一些不好的編碼習慣就很可能會導致內存泄漏癣朗,而這些內存泄漏會導致應用內存越占越大,使得應用變得卡頓旺罢,甚至造成OOM(Out Of Memory)內存溢出問題旷余,同時也使應用變得極其不穩(wěn)定,因為當內存不足的時候扁达,系統(tǒng)會優(yōu)先回收那些“內存占比”大的應用正卧。

Java的內存分配機制

首先我們先來了解下Java的內存分配機制,Java 程序運行時的內存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配跪解,對應的炉旷,三種存儲策略使用的內存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)叉讥。

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

棧區(qū) :當方法被執(zhí)行時,方法體內的局部變量(其中包括基礎數(shù)據(jù)類型救崔、對象的引用)都在棧上創(chuàng)建惶看,并在方法執(zhí)行結束時這些局部變量所持有的內存將會自動被釋放捏顺。因為棧內存分配運算內置于處理器的指令集中,效率很高纬黎,但是分配的內存容量有限幅骄。

堆區(qū) : 又稱動態(tài)內存分配,通常就是指在程序運行時直接 new 出來的內存本今,也就是對象的實例拆座。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。

那什么樣的對象會被回收呢诈泼?

Java內存管理有向圖

為了更好理解 GC 的工作原理懂拾,我們可以將對象考慮為有向圖的頂點煤禽,將引用關系考慮為圖的有向邊铐达,有向邊從引用者指向被引對象。另外檬果,每個線程對象可以作為一個圖的起始頂點瓮孙,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹选脊。在這個有向圖中杭抠,根頂點可達的對象都是有效對象,GC將不回收這些對象恳啥。如果某個對象 (連通子圖)與這個根頂點不可達(注意偏灿,該圖為有向圖),那么我們認為這個(這些)對象不再被引用钝的,可以被 GC 回收翁垂。

常見的內存泄漏和解決方案

1、單例引起的內存泄漏
由于單例的靜態(tài)特性導致它的生命周期和整個應用的生命周期一樣長硝桩,如果有對象已經(jīng)不再使用了沿猜,但又卻被單例持有引用,那么就會導致這個對象就沒辦法被回收碗脊,從而導致內存泄漏啼肩。

// 使用了單例模式
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;
    }
}

問題所在:
從上面的代碼我們可以看出,在創(chuàng)建單例對象的時候衙伶,引入了一個Context上下文對象祈坠,如果我們把Activity注入進來,會導致這個Activity一直被單例對象持有引用矢劲,當這個Activity銷毀的時候赦拘,對象也是沒有辦法被回收的。

解決方案:
在這里我們只需要讓這個上下文對象指向應用的上下文即可(this.context=context.getApplicationContext())卧须,因為應用的上下文對象的生命周期和整個應用一樣長另绩。

2儒陨、非靜態(tài)內部類創(chuàng)建靜態(tài)實例引起的內存泄漏
由于非靜態(tài)內部類會默認持有外部類的引用,如果我們在外部類中去創(chuàng)建這個內部類對象笋籽,當頻繁打開關閉Activity蹦漠,會導致重復創(chuàng)建對象,造成資源的浪費车海,為了避免這個問題我們一般會把這個實例設置為靜態(tài)笛园,這樣雖然解決了重復創(chuàng)建實例,但是會引發(fā)出另一個問題侍芝,就是靜態(tài)成員變量它的生命周期是和應用的生命周期一樣長的研铆,然而這個靜態(tài)成員變量又持有該Activity的引用,所以導致這個Activity銷毀的時候州叠,對象也是無法被回收的棵红。

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 {
    //...
    }
}

問題所在:
其實這個和上面單例對象的內容泄漏問題是一樣的,由于靜態(tài)對象持有Activity的引用咧栗,導致Activity沒辦法被回收逆甜。

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

3致板、Handler引起的內存泄漏
記得我們剛學習Handler的時候交煞,網(wǎng)上資料甚至學校教材“教科書”式的寫法都是這樣的

    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();
    }

問題所在:
別看上面短短幾行代碼,其實涉及到了很多問題斟或,首先我們知道程序啟動時在主線程中會創(chuàng)建一個Looper對象素征,這個Looper里維護著一個MessageQueue消息隊列,這個消息隊列里會按時間順序存放著Message萝挤,不清楚的朋友可以看下我之前寫的這篇文章《從源碼的角度徹底理解Android的消息處理機制》御毅,然后上面的Handler是通過內部類來創(chuàng)建的,內部類會持有外部類的引用平斩,也就是Handler持有Activity的引用亚享,而消息隊列中的消息target是指向Handler的,也就等同消息持有Handler的引用绘面,也就是說當消息隊列中的消息如果還沒有處理完欺税,這些未處理的消息(也可以理解成延遲操作)是持有Activity的引用的,此時如果關閉Activity揭璃,是沒辦法回收的晚凿,從而就會導致內存泄露。

解決方案:
和上文一樣瘦馍,我們需要先把非靜態(tài)內部類改成靜態(tài)內部類(如果是Runnable類也需要改成靜態(tài))歼秽,然后在Activity的onDestroy中移除對應的消息,再來需要在Handler內部用弱引用持有Activity情组,因為讓內部類不再持有外部類的引用時燥筷,程序也就不允許Handler操作Activity對象了箩祥。

   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();
        //移除對應的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引起的內存泄露
關于WebView的內存泄漏肆氓,這是個絕對的大大大大大坑袍祖!不同版本都存在著不同版本的問題,這里我只能給出我平時的處理方法谢揪,可能不同機型上存在的差異蕉陋,只能靠積累了。
方法一:
首先不要在xml去定義<WebView/>拨扶,定義一個ViewGroup就行凳鬓,然后動態(tài)在代碼中new WebView(Context context)(傳入的Context采取弱引用),再通過addView添加到ViewGroup中患民,最后在頁面銷毀執(zhí)行onDestroy()的時候把WebView移除缩举。
方法二:
簡單粗暴,直接為WebView新開辟一個進程酒奶,在結束操作的時候直接System.exit(0)結束掉進程蚁孔,這里需要注意進程間的通訊,可以采取Aidl惋嚎,Messager,Content Provider站刑,Broadcast等方式另伍。

5、Asynctask引起的內存泄露
這部分和Handler比較像绞旅,其實也是因為內部類持有外部類引用摆尝,一樣的改成靜態(tài)內部類,然后在onDestory方法中取消任務即可因悲。

6堕汞、資源對象未關閉引起的內存泄露
這塊就比較簡單了,比如我們經(jīng)常使用的廣播接收者晃琳,數(shù)據(jù)庫的游標讯检,多媒體,文檔卫旱,套接字等人灼。

7、其他一些
還有一些需要注意的顾翼,比如注冊了EventBus沒注銷投放,添加Activity到棧中,銷毀的時候沒移除等适贸。

好了灸芳,以上就是比較常見的內存泄露原因和對應的解決方案涝桅,當然還有一些其他的,這里沒有辦法一一闡述烙样,還是需要大家平時不斷去積累苹支,總結,這里提供一個可以檢查內存泄露的工具LeakCanary误阻,只需要幾行代碼就可以輕松在應用內集成內存監(jiān)控功能了债蜜。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市究反,隨后出現(xiàn)的幾起案子寻定,更是在濱河造成了極大的恐慌,老刑警劉巖精耐,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狼速,死亡現(xiàn)場離奇詭異,居然都是意外死亡卦停,警方通過查閱死者的電腦和手機向胡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惊完,“玉大人僵芹,你說我怎么就攤上這事⌒』保” “怎么了拇派?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凿跳。 經(jīng)常有香客問我件豌,道長,這世上最難降的妖魔是什么控嗜? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任茧彤,我火速辦了婚禮,結果婚禮上疆栏,老公的妹妹穿的比我還像新娘曾掂。我一直安慰自己,他們只是感情好承边,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布遭殉。 她就那樣靜靜地躺著,像睡著了一般博助。 火紅的嫁衣襯著肌膚如雪险污。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音蛔糯,去河邊找鬼拯腮。 笑死,一個胖子當著我的面吹牛蚁飒,可吹牛的內容都是我干的动壤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼淮逻,長吁一口氣:“原來是場噩夢啊……” “哼琼懊!你這毒婦竟也來了?” 一聲冷哼從身側響起爬早,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤哼丈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筛严,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體醉旦,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年桨啃,在試婚紗的時候發(fā)現(xiàn)自己被綠了车胡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡照瘾,死狀恐怖匈棘,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情网杆,我是刑警寧澤羹饰,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站碳却,受9級特大地震影響,放射性物質發(fā)生泄漏笑旺。R本人自食惡果不足惜昼浦,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筒主。 院中可真熱鬧关噪,春花似錦、人聲如沸乌妙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽藤韵。三九已至虐沥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欲险。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工镐依, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人天试。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓槐壳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親喜每。 傳聞我的和親對象是個殘疾皇子务唐,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348