- 內(nèi)存泄漏的原因
- 常見的內(nèi)存泄漏與解決方法
- 檢測內(nèi)存泄漏
認(rèn)識內(nèi)存泄漏
根本原因就是當(dāng)一個(gè)對象理應(yīng)被回收的時(shí)候,因?yàn)樵谀硞€(gè)地方持有該對象的引用糠悼,導(dǎo)致它不能正常被 JVM 回收届榄,而停留在堆內(nèi)存中。
在 Android 中具體的例子大部分是:當(dāng)我們關(guān)閉了一個(gè) Activity/Fragment 時(shí)倔喂,此時(shí) Activity/Fragment 變?yōu)椴豢梢娐撂酰瑑?nèi)存中的實(shí)例也應(yīng)當(dāng)被回收,假如這時(shí)候還有別的對象實(shí)例強(qiáng)引用了 Activity/Fragment 的實(shí)例導(dǎo)致一直無法回收滴劲,則出現(xiàn)了內(nèi)存泄漏攻晒。
關(guān)于 JAVA 的內(nèi)存分配和回收,引用一段:
Java內(nèi)存劃分為棧班挖、堆鲁捏、方法區(qū)等區(qū)域,其中棧保存的是方法的局部變量萧芙,隨方法起隨方法滅给梅,不需要GC;
堆保存所有對象的實(shí)例和數(shù)組双揪,是GC和泄露的重點(diǎn)區(qū)动羽;
方法區(qū)保存的是類信息、常量渔期、靜態(tài)變量等靜態(tài)信息运吓,也需要GC。
堆內(nèi)存的回收中疯趟,判斷對象存活的算法有引用計(jì)數(shù)算法和可達(dá)性分析算法拘哨,引用計(jì)數(shù)算法無法解決對象間循環(huán)引用的問題,虛擬機(jī)通常采用可達(dá)性分析算法信峻。
常見的垃圾回收算法有:標(biāo)記 - 清除法倦青、復(fù)制算法、標(biāo)記 - 整理法盹舞、分代回收算法产镐。
常見的垃圾回收器種類有:Serial隘庄、ParNew、Parallel Scavenge等癣亚。
關(guān)于強(qiáng)引用:
對應(yīng)的常用概念還有軟引用丑掺、弱引用。
強(qiáng)引用特點(diǎn)是 JVM 即使內(nèi)存耗盡也不會(huì)去自動(dòng)回收該對象:
Object o = new Object();//強(qiáng)引用
而軟引用在內(nèi)存不足時(shí)述雾,會(huì)被 JVM 回收:
Staff bean = new Staff();//僅用于創(chuàng)建軟引用的實(shí)例
SoftReference<Staff> staffSR = new SoftReference<Staff>(bean);
//實(shí)際調(diào)用
String StaffID = staffSR.get().getId();
弱引用的使用和軟引用類似吼鱼,不同的是當(dāng) JVM 觸發(fā)了 GC 時(shí),不管當(dāng)前內(nèi)存空間足夠與否绰咽,都會(huì)被回收:
Staff bean = new Staff();//僅用于創(chuàng)建弱引用的實(shí)例
WeakReference<Staff> staffWR = new WeakReference<Staff>(bean);
//實(shí)際調(diào)用
String StaffID = staffSR.get().getId();
內(nèi)存泄漏的實(shí)例
日常的內(nèi)存泄漏其實(shí)追溯到最后還是間接或直接的持有了 Activity/Fragment 的實(shí)例,但是有些確實(shí)防不勝防地粪。一不注意就會(huì)踩雷取募。
- 單例造成的內(nèi)存泄露
這個(gè)就是老生常談了,因?yàn)閱卫纳芷谕瑧?yīng)用一樣長蟆技,又常有構(gòu)造方法中需要傳入 context 的情況玩敏。
這時(shí)候就要注意,如果傳入了 Activity 的 Context质礼,一不小心就會(huì)使這個(gè)單例持有了 Activity 的實(shí)例而出現(xiàn)內(nèi)存泄漏旺聚。
所以這種情況下,一般用 Application 的 Context 來構(gòu)造單例對象的實(shí)例眶蕉。
((Activity)context).getApplicationContext();
首先要回憶內(nèi)部類的特性砰粹,內(nèi)部類可以訪問外部類的實(shí)例。
對于 JVM 來說造挽,內(nèi)部類和外部類其實(shí)是兩個(gè)不同的類碱璃,正常來說兩個(gè)類之間調(diào)用方法自然是通過兩者的實(shí)例調(diào)用。
而非靜態(tài)內(nèi)部類之所以能訪問外部類的方法饭入,關(guān)鍵在于非靜態(tài)內(nèi)部類會(huì)默認(rèn)隱性的持有外部類的實(shí)例嵌器。
但靜態(tài)的內(nèi)部類則不會(huì)持有外部類的實(shí)例。
相關(guān)的典型代碼:
public class TestActivity extends Activity {
private final Handler mTestHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//TODO
}
}
}
這時(shí)候的 Handler 是非靜態(tài)的谐丢,所以該實(shí)例持有了 TestActivity 的實(shí)例爽航。
當(dāng)調(diào)用:
//延時(shí)發(fā)送消息
mTestHandler.postDelayed(new Runnable() {
@Override
public void run() {
//TODO
}
}, 1000);
如果不清楚 Handler 的原理和源碼的是時(shí)候去補(bǔ)習(xí)一下了。這時(shí)候 Handler 發(fā)送了 Message 實(shí)例乾忱,而這個(gè) Message 實(shí)例引用了 Handler 實(shí)例讥珍,同時(shí) Message 被主線程的 Looper 引用,此時(shí)的引用鏈:
Looper -> Message -> Handler -> Activity
這個(gè)時(shí)候即使調(diào)用 ((Activity)context).finish()
Activity 也不能被回收饭耳。
所以上面的情況下要將 Handler 轉(zhuǎn)化成靜態(tài)類即可串述,或者繼承 Handler 將隱性引用的 Activity 實(shí)例改為弱引用:
private static class MyHandler extends Handler {
private final WeakReference<TestActivity> mActivity;
public MyHandler(TestActivity activity) {
mActivity = new WeakReference<TestActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
TestActivity activity = mActivity.get();
if (activity != null) {
//TODO
}
}
}
和這個(gè)問題類似的還有線程造成的泄漏,兩者的原因都是因?yàn)橛昧朔庆o態(tài)的內(nèi)部類:
public class ThreadTestActivity extends Activity {
private class MyThread extends Thread {
@Override
public void run() {
super.run();
//TODO
}
}
關(guān)于非靜態(tài)內(nèi)部類還有另一個(gè)注意點(diǎn):
如果在非靜態(tài)內(nèi)部類中創(chuàng)建了一個(gè)靜態(tài)的實(shí)例寞肖,這個(gè)操作相當(dāng)于上面說的用 Activity 的實(shí)例來創(chuàng)建單例對象的實(shí)例:靜態(tài)實(shí)例會(huì)一直持有 Activity(外部類) 的實(shí)例纲酗。資源未關(guān)閉導(dǎo)致
Cursor衰腌、InputStream/OutputStream、File 等資源文件觅赊,如果僅僅是在使用結(jié)束后將引用賦為 null右蕊,而不調(diào)用關(guān)閉的方法,還是有可能會(huì)造成內(nèi)存泄漏吮螺。
特別是 Cursor饶囚,使用數(shù)據(jù)庫時(shí)如果沒有處理好可能會(huì)出現(xiàn) OOM 或 Could not allocate CursorWindow,就是因?yàn)闆]有關(guān)閉導(dǎo)致:
Cursor c ;
//TODO get Cursor
try {
c = query();
//TODO something
c.close();
//如果 try 中拋出異常,上面的 cursor.close() 很大可能不會(huì)執(zhí)行
} catch (Exception e) {
} finally{
//如果沒有 finally 塊會(huì)容易出錯(cuò)
if (c != null) {
c.close();
}
}
類似的還有廣播鸠补、EventBus 等需要注冊的同時(shí)也要記得在合適的地方注銷萝风。
這次負(fù)責(zé)的其中一個(gè)項(xiàng)目是運(yùn)行在固定的設(shè)備上:Android 4.4.2 的大平板上,所以這個(gè)坑是實(shí)實(shí)在在的踩下去了紫岩。
AlertDialog 中的監(jiān)聽回調(diào)都是靠 Handler 來實(shí)現(xiàn)的:
/*
* 以下代碼出自 android-23 - android - app - Dialog
*/
public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
//省略部分代碼
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
而我們使用 Dialog 時(shí) .setPositiveButton()
规惰、.setNegativeButton()
、.setNeutralButton()
都以非靜態(tài)內(nèi)部類的形式實(shí)現(xiàn)泉蝌,而這些點(diǎn)擊的事件同樣是通過 Handler 回調(diào)歇万,這就會(huì)將這些內(nèi)部類包裝成一個(gè) Message 傳給 Dialog,這個(gè) Message 就強(qiáng)引用了 Activity 的實(shí)例勋陪。
而 Dialog 在使用這些 Message 的時(shí)候會(huì)拷貝一個(gè)對象而不是用原來的對象:
private void sendDismissMessage() {
if (mDismissMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mDismissMessage).sendToTarget();
}
}
也就是說后面使用的是 Message 的拷貝贪磺。所以原來的 Message 從沒有被發(fā)送,因此不會(huì)被回收诅愚,所以永久保存著它的內(nèi)容寒锚,直到發(fā)生垃圾回收。
所以現(xiàn)在的引用鏈:
Thread(CookieSyncManager) -> Message -> AlertDialog$3(OnDismissListener) -> AlertDialog -> Activity
當(dāng)然 5.0 以上已經(jīng)解決了這個(gè)問題违孝,所以這個(gè)案例可以看一看就過壕曼。
檢測內(nèi)存泄漏
不管用什么工具和方法,檢測是否內(nèi)存泄漏的方法都依靠 heap dump 文件等浊。
heap dump 文件是一個(gè)二進(jìn)制文件腮郊,保存了某一時(shí)刻 JVM 堆中對象使用情況,就是生成文件時(shí)的 Java 堆棧的快照筹燕。我們可以選擇用 Heap Analyzer 分析 heap dump 文件轧飞,看哪些對象占用了太多的堆棧空間撒踪,或者哪些對象應(yīng)該被回收卻還在內(nèi)存中过咬。而 Leakcanary 等框架可以幫助我們省去一部分的工作。
- Leakcanary
build.gradle 中添加如下依賴:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
}
在 Application 初始化:
public class MyApplication extends Application {
private RefWatcher mWatcher;
@Override
public void onCreate() {
super.onCreate();
mWatcher = LeakCanary.install(this);//此時(shí)已經(jīng)可以檢測到 Activity 的內(nèi)存泄漏
}
public static RefWatcher getWatcher(Context context){
return ((MyApplication)context.getApplicationContext()).mWatcher;
}
}
檢測 Fragment :
@Override
public void onDestroy() {
super.onDestroy();
//watch() 也可以傳入別的對象實(shí)例來檢測是否泄露
MyApplication.getWatcher().watch(this);
}
Leakcanary 的 install(application)
相當(dāng)于在 Activity 的 onDestroy()
中調(diào)用 watch(this)
制妄。
其原理是在 Activity / Fragment 銷毀后掸绞,先手動(dòng)促發(fā)一次 GC(系統(tǒng) GC 并不會(huì)在銷毀后立刻發(fā)生)
如果 watch(Object bean)
中傳入的 bean 實(shí)例依然存在在內(nèi)存中,則 dump heap 到本地,
dump 完成后啟動(dòng) HeapAnalyzerService 服務(wù)讀取本地 dump下來的文件衔掸,使用 HAHA 庫進(jìn)行分析烫幕。
如果檢測到內(nèi)存泄漏,將結(jié)果返回給 DisplayLeakService 服務(wù)敞映,并且彈窗顯示通知较曼。
- Android Studio
在不想額外的依賴 Leakcanary 等框架是,利用 AS 自帶的 Monitor 同樣可以檢測內(nèi)存泄漏振愿,只是多了一些步驟捷犹。
Android Monitor -> Monitor
從左到右:
Initiate GC // 手動(dòng)觸發(fā) GC。
Jump java heap// 獲取 hprof 分析文件冕末。
Start Allocation Tracking// 開始分配追蹤萍歉。
在 Jump java heap 之前還是要記得觸發(fā)一次 GC。點(diǎn)擊后會(huì)生成一個(gè) 后綴為 hprof
的文件档桃,在 AS 打開:
右邊的 Analyzer Tasks
打開分析窗口:
右上角開始翠桦,在 Results 可以看到分析結(jié)果。