一個無用的對象奖恰,被另一個對象所持有應(yīng)用吊趾,造成該對象在虛擬機的堆中占有的內(nèi)存無法釋放,而導(dǎo)致內(nèi)存空間的浪費瑟啃,這種情況就是內(nèi)存泄漏论泛。
情況1: 單例持有Context
如下代碼:
public class TestManager {
private static TestManager mInstance;
private Context mContext;
public static TestManager getInstance(Context context) {
if (mInstance == null) {
mInstance = new TestManager(context);
}
return mInstance;
}
private TestManager(Context context) {
mContext = context;
}
}
分析: 上述代碼中,如果傳入Activity的Context蛹屿,此時TestManager就持有了Activity的引用屁奏,因為mInstance是個靜態(tài)對象,生命周期和整個應(yīng)用一樣長错负,所以如果Activity退出坟瓢,Activity對象占有的內(nèi)存并不能被回收,就會導(dǎo)致內(nèi)存泄漏犹撒。
方案: 此時應(yīng)該傳入全局的上下文Application Context折联,就能避免內(nèi)存泄漏。
情況2: 匿名內(nèi)部類
匿名內(nèi)部類默認持有外部類的引用识颊,如果匿名內(nèi)部類生命周期長于外部類诚镰,就會導(dǎo)致內(nèi)存泄漏,經(jīng)典的例子就是Handler。
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相應(yīng)邏輯
}
}
};
分析: 因為Handler在post消息時清笨,會將自己本身作為成員變量保存在發(fā)送的Msg中月杉,目的是方便去識別哪個Handler去處理消息。即message持有handler的引用抠艾,handler持有外部類(可能是Activity)的引用沙合,而message是在MessageQueue中等待Looper輪詢處理的,有可能Activity退出跌帐,而Message并沒有被處理首懈,就會導(dǎo)致Activity間接被Message引用,從而造成內(nèi)存泄漏谨敛。
方案1:使用靜態(tài)內(nèi)部類 + 弱引用組合
因為靜態(tài)內(nèi)部類不持有外部類應(yīng)用究履,所以如果想更新外部類,可以弱引用脸狸。代碼如下:
public static class MyHandler extends Handler {
private WeakReference<MainActivity> reference;
public MyHandler (Activity activity) {
reference = new WeakReference<> (activity);
}
public void handlerMessage(Message msg) {
MainActivity activity = reference.get();
// 通過activity調(diào)用外部類方法
}
}
方案2: 在Activity退出時最仑,清空所有Handler發(fā)生出去的消息。
@Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
情況3: 廣播注冊后未取消
情況4: Time和TimerTask的使用炊甲,未及時取消
很多情況寫一些定時器或者輪詢?nèi)蝿?wù)時泥彤,會用到Timer,參考如下代碼:
public void timerTest() {
mTimer = new Timer();
mTimerTask = new TimerTask {
@Override
public void run() {
// do something
}
}
mTimer.schedule(mTimerTask, 3000,3000);
}
分析: task是個匿名內(nèi)部類卿啡,同樣默認持有外部類引用吟吝,而且task是定時執(zhí)行的,所以Activity退出時颈娜,有可能task并沒有被執(zhí)行剑逃,就會導(dǎo)致內(nèi)存泄漏。
方案: 在Activity退出時官辽,及時取消掉Task蛹磺;
@Override
public void onDestroy() {
super.onDestroy();
mTimer.cancel();
mTimer = null;
mTimerTask.cancel();
mTimerTask = null;
}
情況5: 資源未及時關(guān)閉,使用 IO同仆、File 流或者 Sqlite萤捆、Cursor時,通常都會使用緩存俗批,如果不及時關(guān)閉俗或,緩存對象就會一直被占用,導(dǎo)致內(nèi)存泄漏扶镀;
情況6: 動畫未及時取消蕴侣。Activity中使用了屬性動畫Animator,動畫中有引用控件臭觉,而控件引用Activity昆雀,若銷毀時沒有及時cancel辱志,會導(dǎo)致Activity一直被引用,無法回收狞膘,從而造成內(nèi)存泄漏揩懒;
情況7: WebView 的內(nèi)存泄露,因為 WebView 在加載網(wǎng)頁后會長期占用內(nèi)存而不能被釋放挽封,因此我 們在 Activity 銷毀后要調(diào)用它的 destory()方法來銷毀它以釋放內(nèi)存已球。
====================== 續(xù) ========================
情況8: DialogFragment引起的內(nèi)存泄漏。
在寫彈框時辅愿,官方推薦使用DialogFragment智亮,但是在彈框消失時,會報DialogFragment內(nèi)存泄漏点待,且引用鏈的上一級是onCancelListener阔蛉,LeakCanary報錯如下:
┬───
│ GC Root: Java local variable
│
├─ android.os.HandlerThread thread
│ Leaking: UNKNOWN
│ Thread name: 'OkHttpProfiler'
│ ↓ HandlerThread.<Java Local>
│ ~~~~~~~~~~~~
├─ android.os.Message instance
│ Leaking: UNKNOWN
│ ↓ Message.obj
│ ~~~
├─ androidx.fragment.app.DialogFragment$2 instance
│ Leaking: UNKNOWN
│ Anonymous class implementing android.content.DialogInterface$OnCancelListener
│ ↓ DialogFragment$2.this$0
│ ~~~~~~
╰→ ***.PrivacyPolicyDialogFragment instance
Leaking: YES (ObjectWatcher was watching this because
***.PrivacyPolicyDialogFragment
分析: DialogFragment被泄漏,引用鏈上一條在于onCancelListener癞埠,看源碼:
// DialogFragment.java
DialogInterface.OnCancelListener mOnCancelListener = new DialogInterface.OnCancelListener() {
@Override
public void onCancel(@Nullable DialogInterface dialog) {
if (mDialog != null) {
// 匿名內(nèi)部類状原,持有對外部類DialogFragment的引用
DialogFragment.this.onCancel(mDialog);
}
}
};
// DialogFragment.java
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 省略代碼
mDialog.setOnCancelListener(mOnCancelListener);
mDialog.setOnDismissListener(mOnDismissListener);
}
//Dialog.java
public void setOnCancelListener(@Nullable OnCancelListener listener) {
// 省略代碼
if (listener != null) {
/**
* 劃重點: listener帶有DialogFragment的引用,此時創(chuàng)建mCancelMessage苗踪,
* 使用obtainMessage方法颠区,在對象池中獲取Message,并將listener賦值給msg的obj屬性通铲,
* 所以此時mCancelMessage持有了DialogFragment的引用
*/
mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
} else {
mCancelMessage = null;
}
}
此時的引用鏈為:
Dialog.mCancelMessage -> DialogFragment. mCancelListener -> DialogFragment 毕莱。
所以只能是mCancelMessage泄漏了,才能引發(fā)DialogFragment泄漏测暗。
根據(jù)leakCanary截圖顯示央串,是HandlerThread持有了Message。
問:mCancelMessage是如何被HandlerThread持有的碗啄?
答:mCancelMessage被創(chuàng)建之前。
看看劃重點的地方稳摄,Message是在對象池中拿到的稚字,在Message對象被Handler處理之后,會被recycle厦酬,放入對象池胆描。但是,如果此時是最后一個Message仗阅,由于Looper獲取Message的方法昌讲,是一個阻塞方法,Looper會一直阻塞减噪,并持有msg的引用短绸。
即:
阻塞狀態(tài)的Looper车吹,一定會持有最后一個Message的引用
看代碼:
// Looper.java
public static void loop() {
//省略代碼
for (;;) {
/**
* 如果沒有消息,next()方法會一直阻塞在這里醋闭,而此時上一個msg窄驹,
* 會一直被Looper持有,Looper被HandlerThread持有
*/
Message msg = queue.next(); // might block
}
// 省略代碼
}
所以证逻,及時Message被回收放入了對象池乐埠,也會被HandlerThread持有。此時的引用鏈為:
HanlderThread -> Looper -> mCancelMessage ->DialogFragment. mCancelListener -> DialogFragment
一切都真相大白了囚企。
方案:等等哈