Android 內存泄漏

前言

內存管理的目的就是讓我們在開發過程中有效避免我們的應用程序出現內存泄露的問題。內存泄露相信大家都不陌生,我們可以這樣理解:「沒有用的對象無法回收的現象就是內存泄露」。

如果程序發生了內存泄露,則會帶來以下這些問題

應用可用的內存減少,增加了堆內存的壓力

降低了應用的性能,比如會觸發更頻繁的 GC

嚴重的時候可能會導致內存溢出錯誤,即 OOM Error

OOM 發生在,當我們嘗試進行創建對象,但是堆內存無法通過 GC 釋放足夠的空間,堆內存也無法再繼續增長,從而完成對象創建請求的時候,OOM 發生很有可能是內存泄露導致的,但并非所有的 OOM 都是由內存泄露引起的,內存泄露也并不一定引起 OOM。

一、基礎準備

如果真的想比較清楚的了解內存泄露的話,對于 Java 的內存管理以及引用類型有一個清晰的認識是必不可少的。

理解 Java 的內存管理能讓我們更深一層地了解 Java 虛擬機是怎樣使用內存的,一旦出現內存泄露,我們也能更加從容地排查問題。

了解 Java 的引用類型,能讓我們更加理解內存泄露出現的原因,以及常見的解決方法。

具體的內容,可以看下這篇文章你真的懂 Java 的內存管理和引用類型嗎?

二、Android 中內存泄露的常見場景 & 解決方案

1、單例造成的內存泄露

單例模式是非常常用的設計模式,使用單例模式的類,只會產生一個對象,這個對象看起來像是一直占用著內存,但這并不意味著就是浪費了內存,內存本來就是拿來裝東西的,只要這個對象一直都被高效的利用就不能叫做泄露。

但是過多的單例會讓內存占用過多,而且單例模式由于其靜態特性,其生命周期 = 應用程序的生命周期,不正確地使用單例模式也會造成內存泄露。

舉個例子:

publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context;? ? }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){? ? ? ? ? ? sInstance =newSingleInstanceTest(context);? ? ? ? }returnsInstance;? ? }}

上面是一個比較簡單的單例模式用法,需要外部傳入一個 Context 來獲取該類的實例,如果此時傳入的 Context 是 Activity 的話,此時單例就有持有該 Activity 的強引用(直到整個應用生命周期結束)。這樣的話,即使該 Activity 退出,該 Activity 的內存也不會被回收,這樣就造成了內存泄露,特別是一些比較大的 Activity,甚至還會導致 OOM(Out Of Memory)。

解決方法:單例模式引用的對象的生命周期 = 應用生命周期

publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context.getApplicationContext();? ? }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){? ? ? ? ? ? sInstance =newSingleInstanceTest(context);? ? ? ? }returnsInstance;? ? }}

可以看到在 SingleInstanceTest 的構造函數中,將 context.getApplicationContext() 賦值給 mContext,此時單例引用的對象是 Application,而 Application 的生命周期本來就跟應用程序是一樣的,也就不存在內存泄露。

這里再拓展一點,很多時候我們在需要用到 Activity 或者 Context 的地方,會直接將 Activity 的實例作為參數傳給對應的類,就像這樣:

publicclassSample{privateContext mContext;publicSample(Context context){this.mContext = context;? ? }publicContextgetContext(){returnmContext;? ? }}// 外部調用Sample sample =newSample(MainActivity.this);

這種情況如果不注意的話,很容易就會造成內存泄露,比較好的寫法是使用弱引用(WeakReference)來進行改進。

publicclassSample{privateWeakReference mWeakReference;publicSample(Context context){this.mWeakReference =newWeakReference<>(context);? ? }publicContextgetContext(){if(mWeakReference.get() !=null){returnmWeakReference.get();? ? ? ? }returnnull;? ? }}// 外部調用Sample sample =newSample(MainActivity.this);

被弱引用關聯的對象只能存活到下一次垃圾回收之前,也就是說即使 Sample 持有 Activity 的引用,但由于 GC 會幫我們回收相關的引用,被銷毀的 Activity 也會被回收內存,這樣我們就不用擔心會發生內存泄露了。

2、非靜態內部類 / 匿名類

我們先來看看非靜態內部類(non static inner class)和 靜態內部類(static inner class)之間的區別。

class 對比static inner classnon static inner class

與外部 class 引用關系如果沒有傳入參數,就沒有引用關系自動獲得強引用

被調用時需要外部實例不需要需要

能否調用外部 class 中的變量和方法不能能

生命周期自主的生命周期依賴于外部類,甚至比外部類更長

可以看到非靜態內部類自動獲得外部類的強引用,而且它的生命周期甚至比外部類更長,這便埋下了內存泄露的隱患。如果一個 Activity 的非靜態內部類的生命周期比 Activity 更長,那么 Activity 的內存便無法被回收,也就是發生了內存泄露,而且還有可能發生難以預防的空指針問題。

舉個例子:

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);newMyAscnyTask().execute();? ? }classMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{? ? ? ? ? ? ? ? Thread.sleep(5000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }return"";? ? ? ? }? ? }}

可以看到我們在 Activity 中繼承 AsyncTask 自定義了一個非靜態內部類,在 doInbackground() 方法中做了耗時的操作,然后在 onCreate() 中啟動 MyAsyncTask。如果在耗時操作結束之前,Activity 被銷毀了,這時候因為 MyAsyncTask 持有 Activity 的強引用,便會導致 Activity 的內存無法被回收,這時候便會產生內存泄露。

解決方法:將 MyAsyncTask 變成靜態內部類

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);newMyAscnyTask().execute();? ? }staticclassMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{? ? ? ? ? ? ? ? Thread.sleep(50000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }return"";? ? ? ? }? ? }}

這時候 MyAsyncTask 不再持有 Activity 的強引用,即使 AsyncTask 的耗時操作還在繼續,Activity 的內存也能順利地被回收。

匿名類和非靜態內部類最大的共同點就是都持有外部類的引用,因此,匿名類造成內存泄露的原因也跟靜態內部類基本是一樣的,下面舉個幾個比較常見的例子:

publicclassMainActivityextendsAppCompatActivity{privateHandler mHandler =newHandler(){@OverridepublicvoidhandleMessage(Message msg){super.handleMessage(msg);? ? ? ? }? ? };@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);// ① 匿名線程持有 Activity 的引用,進行耗時操作newThread(newRunnable() {@Overridepublicvoidrun(){try{? ? ? ? ? ? ? ? ? ? Thread.sleep(50000);? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }).start();// ② 使用匿名 Handler 發送耗時消息Message message = Message.obtain();? ? ? ? mHandler.sendMessageDelayed(message,60000);? ? }

上面舉出了兩個比較常見的例子

new 出一個匿名的 Thread,進行耗時的操作,如果 MainActivity 被銷毀而 Thread 中的耗時操作沒有結束的話,便會產生內存泄露

new 出一個匿名的 Handler,這里我采用了 sendMessageDelayed() 方法來發送消息,這時如果 MainActivity 被銷毀,而 Handler 里面的消息還沒發送完畢的話,Activity 的內存也不會被回收

解決方法:

繼承 Thread? 實現靜態內部類

繼承 Handler 實現靜態內部類,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);

3、集合類

集合類添加元素后,仍引用著集合元素對象,導致該集合中的元素對象無法被回收,從而導致內存泄露,舉個例子:

staticList objectList =newArrayList<>();for(int i =0; i <10; i++) {Objectobj =newObject();? ? ? objectList.add(obj);? ? ? obj =null;? ? }

在這個例子中,循環多次將 new 出來的對象放入一個靜態的集合中,因為靜態變量的生命周期和應用程序一致,而且他們所引用的對象 Object 也不能釋放,這樣便造成了內存泄露。

解決方法:在集合元素使用之后從集合中刪除,等所有元素都使用完之后,將集合置空。

objectList.clear();? ? objectList =null;

4、其他的情況

除了上述 3 種常見情況外,還有其他的一些情況

1、需要手動關閉的對象沒有關閉

網絡、文件等流忘記關閉

手動注冊廣播時,退出時忘記 unregisterReceiver()

Service 執行完后忘記 stopSelf()

EventBus 等觀察者模式的框架忘記手動解除注冊

2、static 關鍵字修飾的成員變量

3、ListView 的 Item 泄露

三、利用工具進行內存泄露的排查

除了必須了解常見的內存泄露場景以及相應的解決方法之外,掌握一些好用的工具,能讓我們更有效率地解決內存泄露的問題。

1、Android Lint

Lint 是 Android Studio 提供的代碼掃描分析工具,它可以幫助我們發現代碼機構 / 質量問題,同時提供一些解決方案,檢測內存泄露當然也不在話下,使用也是非常的簡單,可以參考下這篇文章:Android 性能優化:使用 Lint 優化代碼、去除多余資源

2、leakcanary

LeakCanary 是 Square 公司開源的「Android 和 Java 的內存泄漏檢測庫」,Square 出品,必屬精品,功能很強大,使用也很簡單。建議直接看 Github 上的說明:leakcanary,也可以參考這篇文章:Android內存優化(六)LeakCanary使用詳解

作者:developerHaoz

鏈接:http://www.lxweimin.com/p/65f914e6a2f8

來源:簡書

簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。

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

推薦閱讀更多精彩內容

  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,382評論 0 12
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,648評論 0 8
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 798評論 0 5
  • 整理平時遇到Android內存泄漏歸納分析心得 內存泄漏:對于Java來說,就是new出來的Object 放在堆上...
    安仔夏天勤奮閱讀 1,075評論 1 28
  • 今天上午媽媽帶我去上輪滑課。我見到了張光耀我們一起愉快的玩耍。我們還認識了一個新的教練,他的名字叫小天教練。今天的...
    小虎寶1閱讀 244評論 0 0