一、引言
一般情況下Android的內(nèi)存泄漏是因為,存在引用指向一個本該被回收的對象豺鼻,例如已經(jīng)執(zhí)行onDestroy()
的Activity。在這種情況下,由于Activity內(nèi)某些對象的生命周期比Activity要長噩斟,在Activity理論上被銷毀時,該對象依舊存在并持有Activity的引用孤个,因此內(nèi)存回收機(jī)制(GC)無法釋放Activity剃允,最終導(dǎo)致內(nèi)存泄漏。
為了發(fā)現(xiàn)和修復(fù)APP中存在的內(nèi)存泄漏齐鲤,開發(fā)人員會在APP上安裝內(nèi)存檢測工具(如leakcanary)斥废,當(dāng)出現(xiàn)內(nèi)存泄漏時,該工具會提供一個報告给郊,里面包含了一條引用鏈营袜,指明可能造成內(nèi)存泄漏的引用。開發(fā)人員需要在合適的地方切斷引用鏈丑罪,以便GC釋放掉沒有被引用的對象荚板。
有些內(nèi)存泄漏的修復(fù)很簡單,將非靜態(tài)內(nèi)部類內(nèi)部類改為靜態(tài)內(nèi)部類或者將Context改為ApplicationContext后檢測工具就檢測不出內(nèi)存泄漏了吩屹,但是這到底是為什么呢跪另?而且就算檢測工具檢測不出內(nèi)存泄漏,就真的萬無一失了嗎煤搜?
帶著這些問題免绿,我們來分析一下Android常見的內(nèi)存泄漏場景以及解決方案。
二擦盾、Java內(nèi)存管理及垃圾回收機(jī)制
在了解Android的內(nèi)存泄漏之前嘲驾,我們需要先了解Java的內(nèi)存管理以及垃圾回收機(jī)制。
2.1 內(nèi)存管理
Java的內(nèi)存分配區(qū)域主要分為以下幾個部分迹卢。
1. 靜態(tài)變量區(qū)
用于存儲被static修飾的靜態(tài)變量辽故,這塊區(qū)域在程序開始運(yùn)行時就已經(jīng)分配完畢,并且存在于程序的整個運(yùn)行過程腐碱。
2. 棧
主要用于分配局部變量誊垢,包括基本類型的變量和對象的引用變量,當(dāng)局部變量的作用域結(jié)束之后,Java會自動釋放掉該變量占用的內(nèi)存空間喂走。
3. 堆
堆是動態(tài)內(nèi)存區(qū)域殃饿,程序運(yùn)行期間新建的對象實例和數(shù)組都存儲在堆中,垃圾回收機(jī)制(GC)管理的就是這塊內(nèi)存芋肠。為了及時地將不被使用的對象釋放掉乎芳,GC需要監(jiān)控每一個對象的狀態(tài),當(dāng)一個對象不再被引用時帖池,GC就會釋放該對象奈惑。
4. 常量池
常量池中的內(nèi)容在編譯時就已經(jīng)確定,主要包含代碼中的基本類型和對象類型的常量值碘裕。
例如携取,String就是對象類型,如果在編譯時確定了String的值(String s = "test"
)帮孔,那么它的值就存儲在常量池中雷滋,而它的引用存儲在棧中。如果String的值是在程序運(yùn)行時確定的(String s = new String("...")
)文兢,那么它的值就存儲在堆中晤斩。
假設(shè)當(dāng)前有一個實例A存儲在堆中,我們定義了一個引用a指向?qū)嵗鼳姆坚。此時引用a其實是保存在棧中的澳泵,它的值為實例A在堆內(nèi)存中的首地址,此時程序就可以通過a讀寫A的值兼呵。
2.2 垃圾回收機(jī)制
上面提到兔辅,當(dāng)一個對象不再被引用時,GC就應(yīng)該將其回收击喂。確實有一種引用計數(shù)法來判斷一個對象是否需要被釋放维苔,當(dāng)該對象的引用計數(shù)為0時代表它需要被回收。但是如果存在兩個對象懂昂,沒有別的引用指向它們介时,但是它們互相引用,此時它們的引用計數(shù)都不為0凌彬,導(dǎo)致無法釋放沸柔,容易造成內(nèi)存泄漏。
目前主流的的方法是通過可達(dá)性分析來判斷一個對象是否需要被釋放铲敛。該算法的基本思路就是通過一些被稱為引用鏈(GC Roots)的對象作為起點褐澎,從這些節(jié)點開始向下搜索,搜索走過的路徑被稱為(Reference Chain)原探,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節(jié)點到該節(jié)點不可達(dá))乱凿,則證明該對象是不可用的顽素。
在Java中咽弦,可作為GC Root的對象包括以下幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
三徒蟆、Android常見內(nèi)存泄漏場景
3.1 內(nèi)部類持有外部類引用造成的內(nèi)存泄漏
1. 非靜態(tài)內(nèi)部類
我們知道非靜態(tài)內(nèi)部類可以訪問外部類的變量,它通過變量this$0
隱式地持有外部類的引用型型,這個變量是編譯器為非靜態(tài)內(nèi)部類添加的段审,如果內(nèi)部類的生命周期超過外部類,則會引發(fā)內(nèi)存泄漏闹蒜。
造成這種情況的具體原因很多寺枉,可能是多線程或者監(jiān)聽器未反注冊。如果需要快速修復(fù)绷落,可以將內(nèi)部類改為static
姥闪,但是static
變量的生命周期與App相同,該變量不會被回收砌烁。因此最好是在出現(xiàn)內(nèi)存泄漏時筐喳,通過引用鏈尋找可以切斷的地方。后文的監(jiān)聽器和Handler
都屬于這種情況函喉。
2. 匿名內(nèi)部類
匿名內(nèi)部類引發(fā)內(nèi)存泄漏的原因與非靜態(tài)內(nèi)部類相似避归,匿名內(nèi)部類通過xxx$1.class
持有了外部類的引用,如果匿名內(nèi)部類的生命周期超過外部類管呵,在外部類例如Activity銷毀時梳毙,內(nèi)部類依舊持有外部類的引用,就會引發(fā)內(nèi)存泄漏捐下。
如果像下面這樣直接在匿名內(nèi)部類中使用Runnable
或者Handler
時就非常容易引起內(nèi)存泄漏账锹。由于Runnable
執(zhí)行的時間很可能超過Activity,Activity在onDestroy()
后匿名內(nèi)部類依舊存在坷襟,最終導(dǎo)致Activity泄露奸柬。
button.setOnClickListener(new View.OnClickListener() {
@override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
// ......
}
}).start();
}
});
匿名內(nèi)部類引發(fā)的內(nèi)存泄漏不易修改,因為沒有辦法獲得該對象的引用啤握,也就無法在Activity被銷毀時通過引用清除這些資源鸟缕。因此對于可能引發(fā)內(nèi)存泄漏的匿名內(nèi)部類來說,應(yīng)該改為內(nèi)部類實現(xiàn)排抬。
3.2 多線程造成的內(nèi)存泄漏
1. Runnable(Thread)
當(dāng)異步線程持有外部Activity的引用時懂从,如果Activity銷毀時線程還沒有執(zhí)行完,就會導(dǎo)致內(nèi)存泄漏蹲蒲。
解決辦法很簡單番甩,只需要在Activity銷毀之前終止線程即可。
2. AsyncTask
AsyncTask
是Handler
+Thread
的封裝届搁,用于完成異步任務(wù)缘薛。我們在使用時窍育,一般繼承AsyncTask
并重寫doInBackground()
方法和onPostExecute()
方法,doInBackground()
方法進(jìn)行耗時操作宴胧,onPostExecute()
方法在主線程更新UI漱抓。
其常見的內(nèi)存泄漏原因與Runnable
類似,也是由于AsyncTask
未執(zhí)行完時Activity被銷毀恕齐,而AsyncTask
又持有Activity的引用乞娄,導(dǎo)致Activity無法釋放,引起內(nèi)存泄漏显歧。
對于AsyncTask造成的內(nèi)存泄漏仪或,推薦使用cancel
+isCancelled
來解決。
如果一個任務(wù)沒有被執(zhí)行并且cancel
方法被調(diào)用士骤,那么任務(wù)會立即取消且不會被執(zhí)行范删。對于已經(jīng)在執(zhí)行的任務(wù),cancel
方法只能保證其onPostExecute()
不會被執(zhí)行拷肌,也就是說到旦,即使調(diào)用了cancel
方法,任務(wù)也不會立即停止廓块,需要等待doInBackground()
方法完成厢绝。cancel
方法不會終止一個正在運(yùn)行的線程,只是給它設(shè)置cancelled
狀態(tài)带猴,通知該線程應(yīng)該中斷了昔汉。
因此給任務(wù)調(diào)用cancel
方法后還要檢查當(dāng)前task的狀態(tài),保證其及時退出拴清。
@Override
protected Integer doInBackground(Void... args) {
// Task被取消了靶病,馬上退出
if(isCancelled()) return null;
.......
// Task被取消了,馬上退出
if(isCancelled()) return null;
}
雖然有這樣的解決辦法口予,但是對于異步操作娄周,這里更推薦RxJava。
3.3 視圖造成的內(nèi)存泄漏
1. WebView
在進(jìn)行混合開發(fā)時沪停,經(jīng)常需要在Activity中嵌入WebView
來訪問前端頁面煤辨,此時需要注意WebView
的創(chuàng)建和回收問題。
在Activity中使用WebView
時木张,推薦使用動態(tài)創(chuàng)建和回收的方式進(jìn)行管理众辨。在布局文件中定義一個ViewGroup
,然后動態(tài)地將WebView
添加到ViewGroup
中舷礼。
@override
protected void onCreate(Bundle savedInstanceState) {
mWebView = new WebView(this);
// WebView settings
mWebView.setWebViewClient(...);
mWebView.setWebChromeClient(...);
// 將 WebView 添加到布局中的 ViewGroup 中
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebViewLayout.addView(mWebView, layoutParams);
}
之后在Activity的onDestroy()
方法中回收WebView
相關(guān)資源鹃彻。由于WebView
內(nèi)部存在component callbacks,該回調(diào)在onAttachedToWindow()
方法中進(jìn)行注冊妻献,并在onDetachedFromWindow()
方法中進(jìn)行反注冊蛛株。為了順利反注冊該回調(diào)团赁,需要在WebView
執(zhí)行destroy()
之前將其從布局上移除。(具體見下方的參考2)
@override
protected void onDestroy() {
// 從父容器移除 WebView 后再將其銷毀
if (mWebView != null) {
mWebView.loadDataWithBaseURL(
null, "", "text/html", "utf-8", "");
mWebView.clearHistory();
mWebView.setWebViewClient(null);
mWebView.setWebChromeClient(null);
mWebViewLayout.removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
}
2. static view
如果某個View
在初始化時需要消耗大量資源谨履,并且要求其在Activity生命周期中不變欢摄,就可能將其修飾為static
加載到視圖樹上。由于View
在新建時就持有Activity的引用屉符,因此Activity銷毀時需要釋放資源剧浸。
public View(Context context) {
mContext = context; // 此時View已經(jīng)持有Activity的引用
// ......
}
面對這種情況锹引,最好是將View設(shè)置為普通變量矗钟,可以避免這類內(nèi)存泄漏。
3.4 廣播嫌变、監(jiān)聽器等未反注冊
這一類的內(nèi)存泄漏主要與觀察者模式有關(guān)吨艇,一般情況下是有多個觀察者(Observer)對同一個被觀察者(Observable)進(jìn)行監(jiān)聽。
如果有一個Manager對觀察者進(jìn)行統(tǒng)一管理的話腾啥,那么觀察者的對被觀察者監(jiān)聽的注冊與反注冊一定是成對出現(xiàn)的东涡,不然就會出現(xiàn)內(nèi)存泄漏谅年。在監(jiān)聽器一節(jié)中會詳細(xì)描述這種場景霍弹。
1. 廣播
廣播的主要流程如下:
1:廣播接收者BroadcastReceiver通過Binder機(jī)制向AMS(Activity Manager Service)進(jìn)行注冊
2:廣播發(fā)送者通過binder機(jī)制向AMS發(fā)送廣播
3:AMS查找符合相應(yīng)條件(IntentFilter/Permission等)的BroadcastReceiver,將廣播發(fā)送到BroadcastReceiver(一般情況下是Activity)相應(yīng)的消息循環(huán)隊列中
4:消息循環(huán)執(zhí)行拿到此廣播丸边,回調(diào)BroadcastReceiver中的onReceive()方法
根據(jù)上述流程凸舵,Activity在銷毀之前應(yīng)及時反注冊祖娘,否則廣播管理者會一直保留當(dāng)前Activity的引用,而廣播管理者的生命周期是整個Application啊奄,最終會導(dǎo)致內(nèi)存泄漏渐苏。
2. 監(jiān)聽器
上面提過,如果存在一個統(tǒng)一的Manager對監(jiān)聽器進(jìn)行管理的話菇夸,注冊和反注冊一定要成對出現(xiàn)琼富,否則很容易出現(xiàn)內(nèi)存泄漏的情況。下面來分析該場景庄新。
假設(shè)當(dāng)前存在一個監(jiān)聽器如下所示鞠眉。
public interface MyListener {
void run(...);
}
定義一個ListenerManager
來對所有的監(jiān)聽器進(jìn)行管理。
public class ListenerManager {
// 單例模式
private static final INSTANCE = new ListenerManager();
// 存儲所有的監(jiān)聽器
private List<MyListener> mListeners = new CopyOnWriteArrayList<>();
public static ListenerManager getInstance() {
return INSTANCE;
}
// 注冊監(jiān)聽器時將該監(jiān)聽器添加到列表中
public void registerListener(MyListener listener) {
if (listener == null) return;
if (mListeners.contains(listener)) return;
mListeners.add(listener);
}
// 反注冊時將該監(jiān)聽器從列表中移除
public void unRegisterListener(MyListener listener) {
if (listener == null) return false;
return mListeners.remove(listener);
}
public void run() {
for (MyListener listener : mListeners) {
listener.run(...);
}
}
}
在使用到該監(jiān)聽的Activity中添加如下代碼择诈。
public class TestActivity {
private TestListener mTestListener;
@override
protected void onCreate(...) {
// ...
mTestListener = new TestListener();
ListenerManager.getInstance().registerListener(mTestListener);
}
@override
protected void onDestroy() {
// ...
ListenerManager.getInstance().unRegisterListener(mTestListener);
}
private class TestListener implements MyListener {
@override
void run(...) {
// ...
}
}
}
可以看到械蹋,在Activity中使用了內(nèi)部類的形式定義了監(jiān)聽器,隨后在onCreate()
方法中注冊吭从,并在onDestroy()
中反注冊朝蜘。那么如果沒有反注冊會出現(xiàn)什么情況呢?
首先ListenerManager
的生命周期比Activity要長涩金,如果Activity未進(jìn)行反注冊谱醇,ListenerManager
中的mListeners
會一直持有TestListener
對象的引用暇仲,又因為TestListener
是內(nèi)部類,它持有Activity的引用副渴。
最終形成了ListenerManager->mListeners->mTestListener->Activity
的引用鏈奈附,導(dǎo)致Activity無法被釋放,形成了內(nèi)存泄漏煮剧。
3.5 其余情況
1. Handler
Handler
作為Android的一種消息機(jī)制斥滤,通過Handler
、Message
勉盅、MessageQueue
佑颇、Looper
四個類協(xié)調(diào)合作完成通信任務(wù)。
其中草娜,Message
是消息實體挑胸,包含硬件消息和軟件消息;
MessageQueue
是消息隊列宰闰,主要的功能是向消息池投遞消息和取走消息池的消息茬贵;
Handler
是輔助類,主要功能是向消息池發(fā)送消息事件(Handler.sendMessage()
)和處理相應(yīng)消息事件(Handler.handleMessage()
)移袍;
Looper
是循環(huán)機(jī)制解藻,不斷循環(huán)執(zhí)行將消息分發(fā)給目標(biāo)處理者。
如果我們在Activity中創(chuàng)建非靜態(tài)的Handler
實例并重寫handleMessage()
方法葡盗,此時Handler
隱式持有外部Activity的引用螟左,而MessageQueue
會持有Message
引用,Message
又持有Handler
引用(Message
需要知道自己會被發(fā)往哪個Handler
)戳粒。
也就是說路狮,如果Message
不被消費(fèi),Activity就不會被釋放蔚约,如果使用postDelayed
奄妨,在信息被消費(fèi)前關(guān)閉了Activity,就會造成內(nèi)存泄漏苹祟。
面對這種情況砸抛,最好是在Activity執(zhí)行onDestroy()
時調(diào)用Handler
的removeCallbacksAndMessages
清除所有信息;也可以選擇將Handler
定義為靜態(tài)內(nèi)部類树枫,這樣就不會持有外部Activity的引用了直焙。
2. 資源未關(guān)閉
資源性對象(比如Cursor、File等)往往都做了一些緩沖砂轻,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷奔誓,否則這些資源將不會被回收,造成內(nèi)存泄漏搔涝。
3. 工具類生命周期問題
有時代碼中會新建工具類用于完成一系列相同的操作厨喂,某些工具類在新建時需要傳入Context
和措,如下所示。
public class Utils {
private Context mContext;
public Utils(Context context) {
mContext = context;
}
}
有時候工具類對象是在Activity內(nèi)部新建的蜕煌,它的生命周期與Activity的生命周期相同派阱,那么即使它持有context
也不會引發(fā)內(nèi)存泄漏問題。但是如果工具類的生命周期比Activity長(如單例)斜纪,那么傳入了哪個Activity的context
贫母,哪個Activity就會泄露。
正確的做法是使用ApplicationContext
代替Context
盒刚,使得工具類的生命周期與APP相同腺劣,就不會引發(fā)Activity的內(nèi)存泄露。
不過如果該工具類只在某幾個場景下用到呢伪冰?如果它的生命周期還是整個APP誓酒,雖然沒有內(nèi)存泄漏,但也是浪費(fèi)了一部分內(nèi)存贮聂。這時候就需要開發(fā)人員對工具類的生命周期進(jìn)行管理,可以選擇在合適的時候清除該工具類對象寨辩。