分析Android常見的內(nèi)存泄露和解決方案
一、前言
目前 java 垃圾回收主流算法是虛擬機(jī)采用 GC Roots Tracing 算法。算法的基本思路是:通過一系列的名為 GC Roots (GC 根節(jié)點(diǎn))的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(圖論說:從GC Roots 到這個(gè)對(duì)象不可達(dá))時(shí), 證明此對(duì)象是不可用的。
關(guān)于可達(dá)性的對(duì)象,便是能與 GC Roots 構(gòu)成連通圖的對(duì)象,如下圖:
根搜索算法的基本思路就是通過一系列名為 "GC Roots" 的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈 ( Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。
從上圖,reference1、reference2、reference3 都是 GC Roots,可以看出:
reference1-> 對(duì)象實(shí)例1;
reference2-> 對(duì)象實(shí)例2;
reference3-> 對(duì)象實(shí)例4;
reference3-> 對(duì)象實(shí)例4 -> 對(duì)象實(shí)例6;
可以得出對(duì)象實(shí)例1、2、4、6都具有 GC Roots 可達(dá)性,也就是存活對(duì)象,不能被 GC 回收的對(duì)象。
而對(duì)于對(duì)象實(shí)例3、5直接雖然連通,但并沒有任何一個(gè) GC Roots 與之相連,這便是 GC Roots 不可達(dá)的對(duì)象,這就是 GC 需要回收的垃圾對(duì)象。
在了解 GC 之后,開始去了解 Android 的內(nèi)存泄露情況了。
二、Android 內(nèi)存泄露場(chǎng)景
下面會(huì)詳細(xì)介紹一些常見的內(nèi)存泄露場(chǎng)景,以及對(duì)應(yīng)的修復(fù)辦法。
2.1、非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例
比如我們?cè)?Activity 內(nèi)部定義了一個(gè)內(nèi)部類InnerClass,同時(shí)定義了一個(gè)靜態(tài)變量inner,并給予賦值。假設(shè)你在 onDestory 的時(shí)候沒有將 inner 置 null;那么就會(huì)引起內(nèi)存泄露。原因是靜態(tài)變量持有了內(nèi)部類的實(shí)例,內(nèi)部類會(huì)對(duì)外部類有個(gè)引用,從而導(dǎo)致 Activity 得不到釋放。
private static Object inner; 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(); } });
記得在生命周期結(jié)束的時(shí)候,將不需要的靜態(tài)變量置 null。
2.2、多線程相關(guān)的匿名內(nèi)部類/非靜態(tài)內(nèi)部類
和非靜態(tài)內(nèi)部類一樣,匿名內(nèi)部類也會(huì)持有外部類實(shí)例的引用。多線程相關(guān)的類有 AsyncTask 類,Thread 類和 Runnable 接口的類等,它們的匿名內(nèi)部類如果做耗時(shí)操作
就可能發(fā)生內(nèi)存泄露,這里以 AsyncTask 的匿名內(nèi)部類舉例,如下所示:
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(); } });
當(dāng)異步任務(wù)在后臺(tái)執(zhí)行耗時(shí)任務(wù)期間,Activity 不幸被銷毀了(比如:用戶退出,系統(tǒng)回收),這個(gè)被 AsyncTask 持有的 Activity 實(shí)例就不會(huì)被垃圾回收器回收,直到異步任務(wù)結(jié)束。
解決方法是繼承 AsyncTask 新建一個(gè)靜態(tài)內(nèi)部類,用靜態(tài)內(nèi)部類創(chuàng)建實(shí)例就不會(huì)存在對(duì)外部實(shí)例的引用了。
2.3、Handler 內(nèi)存泄露
同樣道理,Handler 的 message 被傳遞到消息隊(duì)列MessageQueue
中,在Message
消息沒有被處理之前,handler 的實(shí)例也不無法被回收,如果 handler 實(shí)例不是靜態(tài)的,就會(huì)導(dǎo)致引用它的 activity 或者 service 不能被回收,于是就會(huì)發(fā)生內(nèi)存泄漏。
void createHandler() { new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.sendMessageDelayed(Message.obtain(), 60000); } View hButton = findViewById(R.id.h_button); hButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createHandler(); nextActivity(); } });
對(duì)于上述問題,有兩種解決辦法,一種是使用一個(gè)靜態(tài)的 handler 內(nèi)部類,并且其持有的對(duì)象都改成弱引用形式進(jìn)行引用。還有一種是在銷毀 activity 的時(shí)候,將發(fā)送的消息進(jìn)行移除。
myHandler.removeCallbackAndMessages(null);
這種有個(gè)問題就是 Handler 中的消息可能無法全部被處理完。
另外還有一個(gè)要注意的是,最好不要直接使用 View#post 來做一些操作。如果要用,確保要用的話,確保 view 已經(jīng)被 attach 到了 window。
2.4、靜態(tài) Activity 或 View
在類中定義了靜態(tài)Activity
變量,把當(dāng)前運(yùn)行的Activity
實(shí)例賦值于這個(gè)靜態(tài)變量。
如果這個(gè)靜態(tài)變量在Activity
生命周期結(jié)束后沒有清空,就導(dǎo)致內(nèi)存泄漏。因?yàn)?static 變量是貫穿這個(gè)應(yīng)用的生命周期的,所以被泄漏的Activity
就會(huì)一直存在于應(yīng)用的進(jìn)程中,不會(huì)被垃圾回收器回收。
static Activity activity; void setStaticActivity() { activity = this; } View saButton = findViewById(R.id.sa_button); saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticActivity(); nextActivity(); } });
為了能夠被回收,需要在不需要使用的時(shí)候進(jìn)行置 null 操作。比如銷毀當(dāng)前 activity 的時(shí)候。
特殊情況:如果一個(gè) View 初始化耗費(fèi)大量資源,而且在一個(gè)Activity
生命周期內(nèi)保持不變,那可以把它變成 static,加載到視圖樹上 (View Hierachy),像這樣,當(dāng)Activity
被銷毀時(shí),應(yīng)當(dāng)釋放資源。
static view; 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)存泄露的問題,在 Activity 銷毀的時(shí)候把這個(gè) static view 置 null 即可,但是還是不建議用這個(gè) static view的方法。
2.5、Eventbus 等注冊(cè)監(jiān)聽造成的內(nèi)存泄露
相信很多同學(xué)都在項(xiàng)目里面會(huì)用到 Eventbus。對(duì)于一些沒有經(jīng)驗(yàn)的同學(xué)在使用的時(shí)候經(jīng)常會(huì)出現(xiàn)一些問題。比如說在 onCreate 的時(shí)候進(jìn)行注冊(cè),卻忘了反注冊(cè),或者說,在onStop的時(shí)候進(jìn)行反注冊(cè),這些都會(huì)導(dǎo)致 Eventbus 的內(nèi)存泄露。
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); EventBus.getDefault().register(this);// 注意在onCreate()方法中注冊(cè) } @Override public void onDestroy() { EventBus.getDefault().unregister(this);// 注意在onDestory()方法中注冊(cè) super.onDestroy(); }
注冊(cè)和反注冊(cè)(取消注冊(cè))是對(duì)應(yīng)的,必須要添加,否則會(huì)引起組件的內(nèi)存泄漏。因?yàn)樽?cè)的時(shí)候組件是被 EventBus 內(nèi)部的單例隊(duì)列所持有引用的。
如果你是在 View 里面注冊(cè) Eventbus 的,記得是在 View 的生命周期 onAttachedToWindow 和 onDetachedFromWindow 的時(shí)候進(jìn)行注冊(cè)和反注冊(cè)。
最近跟我的同事進(jìn)行聊天的時(shí)候發(fā)現(xiàn),他們?yōu)榱私鉀Q eventbus 導(dǎo)致的內(nèi)存泄露問題(已經(jīng)成對(duì)注冊(cè)和反注冊(cè)還是存在內(nèi)存泄露問題),于是打算創(chuàng)建一個(gè) object 的實(shí)例,用這個(gè)來進(jìn)行注冊(cè)與反注冊(cè),這樣即使發(fā)生內(nèi)存泄露也只會(huì)占用很小的內(nèi)存空間。
2.6、單例引起的內(nèi)存泄露
項(xiàng)目中,經(jīng)常會(huì)存在很多單例。有時(shí)候需要我們將當(dāng)前 Activity 實(shí)例傳給單例,然后去做一些事情。如下面的代碼:
public class SingleInstance { private Context mContext; private static SingleInstance instance; private SingleInstance(Context context) { this.mContext = context; } public static SingleInstance getInstance(Context context) { if (instance == null) { instance = new SingleInstance(context); } return instance; } }
上述單例中傳入一個(gè) context ,就會(huì)導(dǎo)致 context 的生命時(shí)長和應(yīng)用的生命時(shí)長一樣。就會(huì)造成內(nèi)存泄露。
對(duì)于這種有三種解決辦法:
1、采用弱引用的方式進(jìn)行引用,確保能夠被回收;
2、在對(duì)應(yīng)的 context 要被銷毀的時(shí)候,進(jìn)行置 null;確保不會(huì)長于原本的生命時(shí)長;
3、看是否能夠使用 APP context;這樣就不會(huì)存在內(nèi)存泄露的問題了。
2.7、資源對(duì)象沒關(guān)閉造成內(nèi)存泄漏
當(dāng)我們打開資源時(shí),一般都會(huì)使用緩存。比如讀寫文件資源、打開數(shù)據(jù)庫資源、使用 Bitmap 資源等等。當(dāng)我們不再使用時(shí),應(yīng)該關(guān)閉它們,使得緩存內(nèi)存區(qū)域及時(shí)回收。雖然有些對(duì)象,如果我們不去關(guān)閉,它自己在 finalize() 函數(shù)中會(huì)自行關(guān)閉。但是這得等到 GC 回收時(shí)才關(guān)閉,這樣會(huì)導(dǎo)致緩存駐留一段時(shí)間。如果我們頻繁的打開資源,內(nèi)存泄漏帶來的影響就比較明顯了。
解決辦法:及時(shí)關(guān)閉資源
2.8、WebView
不同的Android 版本的 webView 會(huì)有差異,加上不同的廠商定制的 ROM 的 webView 差異,這就導(dǎo)致 webView 存在很大的兼容性問題。weView 都會(huì)存在內(nèi)存泄露問題,在應(yīng)用中只要使用一次,內(nèi)存就不會(huì)被釋放。通常的做法是為 webView 單獨(dú)開一個(gè)進(jìn)程,使用 AIDL 與應(yīng)用的主進(jìn)程進(jìn)程通信。webView 進(jìn)程可以根據(jù)業(yè)務(wù)的需求,在合適的時(shí)機(jī)進(jìn)行銷毀。
以上就是分析Android常見的內(nèi)存泄露和解決方案的詳細(xì)內(nèi)容,更多關(guān)于Android 內(nèi)存泄露和解決方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解MVP模式在Android開發(fā)中的應(yīng)用
MVP是MVC衍生而來的,很早以前就由某軟公司提出,近年來在Android應(yīng)用開發(fā)中越來越多的被提及,越來越重要了。這篇文章主要介紹了詳解MVP模式在Android開發(fā)中的應(yīng)用,有興趣的可以了解一下。2016-11-11Android自定義帶動(dòng)畫的半圓環(huán)型進(jìn)度效果
這篇文章主要為大家詳細(xì)介紹了Android自定義帶動(dòng)畫的半圓環(huán)型進(jìn)度效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Android通過SeekBar調(diào)節(jié)布局背景顏色
這篇文章主要為大家詳細(xì)介紹了Android通過SeekBar調(diào)節(jié)布局背景顏色,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Android多線程+單線程+斷點(diǎn)續(xù)傳+進(jìn)度條顯示下載功能
這篇文章主要介紹了Android多線程+單線程+斷點(diǎn)續(xù)傳+進(jìn)度條顯示下載功能,需要的朋友可以參考下2017-06-06詳解Matisse與Glide--java.lang.NoSuchMethodError:com.bumptech.gl
這篇文章主要介紹了在使用Matisse與glide4.0.0以及4.0.0之后的版本過程中,碰到該問題java.lang.NoSuchMethodError:com.bumptech.glide.RequestManager.load的解決方法2021-08-08Android使用RecyclerView實(shí)現(xiàn)今日頭條頻道管理功能
這篇文章主要為大家詳細(xì)介紹了Android使用RecyclerView實(shí)現(xiàn)今日頭條頻道管理功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android結(jié)合xml實(shí)現(xiàn)幀動(dòng)畫
將一組動(dòng)作相近的圖片組合在一起,然后按照一定的時(shí)間來播放,就會(huì)形成一個(gè)動(dòng)畫,我們可以稱之為幀動(dòng)畫。在 Android 中可通過結(jié)合 xml 的方式來輕松實(shí)現(xiàn)。2021-05-05Android應(yīng)用動(dòng)態(tài)修改主題的方法示例
今天小編就為大家分享一篇關(guān)于Android應(yīng)用動(dòng)態(tài)修改主題的方法示例,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03