Android的內(nèi)存泄露

對于開發(fā)老手扇调,這個問題想必已經(jīng)深入你的心;若是一名新手或者一直對內(nèi)存泄漏這個東西模模糊糊的工程師在讶,你的答案可能讓面試官并不滿意煞抬,這里將從底到上對內(nèi)存泄漏的原因、排查方法和一些經(jīng)驗為你做一次完整的解剖构哺。

處理內(nèi)存泄漏的問題是將軟件做到極致的一個必須的步驟此疹,尤其是那種將被用戶高強度使用的軟件。

案例:

public class PendingOrderManager {
    private static PendingOrderManager instance;
    private Context mContext;

    public PendingOrderManager(Context context) {    
        this.mContext = context;
    }

    public static PendingOrderManager getInstance(Context context) {
        if (instance == null) {
            instance = new PendingOrderManager(context);
        }
        return instance;
    }
   public void func(){
     ...
    }

...
}

然后讓你的某個Activity去使用這個PendingOrderManager單例遮婶,并且某個時候退出這個Activity:

//belong to some Activity
PendingOrderManager.getInstance(this).func();

...
finish()

這個時候內(nèi)存泄漏已經(jīng)發(fā)生:你退出了你的這個Activity本以為java的垃圾回收會將它釋放,但實際上Activity一直被PendingOrderManager持有著湖笨。Acitivity這個Context被長生命周期個體(單例一旦被創(chuàng)建就是整個app的生命周期)持有導(dǎo)致了這個Context發(fā)生了內(nèi)存泄漏旗扑。

這個例子和上面的例子是相通的,上面的C的例子因為忘記了手動執(zhí)行free一個10字節(jié)內(nèi)存導(dǎo)致內(nèi)存泄漏慈省。而下面這個例子是垃圾回收機制“故意忘記”了回收Context的內(nèi)存而導(dǎo)致了內(nèi)存泄漏臀防。下面兩節(jié)將對這個里面到底發(fā)生了什么進行說明。

靜態(tài)边败、堆和棧

編譯原理說軟件內(nèi)存分配的時候一般會放在三種位置:靜態(tài)存儲區(qū)域袱衷、堆和棧,他們的位置笑窜、功能致燥、速度都各不相同,區(qū)別如下:

  • 靜態(tài)存儲區(qū):內(nèi)存在程序編譯的時候就已經(jīng)分配好排截,這塊內(nèi)存在程序整個運行期間都存在嫌蚤。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量
  • 棧:就是CPU的寄存器(并不是內(nèi)存)断傲,特點是容量很小但是速度最快脱吱,函數(shù)或者方法的的方法體內(nèi)聲明的變量或者指向?qū)ο蟮囊谩⒕植孔兞考捶峙湓谶@里认罩,生命周期到該函數(shù)或者方法體尾部即止
  • 堆:就是動態(tài)內(nèi)存分配去(就是實體的內(nèi)存RAM)箱蝠,C中malloc和fee,java中的new和垃圾回收直接操作的就是這里的區(qū)域垦垂,類的成員變量分配在這里
    從上面即可看出靜態(tài)存儲區(qū)域是編譯時已經(jīng)分配好的宦搬,棧是CPU自動控制的,那么我們所討論的內(nèi)存泄漏的問題實際上就是分配在堆里面的內(nèi)存出現(xiàn)了問題乔外,一般問題在于兩點:
  1. 快速不斷的進行new操作床三。比如Android的自定義View的時候你在onDraw里面new出對象,就會導(dǎo)致這個自定義View的繪制特別卡杨幼,這是因為onDraw是快速重復(fù)執(zhí)行的方法撇簿,在這個方法里面每次都new出對象會導(dǎo)致連續(xù)不斷的new出新的對象聂渊,也導(dǎo)致gc也在不斷的執(zhí)行從而不斷的回收堆內(nèi)存。由于堆位于內(nèi)存RAM上四瘫,這樣子就導(dǎo)致了內(nèi)存的不斷的分配和回收消耗了CPU汉嗽,同時導(dǎo)致了內(nèi)存出現(xiàn)“空洞”(因為堆內(nèi)存不是連續(xù)的)
  2. 忘記釋放。如果你忘記了手動釋放應(yīng)該釋放的內(nèi)存找蜜,或者gc誤判導(dǎo)致沒有釋放本應(yīng)該釋放的內(nèi)存饼暑,那么久導(dǎo)致了內(nèi)存泄漏。由于Android給一個app可在堆上(可以在AndroidManifest設(shè)置一個largeHeap="true"增大可分配量)分配的內(nèi)存量是有限的洗做,如果內(nèi)存泄漏不斷的發(fā)生弓叛,總有一天會消耗完畢,從而導(dǎo)致OOM

Java有了垃圾回收(GC)為什么依然會內(nèi)存泄漏

在Java中诚纸,內(nèi)存的分配是由程序完成的撰筷,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的畦徘,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存毕籽,但它只能回收無用并且不再被其它對象引用的那些對象所占用的空間。但是誤判是經(jīng)常發(fā)生的井辆,有些內(nèi)存實際上已經(jīng)沒有用處了关筒,但是GC并不知道。這里簡單介紹下GC的機制:

上面一節(jié)說過棧上的局部變量可以引用堆上的分配的內(nèi)存杯缺,所以GC發(fā)生的時候蒸播,一般是遍歷一下靜態(tài)存儲區(qū)、棧從而列出所有堆上被他們引用的內(nèi)存(對象)集合萍肆,這些內(nèi)存都是有個引用計數(shù)廉赔,那么除此之外,其他的內(nèi)存就是沒有被引用的(或者說引用計數(shù)歸零)匾鸥,這些內(nèi)存就是要被釋放的蜡塌,隨后GC開始清理這些內(nèi)存(對象)

那么這里第一節(jié)的兩個例子就很好理解了,那個單例模式由于生命周期太長(可以把他看作一個虛擬的棧中的局部變量)并且一直引用了Context(即Activity)勿负,所以GC的時候發(fā)現(xiàn)這個Activity的引用計數(shù)還是大于1馏艾,所以回收內(nèi)存的時候把他跳過,但實際上我們已經(jīng)不需要這塊內(nèi)存了奴愉。這樣就導(dǎo)致了內(nèi)存泄漏琅摩。

Android使用弱引用和完美退出app的方法

從上面來看,內(nèi)存泄漏因為對象被別人引用了而導(dǎo)致锭硼,java為了避免這種問題(假如你的單例模式必須要傳入個Context)房资,特地提供了幾個特殊引用類型,其中一個叫做弱引用WeakReference檀头,當它引用一個對象的時候轰异,即使該WeakReference的生命周期更長岖沛,但是只要發(fā)生GC,它就立即釋放所被引用的內(nèi)存而不會繼續(xù)持有搭独。

這里有一個常用的例子:

通常我們會在自定義的Application中來記住app中創(chuàng)建的Activity婴削,從而中途在某個Activity中需要完全退出app時可以完全的銷毀所有已經(jīng)打開的Activity,這里我們可以對自定義Application改造牙肝,讓其只有一個對Activity的弱引用的HashMap唉俗,大致的代碼如下:

public class CustomApplication extends Application {

    private HashMap<String, WeakReference<Activity>> activityList = new HashMap<String, WeakReference<Activity>>();
    private static CustomApplication instance;

    public static CustomApplication getInstance() {
        return instance;
    }

    public void addActivity(Activity activity) {
        if (null != activity) {
            L.d("********* add Activity " + activity.getClass().getName());
            activityList.put(activity.getClass().getName(), new WeakReference<>(activity));
        }
    }


    public void removeActivity(Activity activity) {
        if (null != activity) {
            L.d("********* remove Activity " + activity.getClass().getName());
            activityList.remove(activity.getClass().getName());
        }
    }


    public void exit() {
        
        for (String key : activityList.keySet()) {
            WeakReference<Activity> activity = activityList.get(key);
            if (activity != null && activity.get() != null) {
                L.d("********* Exit " + activity.get().getClass().getSimpleName());
                activity.get().finish();
            }
        }

        System.exit(0);
        android.os.Process.killProcess(android.os.Process.myPid());
    }


}

我們在自定義的Activity的基類BaseActivity中的onCreate執(zhí)行:
CustomApplication.getInstance().addActivity(this);

在BaseActivity的onDestroy中執(zhí)行:
CustomApplication.getInstance().removeActivity(this);

哪些情況會導(dǎo)致內(nèi)存泄漏

到此你應(yīng)該對內(nèi)存泄漏的本質(zhì)已經(jīng)有所了解了,這里列舉出一些會導(dǎo)致內(nèi)存泄漏的地方配椭,可以作為排查內(nèi)存泄漏的一個checklist

  • 某個集合類(List)被一個static變量引用虫溜,同時這個集合類沒有刪除自己內(nèi)部的元素
  • 單例模式持有外部本應(yīng)該被釋放的對象(第一節(jié)中那個例子)
  • Android特殊組件或者類忘記釋放,比如:BraodcastReceiver忘記解注冊股缸、Cursor忘記銷毀吼渡、Socket忘記close、TypedArray忘記recycle乓序、callback忘記remove。如果你自己定義了一個類坎背,最好不要直接將一個Activity類型作為他的屬性替劈,如果必須要用,要么處理好釋放的問題得滤,要么使用弱引用
  • Handler陨献。只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有懂更。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的眨业。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放沮协。如上所述龄捡,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生慷暂。
  • Thread聘殖。如果Thread的run方法一直在循環(huán)的執(zhí)行不停,而該Thread又持有了外部變量行瑞,那么這個外部變量即發(fā)生內(nèi)存泄漏奸腺。
  • 網(wǎng)絡(luò)請求或者其他異步線程。之前Volley會有這樣的一個問題血久,在Volley的response來到之前如果Activity已經(jīng)退出了而且response里面含有Activity的成員變量突照,會導(dǎo)致該Activity發(fā)生內(nèi)存泄漏,該問題一直沒有找到合適的解決辦法氧吐。不過看來Volley官網(wǎng)已經(jīng)注意到這個問題了讹蘑,目前最新的版本已經(jīng)fix this leak


使用leakcanary

之前Android開發(fā)通常使用MAT內(nèi)存分析工具來排查heap的問題末盔,之類的文章比較多,大家可以自己找衔肢。這里推薦一個叫做leakcanary的工具庄岖,他可以集成在你的代碼里面。這個東西大家可以參考:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html

特別鳴謝

文章改編于: 內(nèi)存泄漏弄個明白 - soaringEveryday - 博客園
http://www.cnblogs.com/soaringEveryday/p/5035366.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末角骤,一起剝皮案震驚了整個濱河市隅忿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌邦尊,老刑警劉巖背桐,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蝉揍,居然都是意外死亡链峭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進店門又沾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弊仪,“玉大人,你說我怎么就攤上這事杖刷±” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵滑燃,是天一觀的道長役听。 經(jīng)常有香客問我,道長表窘,這世上最難降的妖魔是什么典予? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮乐严,結(jié)果婚禮上瘤袖,老公的妹妹穿的比我還像新娘。我一直安慰自己昂验,他們只是感情好孽椰,可當我...
    茶點故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凛篙,像睡著了一般黍匾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呛梆,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天锐涯,我揣著相機與錄音,去河邊找鬼填物。 笑死纹腌,一個胖子當著我的面吹牛霎终,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播升薯,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼莱褒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了涎劈?” 一聲冷哼從身側(cè)響起广凸,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛛枚,沒想到半個月后谅海,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡蹦浦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年扭吁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盲镶。...
    茶點故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡侥袜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溉贿,到底是詐尸還是另有隱情枫吧,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布顽照,位于F島的核電站,受9級特大地震影響闽寡,放射性物質(zhì)發(fā)生泄漏代兵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一爷狈、第九天 我趴在偏房一處隱蔽的房頂上張望植影。 院中可真熱鬧,春花似錦涎永、人聲如沸思币。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谷饿。三九已至,卻和暖如春妈倔,著一層夾襖步出監(jiān)牢的瞬間博投,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工盯蝴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毅哗,地道東北人听怕。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像虑绵,于是被迫代替她去往敵國和親尿瞭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,926評論 2 361