我的CSDN: ListerCi
我的簡(jiǎn)書(shū): 東方未曦
一、引言
一般情況下Android的內(nèi)存泄漏是因?yàn)椋嬖谝弥赶蛞粋€(gè)本該被回收的對(duì)象,例如已經(jīng)執(zhí)行onDestroy()
的Activity。在這種情況下,由于Activity內(nèi)某些對(duì)象的生命周期比Activity要長(zhǎng),在Activity理論上被銷毀時(shí),該對(duì)象依舊存在并持有Activity的引用,因此內(nèi)存回收機(jī)制(GC)無(wú)法釋放Activity,最終導(dǎo)致內(nèi)存泄漏。
為了發(fā)現(xiàn)和修復(fù)APP中存在的內(nèi)存泄漏,開(kāi)發(fā)人員會(huì)在APP上安裝內(nèi)存檢測(cè)工具(如leakcanary),當(dāng)出現(xiàn)內(nèi)存泄漏時(shí),該工具會(huì)提供一個(gè)報(bào)告,里面包含了一條引用鏈,指明可能造成內(nèi)存泄漏的引用。開(kāi)發(fā)人員需要在合適的地方切斷引用鏈,以便GC釋放掉沒(méi)有被引用的對(duì)象。
有些內(nèi)存泄漏的修復(fù)很簡(jiǎn)單,將非靜態(tài)內(nèi)部類內(nèi)部類改為靜態(tài)內(nèi)部類或者將Context改為ApplicationContext后檢測(cè)工具就檢測(cè)不出內(nèi)存泄漏了,但是這到底是為什么呢?而且就算檢測(cè)工具檢測(cè)不出內(nèi)存泄漏,就真的萬(wàn)無(wú)一失了嗎?
帶著這些問(wèn)題,我們來(lái)分析一下Android常見(jiàn)的內(nèi)存泄漏場(chǎng)景以及解決方案。
二、Java內(nèi)存管理及垃圾回收機(jī)制
在了解Android的內(nèi)存泄漏之前,我們需要先了解Java的內(nèi)存管理以及垃圾回收機(jī)制。
2.1 內(nèi)存管理
Java的內(nèi)存分配區(qū)域主要分為以下幾個(gè)部分。
1. 靜態(tài)變量區(qū)
用于存儲(chǔ)被static修飾的靜態(tài)變量,這塊區(qū)域在程序開(kāi)始運(yùn)行時(shí)就已經(jīng)分配完畢,并且存在于程序的整個(gè)運(yùn)行過(guò)程。
2. 棧
主要用于分配局部變量,包括基本類型的變量和對(duì)象的引用變量,當(dāng)局部變量的作用域結(jié)束之后,Java會(huì)自動(dòng)釋放掉該變量占用的內(nèi)存空間。
3. 堆
堆是動(dòng)態(tài)內(nèi)存區(qū)域,程序運(yùn)行期間新建的對(duì)象實(shí)例和數(shù)組都存儲(chǔ)在堆中,垃圾回收機(jī)制(GC)管理的就是這塊內(nèi)存。為了及時(shí)地將不被使用的對(duì)象釋放掉,GC需要監(jiān)控每一個(gè)對(duì)象的狀態(tài),當(dāng)一個(gè)對(duì)象不再被引用時(shí),GC就會(huì)釋放該對(duì)象。
4. 常量池
常量池中的內(nèi)容在編譯時(shí)就已經(jīng)確定,主要包含代碼中的基本類型和對(duì)象類型的常量值。
例如,String就是對(duì)象類型,如果在編譯時(shí)確定了String的值(String s = "test"
),那么它的值就存儲(chǔ)在常量池中,而它的引用存儲(chǔ)在棧中。如果String的值是在程序運(yùn)行時(shí)確定的(String s = new String("...")
),那么它的值就存儲(chǔ)在堆中。
假設(shè)當(dāng)前有一個(gè)實(shí)例A存儲(chǔ)在堆中,我們定義了一個(gè)引用a指向?qū)嵗鼳。此時(shí)引用a其實(shí)是保存在棧中的,它的值為實(shí)例A在堆內(nèi)存中的首地址,此時(shí)程序就可以通過(guò)a讀寫(xiě)A的值。
2.2 垃圾回收機(jī)制
上面提到,當(dāng)一個(gè)對(duì)象不再被引用時(shí),GC就應(yīng)該將其回收。確實(shí)有一種引用計(jì)數(shù)法來(lái)判斷一個(gè)對(duì)象是否需要被釋放,當(dāng)該對(duì)象的引用計(jì)數(shù)為0時(shí)代表它需要被回收。但是如果存在兩個(gè)對(duì)象,沒(méi)有別的引用指向它們,但是它們互相引用,此時(shí)它們的引用計(jì)數(shù)都不為0,導(dǎo)致無(wú)法釋放,容易造成內(nèi)存泄漏。
目前主流的的方法是通過(guò)可達(dá)性分析來(lái)判斷一個(gè)對(duì)象是否需要被釋放。該算法的基本思路就是通過(guò)一些被稱為引用鏈(GC Roots)的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索走過(guò)的路徑被稱為(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)(即從GC Roots節(jié)點(diǎn)到該節(jié)點(diǎn)不可達(dá)),則證明該對(duì)象是不可用的。
在Java中,可作為GC Root的對(duì)象包括以下幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象
三、Android常見(jiàn)內(nèi)存泄漏場(chǎng)景
3.1 內(nèi)部類持有外部類引用造成的內(nèi)存泄漏
1. 非靜態(tài)內(nèi)部類
我們知道非靜態(tài)內(nèi)部類可以訪問(wèn)外部類的變量,它通過(guò)變量this$0
隱式地持有外部類的引用,這個(gè)變量是編譯器為非靜態(tài)內(nèi)部類添加的,如果內(nèi)部類的生命周期超過(guò)外部類,則會(huì)引發(fā)內(nèi)存泄漏。
造成這種情況的具體原因很多,可能是多線程或者監(jiān)聽(tīng)器未反注冊(cè)。如果需要快速修復(fù),可以將內(nèi)部類改為static
,但是static
變量的生命周期與App相同,該變量不會(huì)被回收。因此最好是在出現(xiàn)內(nèi)存泄漏時(shí),通過(guò)引用鏈尋找可以切斷的地方。后文的監(jiān)聽(tīng)器和Handler
都屬于這種情況。
2. 匿名內(nèi)部類
匿名內(nèi)部類引發(fā)內(nèi)存泄漏的原因與非靜態(tài)內(nèi)部類相似,匿名內(nèi)部類通過(guò)xxx$1.class
持有了外部類的引用,如果匿名內(nèi)部類的生命周期超過(guò)外部類,在外部類例如Activity銷毀時(shí),內(nèi)部類依舊持有外部類的引用,就會(huì)引發(fā)內(nèi)存泄漏。
如果像下面這樣直接在匿名內(nèi)部類中使用Runnable
或者Handler
時(shí)就非常容易引起內(nèi)存泄漏。由于Runnable
執(zhí)行的時(shí)間很可能超過(guò)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)存泄漏不易修改,因?yàn)闆](méi)有辦法獲得該對(duì)象的引用,也就無(wú)法在Activity被銷毀時(shí)通過(guò)引用清除這些資源。因此對(duì)于可能引發(fā)內(nèi)存泄漏的匿名內(nèi)部類來(lái)說(shuō),應(yīng)該改為內(nèi)部類實(shí)現(xiàn)。
3.2 多線程造成的內(nèi)存泄漏
1. Runnable(Thread)
當(dāng)異步線程持有外部Activity的引用時(shí),如果Activity銷毀時(shí)線程還沒(méi)有執(zhí)行完,就會(huì)導(dǎo)致內(nèi)存泄漏。
解決辦法很簡(jiǎn)單,只需要在Activity銷毀之前終止線程即可。
2. AsyncTask
AsyncTask
是Handler
+Thread
的封裝,用于完成異步任務(wù)。我們?cè)谑褂脮r(shí),一般繼承AsyncTask
并重寫(xiě)doInBackground()
方法和onPostExecute()
方法,doInBackground()
方法進(jìn)行耗時(shí)操作,onPostExecute()
方法在主線程更新UI。
其常見(jiàn)的內(nèi)存泄漏原因與Runnable
類似,也是由于AsyncTask
未執(zhí)行完時(shí)Activity被銷毀,而AsyncTask
又持有Activity的引用,導(dǎo)致Activity無(wú)法釋放,引起內(nèi)存泄漏。
對(duì)于AsyncTask造成的內(nèi)存泄漏,推薦使用cancel
+isCancelled
來(lái)解決。
如果一個(gè)任務(wù)沒(méi)有被執(zhí)行并且cancel
方法被調(diào)用,那么任務(wù)會(huì)立即取消且不會(huì)被執(zhí)行。對(duì)于已經(jīng)在執(zhí)行的任務(wù),cancel
方法只能保證其onPostExecute()
不會(huì)被執(zhí)行,也就是說(shuō),即使調(diào)用了cancel
方法,任務(wù)也不會(huì)立即停止,需要等待doInBackground()
方法完成。cancel
方法不會(huì)終止一個(gè)正在運(yùn)行的線程,只是給它設(shè)置cancelled
狀態(tài),通知該線程應(yīng)該中斷了。
因此給任務(wù)調(diào)用cancel
方法后還要檢查當(dāng)前task的狀態(tài),保證其及時(shí)退出。
@Override
protected Integer doInBackground(Void... args) {
// Task被取消了,馬上退出
if(isCancelled()) return null;
.......
// Task被取消了,馬上退出
if(isCancelled()) return null;
}
雖然有這樣的解決辦法,但是對(duì)于異步操作,這里更推薦RxJava。
3.3 視圖造成的內(nèi)存泄漏
1. WebView
在進(jìn)行混合開(kāi)發(fā)時(shí),經(jīng)常需要在Activity中嵌入WebView
來(lái)訪問(wèn)前端頁(yè)面,此時(shí)需要注意WebView
的創(chuàng)建和回收問(wèn)題。
在Activity中使用WebView
時(shí),推薦使用動(dòng)態(tài)創(chuàng)建和回收的方式進(jìn)行管理。在布局文件中定義一個(gè)ViewGroup
,然后動(dòng)態(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)行注冊(cè),并在onDetachedFromWindow()
方法中進(jìn)行反注冊(cè)。為了順利反注冊(cè)該回調(diào),需要在WebView
執(zhí)行destroy()
之前將其從布局上移除。(具體見(jiàn)下方的參考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
如果某個(gè)View
在初始化時(shí)需要消耗大量資源,并且要求其在Activity生命周期中不變,就可能將其修飾為static
加載到視圖樹(shù)上。由于View
在新建時(shí)就持有Activity的引用,因此Activity銷毀時(shí)需要釋放資源。
public View(Context context) {
mContext = context; // 此時(shí)View已經(jīng)持有Activity的引用
// ......
}
面對(duì)這種情況,最好是將View設(shè)置為普通變量,可以避免這類內(nèi)存泄漏。
3.4 廣播、監(jiān)聽(tīng)器等未反注冊(cè)
這一類的內(nèi)存泄漏主要與觀察者模式有關(guān),一般情況下是有多個(gè)觀察者(Observer)對(duì)同一個(gè)被觀察者(Observable)進(jìn)行監(jiān)聽(tīng)。
如果有一個(gè)Manager對(duì)觀察者進(jìn)行統(tǒng)一管理的話,那么觀察者的對(duì)被觀察者監(jiān)聽(tīng)的注冊(cè)與反注冊(cè)一定是成對(duì)出現(xiàn)的,不然就會(huì)出現(xiàn)內(nèi)存泄漏。在監(jiān)聽(tīng)器一節(jié)中會(huì)詳細(xì)描述這種場(chǎng)景。
1. 廣播
廣播的主要流程如下:
1:廣播接收者BroadcastReceiver通過(guò)Binder機(jī)制向AMS(Activity Manager Service)進(jìn)行注冊(cè)
2:廣播發(fā)送者通過(guò)binder機(jī)制向AMS發(fā)送廣播
3:AMS查找符合相應(yīng)條件(IntentFilter/Permission等)的BroadcastReceiver,將廣播發(fā)送到BroadcastReceiver(一般情況下是Activity)相應(yīng)的消息循環(huán)隊(duì)列中
4:消息循環(huán)執(zhí)行拿到此廣播,回調(diào)BroadcastReceiver中的onReceive()方法
根據(jù)上述流程,Activity在銷毀之前應(yīng)及時(shí)反注冊(cè),否則廣播管理者會(huì)一直保留當(dāng)前Activity的引用,而廣播管理者的生命周期是整個(gè)Application,最終會(huì)導(dǎo)致內(nèi)存泄漏。
2. 監(jiān)聽(tīng)器
上面提過(guò),如果存在一個(gè)統(tǒng)一的Manager對(duì)監(jiān)聽(tīng)器進(jìn)行管理的話,注冊(cè)和反注冊(cè)一定要成對(duì)出現(xiàn),否則很容易出現(xiàn)內(nèi)存泄漏的情況。下面來(lái)分析該場(chǎng)景。
假設(shè)當(dāng)前存在一個(gè)監(jiān)聽(tīng)器如下所示。
public interface MyListener {
void run(...);
}
定義一個(gè)ListenerManager
來(lái)對(duì)所有的監(jiān)聽(tīng)器進(jìn)行管理。
public class ListenerManager {
// 單例模式
private static final INSTANCE = new ListenerManager();
// 存儲(chǔ)所有的監(jiān)聽(tīng)器
private List<MyListener> mListeners = new CopyOnWriteArrayList<>();
public static ListenerManager getInstance() {
return INSTANCE;
}
// 注冊(cè)監(jiān)聽(tīng)器時(shí)將該監(jiān)聽(tīng)器添加到列表中
public void registerListener(MyListener listener) {
if (listener == null) return;
if (mListeners.contains(listener)) return;
mListeners.add(listener);
}
// 反注冊(cè)時(shí)將該監(jiān)聽(tīng)器從列表中移除
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)聽(tīng)的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)聽(tīng)器,隨后在onCreate()
方法中注冊(cè),并在onDestroy()
中反注冊(cè)。那么如果沒(méi)有反注冊(cè)會(huì)出現(xiàn)什么情況呢?
首先ListenerManager
的生命周期比Activity要長(zhǎng),如果Activity未進(jìn)行反注冊(cè),ListenerManager
中的mListeners
會(huì)一直持有TestListener
對(duì)象的引用,又因?yàn)?code>TestListener是內(nèi)部類,它持有Activity的引用。
最終形成了ListenerManager->mListeners->mTestListener->Activity
的引用鏈,導(dǎo)致Activity無(wú)法被釋放,形成了內(nèi)存泄漏。
3.5 其余情況
1. Handler
Handler
作為Android的一種消息機(jī)制,通過(guò)Handler
、Message
、MessageQueue
、Looper
四個(gè)類協(xié)調(diào)合作完成通信任務(wù)。
其中,Message
是消息實(shí)體,包含硬件消息和軟件消息;
MessageQueue
是消息隊(duì)列,主要的功能是向消息池投遞消息和取走消息池的消息;
Handler
是輔助類,主要功能是向消息池發(fā)送消息事件(Handler.sendMessage()
)和處理相應(yīng)消息事件(Handler.handleMessage()
);
Looper
是循環(huán)機(jī)制,不斷循環(huán)執(zhí)行將消息分發(fā)給目標(biāo)處理者。
如果我們?cè)贏ctivity中創(chuàng)建非靜態(tài)的Handler
實(shí)例并重寫(xiě)handleMessage()
方法,此時(shí)Handler
隱式持有外部Activity的引用,而MessageQueue
會(huì)持有Message
引用,Message
又持有Handler
引用(Message
需要知道自己會(huì)被發(fā)往哪個(gè)Handler
)。
也就是說(shuō),如果Message
不被消費(fèi),Activity就不會(huì)被釋放,如果使用postDelayed
,在信息被消費(fèi)前關(guān)閉了Activity,就會(huì)造成內(nèi)存泄漏。
面對(duì)這種情況,最好是在Activity執(zhí)行onDestroy()
時(shí)調(diào)用Handler
的removeCallbacksAndMessages
清除所有信息;也可以選擇將Handler
定義為靜態(tài)內(nèi)部類,這樣就不會(huì)持有外部Activity的引用了。
2. 資源未關(guān)閉
資源性對(duì)象(比如Cursor、File等)往往都做了一些緩沖,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷,否則這些資源將不會(huì)被回收,造成內(nèi)存泄漏。
3. 工具類生命周期問(wèn)題
有時(shí)代碼中會(huì)新建工具類用于完成一系列相同的操作,某些工具類在新建時(shí)需要傳入Context
,如下所示。
public class Utils {
private Context mContext;
public Utils(Context context) {
mContext = context;
}
}
有時(shí)候工具類對(duì)象是在Activity內(nèi)部新建的,它的生命周期與Activity的生命周期相同,那么即使它持有context
也不會(huì)引發(fā)內(nèi)存泄漏問(wèn)題。但是如果工具類的生命周期比Activity長(zhǎng)(如單例),那么傳入了哪個(gè)Activity的context
,哪個(gè)Activity就會(huì)泄露。
正確的做法是使用ApplicationContext
代替Context
,使得工具類的生命周期與APP相同,就不會(huì)引發(fā)Activity的內(nèi)存泄露。
不過(guò)如果該工具類只在某幾個(gè)場(chǎng)景下用到呢?如果它的生命周期還是整個(gè)APP,雖然沒(méi)有內(nèi)存泄漏,但也是浪費(fèi)了一部分內(nèi)存。這時(shí)候就需要開(kāi)發(fā)人員對(duì)工具類的生命周期進(jìn)行管理,可以選擇在合適的時(shí)候清除該工具類對(duì)象。