常見的內存泄漏

轉載來之
http://blog.nimbledroid.com/2016/05/23/memory-leaks-zh.html

像 Java 這樣具有垃圾回收功能的語言的好處之一,就是程序員無需手動管理內存分配。這減少了段錯誤(segmentation fault)導致的閃退,也減少了內存泄漏導致的堆空間膨脹,讓編寫的代碼更加安全。然而,Java 中依然有可能發生內存泄漏。所以你的安卓 APP 依然有可能浪費了大量的內存,甚至由于內存耗盡(OOM)導致閃退。
傳統的內存泄漏是由忘記釋放分配的內存導致的,而邏輯上的內存泄漏則是由于忘記在對象不再被使用的時候釋放對其的引用導致的。如果一個對象仍然存在強引用,垃圾回收器就無法對其進行垃圾回收。在安卓平臺,泄漏 Context 對象問題尤其嚴重。這是因為像 Activity 這樣的 Context 對象會引用大量很占用內存的對象,例如 View 層級,以及其他的資源。如果 Context 對象發生了內存泄漏,那它引用的所有對象都被泄漏了。安卓設備大多內存有限,如果發生了大量這樣的內存泄漏,那內存將很快耗盡。
如果一個對象的合理生命周期沒有清晰的定義,那判斷邏輯上的內存泄漏將是一個見仁見智的問題。幸運的是,activity 有清晰的生命周期定義,使得我們可以很明確地判斷 activity 對象是否被內存泄漏。onDestroy() 函數將在 activity 被銷毀時調用,無論是程序員主動銷毀 activity,還是系統為了回收內存而將其銷毀。如果 onDestroy 執行完畢之后,activity 對象仍被 heap root 強引用,那垃圾回收器就無法將其回收。所以我們可以把生命周期結束之后仍被引用的 activity 定義為被泄漏的 activity。
Activity 是非常重量級的對象,所以我們應該極力避免妨礙系統對其進行回收。然而有多種方式會讓我們無意間就泄露了 activity 對象。我們把可能導致 activity 泄漏的情況分為兩類,一類是使用了進程全局(process-global)的靜態變量,無論 APP 處于什么狀態,都會一直存在,它們持有了對 activity 的強引用進而導致內存泄漏,另一類是生命周期長于 activity 的線程,它們忘記釋放對 activity 的強引用進而導致內存泄漏。下面我們就來詳細分析一下這些可能導致 activity 泄漏的情況。

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

內存泄漏場景 1 - Static Activity

  1. 靜態 View
    另一種類似的情況是對經常啟動的 activity 實現一個單例模式,讓其常駐內存可以使它能夠快速恢復狀態。然而,就像前文所述,不遵循系統定義的 activity 生命周期是非常危險的,也是沒必要的,所以我們應該極力避免。
    但是如果我們有一個創建起來非常耗時的 View,在同一個 activity 不同的生命周期中都保持不變呢?所以讓我們為它實現一個單例模式,就像這段代碼。現在一旦 activity 被銷毀,那我們就應該釋放大部分的內存了。
    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(); }});

內存泄漏場景 2 - Static View

內存泄漏了!因為一旦 view 被加入到界面中,它就會持有 context 的強引用,也就是我們的 activity。由于我們通過一個靜態成員引用了這個 view,所以我們也就引用了 activity,因此 activity 就發生了泄漏。所以一定不要把加載的 view 賦值給靜態變量,如果你真的需要,那一定要確保在 activity 銷毀之前將其從 view 層級中移除

  1. 內部類
    現在讓我們在 activity 內部定義一個類,也就是內部類。這樣做的原因有很多,比如增加封裝性和可讀性。如果我們創建了一個內部類的對象,并且通過靜態變量持有了 activity 的引用,那也會發生 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(); }});

內存泄漏場景 3 - Inner Class

不幸的是,內部類能夠引用外部類的成員這一優勢,就是通過持有外部類的引用來實現的,而這正是 activity 泄漏的原因。

  1. 匿名類
    類似的,匿名類同樣會持有定義它們的對象的引用。因此如果在 activity 內定義了一個匿名的 AsyncTask 對象,就有可能發生內存泄漏了。如果 activity 被銷毀之后 AsyncTask 仍然在執行,那就會組織垃圾回收器回收 activity 對象,進而導致內存泄漏,直到執行結束才能回收 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(); }});

內存內存泄漏場景 4 - AsyncTask

  1. Handlers
    同樣的,定義一個匿名的 Runnable 對象并將其提交到 Handler 上也可能導致 activity 泄漏。Runnable 對象間接地引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷毀時還沒有被處理,那就會導致 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(); }});

內存泄漏場景 5 - Handler

  1. Threads
    同樣的,使用 ThreadTimerTask 也可能導致 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(); }});

內存泄漏場景 6 - Thread

  1. Timer Tasks
    只要它們是通過匿名類創建的,盡管它們在單獨的線程被執行,它們也會持有對 activity 的強引用,進而導致內存泄漏。
    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(); }});

內存泄漏場景 7 - TimerTask

  1. Sensor Manager
    最后,系統服務可以通過 context.getSystemService 獲取,它們負責執行某些后臺任務,或者為硬件訪問提供接口。如果 context 對象想要在服務內部的事件發生時被通知,那就需要把自己注冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷毀時取消注冊,那就會導致 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(); }});

內存泄漏場景 8 - Sensor Manager

現在,我們展示了八種很容易不經意間就泄漏大量內存的情景。請記住,最壞的情況下,你的 APP 可能會由于大量的內存泄漏而內存耗盡,進而閃退,但它并不總是這樣。相反,內存泄漏會消耗大量的內存,但卻不至于內存耗盡,這時,APP 會由于內存不夠分配而頻繁進行垃圾回收。垃圾回收是非常耗時的操作,會導致嚴重的卡頓。在 activity 內部創建對象時,一定要格外小心,并且要經常測試是否存在內存泄漏。

SDK內存泄漏 :

今天在使用LeakCanary檢查應用的內存泄露時,報了一個這樣的錯誤,并且還給出了參考鏈接,原來這是Android輸入法的一個bug,在15<=API<=23中都存在。


?LeakCanary之所以能夠顯示參考鏈接是因為它有一個針對SDK已知內存泄露的列表,放在AndroidExcludedRefs.java中,比如輸入法的這個。
[圖片上傳中。。。(2)]
?這個問題很多人都遇到過,網上已經有比較成熟的方案,分析原因比較透徹的是這篇文章:[Android][Memory Leak] InputMethodManager內存泄露現象及解決,改善方案可以參考Leaknary給出的方案:InputMethodManager內存泄露修正方案,在退出使用InputMethodManager的Activity時,調用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; } } }}

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

推薦閱讀更多精彩內容