常見的內(nèi)存泄漏

轉(zhuǎn)載來之
http://blog.nimbledroid.com/2016/05/23/memory-leaks-zh.html

像 Java 這樣具有垃圾回收功能的語言的好處之一灭翔,就是程序員無需手動管理內(nèi)存分配涣雕。這減少了段錯誤(segmentation fault)導(dǎo)致的閃退简僧,也減少了內(nèi)存泄漏導(dǎo)致的堆空間膨脹,讓編寫的代碼更加安全。然而,Java 中依然有可能發(fā)生內(nèi)存泄漏。所以你的安卓 APP 依然有可能浪費了大量的內(nèi)存敛纲,甚至由于內(nèi)存耗盡(OOM)導(dǎo)致閃退。
傳統(tǒng)的內(nèi)存泄漏是由忘記釋放分配的內(nèi)存導(dǎo)致的剂癌,而邏輯上的內(nèi)存泄漏則是由于忘記在對象不再被使用的時候釋放對其的引用導(dǎo)致的淤翔。如果一個對象仍然存在強引用,垃圾回收器就無法對其進(jìn)行垃圾回收佩谷。在安卓平臺旁壮,泄漏 Context 對象問題尤其嚴(yán)重辞做。這是因為像 Activity 這樣的 Context 對象會引用大量很占用內(nèi)存的對象,例如 View 層級寡具,以及其他的資源秤茅。如果 Context 對象發(fā)生了內(nèi)存泄漏,那它引用的所有對象都被泄漏了童叠。安卓設(shè)備大多內(nèi)存有限框喳,如果發(fā)生了大量這樣的內(nèi)存泄漏,那內(nèi)存將很快耗盡厦坛。
如果一個對象的合理生命周期沒有清晰的定義五垮,那判斷邏輯上的內(nèi)存泄漏將是一個見仁見智的問題。幸運的是杜秸,activity 有清晰的生命周期定義放仗,使得我們可以很明確地判斷 activity 對象是否被內(nèi)存泄漏。onDestroy() 函數(shù)將在 activity 被銷毀時調(diào)用撬碟,無論是程序員主動銷毀 activity诞挨,還是系統(tǒng)為了回收內(nèi)存而將其銷毀。如果 onDestroy 執(zhí)行完畢之后呢蛤,activity 對象仍被 heap root 強引用惶傻,那垃圾回收器就無法將其回收。所以我們可以把生命周期結(jié)束之后仍被引用的 activity 定義為被泄漏的 activity其障。
Activity 是非常重量級的對象银室,所以我們應(yīng)該極力避免妨礙系統(tǒng)對其進(jìn)行回收。然而有多種方式會讓我們無意間就泄露了 activity 對象励翼。我們把可能導(dǎo)致 activity 泄漏的情況分為兩類蜈敢,一類是使用了進(jìn)程全局(process-global)的靜態(tài)變量,無論 APP 處于什么狀態(tài)汽抚,都會一直存在抓狭,它們持有了對 activity 的強引用進(jìn)而導(dǎo)致內(nèi)存泄漏,另一類是生命周期長于 activity 的線程殊橙,它們忘記釋放對 activity 的強引用進(jìn)而導(dǎo)致內(nèi)存泄漏辐宾。下面我們就來詳細(xì)分析一下這些可能導(dǎo)致 activity 泄漏的情況狱从。

  1. 靜態(tài) Activity
    泄漏 activity 最簡單的方法就是在 activity 類中定義一個 static 變量膨蛮,并且將其指向一個運行中的 activity 實例。如果在 activity 的生命周期結(jié)束之前季研,沒有清除這個引用敞葛,那它就會泄漏了。這是因為 activity(例如 MainActivity) 的類對象是靜態(tài)的与涡,一旦加載惹谐,就會在 APP 運行時一直常駐內(nèi)存持偏,因此如果類對象不卸載,其靜態(tài)成員就不會被垃圾回收氨肌。
    void setStaticActivity() { activity = this;}View saButton = findViewById(R.id.sa_button);saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticActivity(); nextActivity(); }});

內(nèi)存泄漏場景 1 - Static Activity

  1. 靜態(tài) View
    另一種類似的情況是對經(jīng)常啟動的 activity 實現(xiàn)一個單例模式鸿秆,讓其常駐內(nèi)存可以使它能夠快速恢復(fù)狀態(tài)。然而怎囚,就像前文所述卿叽,不遵循系統(tǒng)定義的 activity 生命周期是非常危險的,也是沒必要的恳守,所以我們應(yīng)該極力避免考婴。
    但是如果我們有一個創(chuàng)建起來非常耗時的 View,在同一個 activity 不同的生命周期中都保持不變呢催烘?所以讓我們?yōu)樗鼘崿F(xiàn)一個單例模式沥阱,就像這段代碼。現(xiàn)在一旦 activity 被銷毀伊群,那我們就應(yīng)該釋放大部分的內(nèi)存了考杉。
    void setStaticView() { view = findViewById(R.id.sv_button);}View svButton = findViewById(R.id.sv_button);svButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticView(); nextActivity(); }});

內(nèi)存泄漏場景 2 - Static View

內(nèi)存泄漏了!因為一旦 view 被加入到界面中舰始,它就會持有 context 的強引用奔则,也就是我們的 activity。由于我們通過一個靜態(tài)成員引用了這個 view蔽午,所以我們也就引用了 activity易茬,因此 activity 就發(fā)生了泄漏。所以一定不要把加載的 view 賦值給靜態(tài)變量及老,如果你真的需要抽莱,那一定要確保在 activity 銷毀之前將其從 view 層級中移除

  1. 內(nèi)部類
    現(xiàn)在讓我們在 activity 內(nèi)部定義一個類骄恶,也就是內(nèi)部類食铐。這樣做的原因有很多,比如增加封裝性和可讀性僧鲁。如果我們創(chuàng)建了一個內(nèi)部類的對象虐呻,并且通過靜態(tài)變量持有了 activity 的引用,那也會發(fā)生 activity 泄漏寞秃。
    void createInnerClass() { class InnerClass { } inner = new InnerClass();}View icButton = findViewById(R.id.ic_button);icButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createInnerClass(); nextActivity(); }});

內(nèi)存泄漏場景 3 - Inner Class

不幸的是斟叼,內(nèi)部類能夠引用外部類的成員這一優(yōu)勢,就是通過持有外部類的引用來實現(xiàn)的春寿,而這正是 activity 泄漏的原因朗涩。

  1. 匿名類
    類似的,匿名類同樣會持有定義它們的對象的引用绑改。因此如果在 activity 內(nèi)定義了一個匿名的 AsyncTask 對象谢床,就有可能發(fā)生內(nèi)存泄漏了兄一。如果 activity 被銷毀之后 AsyncTask 仍然在執(zhí)行,那就會組織垃圾回收器回收 activity 對象识腿,進(jìn)而導(dǎo)致內(nèi)存泄漏出革,直到執(zhí)行結(jié)束才能回收 activity。
    void startAsyncTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { while(true); } }.execute();}super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);View aicButton = findViewById(R.id.at_button);aicButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); nextActivity(); }});

內(nèi)存內(nèi)存泄漏場景 4 - AsyncTask

  1. Handlers
    同樣的渡讼,定義一個匿名的 Runnable 對象并將其提交到 Handler 上也可能導(dǎo)致 activity 泄漏蹋盆。Runnable 對象間接地引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中硝全,如果它在 activity 銷毀時還沒有被處理栖雾,那就會導(dǎo)致 activity 泄漏了。
    void createHandler() { new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.postDelayed(new Runnable() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1);}View hButton = findViewById(R.id.h_button);hButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createHandler(); nextActivity(); }});

內(nèi)存泄漏場景 5 - Handler

  1. Threads
    同樣的伟众,使用 ThreadTimerTask 也可能導(dǎo)致 activity 泄漏析藕。
    void spawnThread() { new Thread() { @Override public void run() { while(true); } }.start();}View tButton = findViewById(R.id.t_button);tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { spawnThread(); nextActivity(); }});

內(nèi)存泄漏場景 6 - Thread

  1. Timer Tasks
    只要它們是通過匿名類創(chuàng)建的,盡管它們在單獨的線程被執(zhí)行凳厢,它們也會持有對 activity 的強引用账胧,進(jìn)而導(dǎo)致內(nèi)存泄漏。
    void scheduleTimer() { new Timer().schedule(new TimerTask() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1);}View ttButton = findViewById(R.id.tt_button);ttButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { scheduleTimer(); nextActivity(); }});

內(nèi)存泄漏場景 7 - TimerTask

  1. Sensor Manager
    最后先紫,系統(tǒng)服務(wù)可以通過 context.getSystemService 獲取治泥,它們負(fù)責(zé)執(zhí)行某些后臺任務(wù),或者為硬件訪問提供接口遮精。如果 context 對象想要在服務(wù)內(nèi)部的事件發(fā)生時被通知居夹,那就需要把自己注冊到服務(wù)的監(jiān)聽器中。然而本冲,這會讓服務(wù)持有 activity 的引用准脂,如果程序員忘記在 activity 銷毀時取消注冊,那就會導(dǎo)致 activity 泄漏了檬洞。
    void registerListener() { SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);}View smButton = findViewById(R.id.sm_button);smButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { registerListener(); nextActivity(); }});

內(nèi)存泄漏場景 8 - Sensor Manager

現(xiàn)在狸膏,我們展示了八種很容易不經(jīng)意間就泄漏大量內(nèi)存的情景。請記住添怔,最壞的情況下湾戳,你的 APP 可能會由于大量的內(nèi)存泄漏而內(nèi)存耗盡,進(jìn)而閃退广料,但它并不總是這樣砾脑。相反,內(nèi)存泄漏會消耗大量的內(nèi)存性昭,但卻不至于內(nèi)存耗盡拦止,這時县遣,APP 會由于內(nèi)存不夠分配而頻繁進(jìn)行垃圾回收糜颠。垃圾回收是非常耗時的操作汹族,會導(dǎo)致嚴(yán)重的卡頓。在 activity 內(nèi)部創(chuàng)建對象時其兴,一定要格外小心顶瞒,并且要經(jīng)常測試是否存在內(nèi)存泄漏。

SDK內(nèi)存泄漏 :

今天在使用LeakCanary檢查應(yīng)用的內(nèi)存泄露時元旬,報了一個這樣的錯誤榴徐,并且還給出了參考鏈接,原來這是Android輸入法的一個bug匀归,在15<=API<=23中都存在坑资。


?LeakCanary之所以能夠顯示參考鏈接是因為它有一個針對SDK已知內(nèi)存泄露的列表,放在AndroidExcludedRefs.java中穆端,比如輸入法的這個袱贮。
[圖片上傳中。体啰。攒巍。(2)]
?這個問題很多人都遇到過,網(wǎng)上已經(jīng)有比較成熟的方案荒勇,分析原因比較透徹的是這篇文章:[Android][Memory Leak] InputMethodManager內(nèi)存泄露現(xiàn)象及解決柒莉,改善方案可以參考Leaknary給出的方案:InputMethodManager內(nèi)存泄露修正方案,在退出使用InputMethodManager的Activity時沽翔,調(diào)用fixFocusedViewLeak方法即可解決兢孝。
/** * Fix for https://code.google.com/p/android/issues/detail?id=171190 . * * When a view that has focus gets detached, we wait for the main thread to be idle and then * check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got * focus, which is what happens if you press home and come back from recent apps. This replaces * the reference to the detached view with a reference to the decor view. * * Should be called from {@link Activity#onCreate(android.os.Bundle)} )}. */public static void fixFocusedViewLeak(Application application) { // Don't know about other versions yet. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1|| Build.VERSION.SDK_INT > 23) { return; } final InputMethodManager inputMethodManager = (InputMethodManager) application.getSystemService(Context.INPUT_METHOD_SERVICE); final Field mServedViewField; final Field mHField; final Method finishInputLockedMethod; final Method focusInMethod; try { mServedViewField = InputMethodManager.class.getDeclaredField("mServedView"); mServedViewField.setAccessible(true); mHField = InputMethodManager.class.getDeclaredField("mServedView"); mHField.setAccessible(true); finishInputLockedMethod = InputMethodManager.class.getDeclaredMethod("finishInputLocked"); finishInputLockedMethod.setAccessible(true); focusInMethod = InputMethodManager.class.getDeclaredMethod("focusIn", View.class); focusInMethod.setAccessible(true); } catch (NoSuchMethodException | NoSuchFieldException unexpected) { Log.e("IMMLeaks", "Unexpected reflection exception", unexpected); return; } application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { @Override public void onActivityDestroyed(Activity activity){ } @Override public void onActivityStarted(Activity activity){ } @Override public void onActivityResumed(Activity activity){ } @Override public void onActivityPaused(Activity activity){ } @Override public void onActivityStopped(Activity activity){ } @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle){ } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { ReferenceCleaner cleaner = new ReferenceCleaner(inputMethodManager, mHField, mServedViewField, finishInputLockedMethod); View rootView = activity.getWindow().getDecorView().getRootView(); ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver(); viewTreeObserver.addOnGlobalFocusChangeListener(cleaner); } });}static class ReferenceCleaner implements MessageQueue.IdleHandler, View.OnAttachStateChangeListener, ViewTreeObserver.OnGlobalFocusChangeListener { private final InputMethodManager inputMethodManager; private final Field mHField; private final Field mServedViewField; private final Method finishInputLockedMethod; ReferenceCleaner(InputMethodManager inputMethodManager, Field mHField, Field mServedViewField, Method finishInputLockedMethod) { this.inputMethodManager = inputMethodManager; this.mHField = mHField; this.mServedViewField = mServedViewField; this.finishInputLockedMethod = finishInputLockedMethod; } @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { if (newFocus == null) { return; } if (oldFocus != null) { oldFocus.removeOnAttachStateChangeListener(this); } Looper.myQueue().removeIdleHandler(this); newFocus.addOnAttachStateChangeListener(this); } @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { v.removeOnAttachStateChangeListener(this); Looper.myQueue().removeIdleHandler(this); Looper.myQueue().addIdleHandler(this); } @Override public boolean queueIdle() { clearInputMethodManagerLeak(); return false; } private void clearInputMethodManagerLeak() { try { Object lock = mHField.get(inputMethodManager); // This is highly dependent on the InputMethodManager implementation. synchronized (lock) { View servedView = (View) mServedViewField.get(inputMethodManager); if (servedView != null) { boolean servedViewAttached = servedView.getWindowVisibility() != View.GONE; if (servedViewAttached) { // The view held by the IMM was replaced without a global focus change. Let's make // sure we get notified when that view detaches. // Avoid double registration. servedView.removeOnAttachStateChangeListener(this); servedView.addOnAttachStateChangeListener(this); } else { // servedView is not attached. InputMethodManager is being stupid! Activity activity = extractActivity(servedView.getContext()); if (activity == null || activity.getWindow() == null) { // Unlikely case. Let's finish the input anyways. finishInputLockedMethod.invoke(inputMethodManager); } else { View decorView = activity.getWindow().peekDecorView(); boolean windowAttached = decorView.getWindowVisibility() != View.GONE; if (!windowAttached) { finishInputLockedMethod.invoke(inputMethodManager); } else { decorView.requestFocusFromTouch(); } } } } } } catch (IllegalAccessException |InvocationTargetException unexpected) { Log.e("IMMLeaks", "Unexpected reflection exception", unexpected); } } private Activity extractActivity(Context context) { while (true) { if (context instanceof Application) { return null; } else if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { Context baseContext = ((ContextWrapper) context).getBaseContext(); // Prevent Stack Overflow. if (baseContext == context) { return null; } context = baseContext; } else { return null; } } }}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市仅偎,隨后出現(xiàn)的幾起案子西潘,更是在濱河造成了極大的恐慌,老刑警劉巖哨颂,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喷市,死亡現(xiàn)場離奇詭異,居然都是意外死亡威恼,警方通過查閱死者的電腦和手機(jī)品姓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箫措,“玉大人腹备,你說我怎么就攤上這事〗锫” “怎么了植酥?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我友驮,道長漂羊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任卸留,我火速辦了婚禮走越,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘耻瑟。我一直安慰自己旨指,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布喳整。 她就那樣靜靜地躺著谆构,像睡著了一般。 火紅的嫁衣襯著肌膚如雪框都。 梳的紋絲不亂的頭發(fā)上低淡,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音瞬项,去河邊找鬼蔗蹋。 笑死,一個胖子當(dāng)著我的面吹牛囱淋,可吹牛的內(nèi)容都是我干的猪杭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼妥衣,長吁一口氣:“原來是場噩夢啊……” “哼皂吮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起税手,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蜂筹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后芦倒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艺挪,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年兵扬,在試婚紗的時候發(fā)現(xiàn)自己被綠了麻裳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡器钟,死狀恐怖津坑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情傲霸,我是刑警寧澤疆瑰,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布眉反,位于F島的核電站,受9級特大地震影響穆役,放射性物質(zhì)發(fā)生泄漏寸五。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一孵睬、第九天 我趴在偏房一處隱蔽的房頂上張望播歼。 院中可真熱鬧伶跷,春花似錦掰读、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雇初,卻和暖如春拢肆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背靖诗。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工郭怪, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刊橘。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓鄙才,卻偏偏與公主長得像,于是被迫代替她去往敵國和親促绵。 傳聞我的和親對象是個殘疾皇子攒庵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容