最近項目開發中,開發人員和測試人員均反應在android5.0以下手機上LeakCanary頻繁監控到內存泄漏,如下圖所示,但凡用到Dialog或DialogFragment地方均出現了內存泄漏。
如上圖所示,存在一個Message實例的obj成員變量,間接引用着Activity的實例,導致Activity無法正常退出。通過Android Monitors內存快照分析,確實有Message實例持有對LoadingDialogFragment的引用,進而導致Activity也無法正常銷毀,出現內存泄漏(如下圖)。
參考一個內存泄漏引發的血案一文,了解到問題發生原因:局部變量的生命周期在Dalvik VM跟ART/JVM中有區別。在DVM中,假如線程死循環或者阻塞,那么線程棧幀中的局部變量假如沒有被置為null,那么就不會被回收。 在 VM 中,每一個棧幀都是本地變量的集合,而垃圾回收器是保守的:只要存在一個存活的引用,就不會回收它。在每次循環結束后,本地變量不再可訪問,然而本地變量仍持有對 Message 的引用,interpreter/JIT 理論上應該在本地變量不可訪問時將其引用置為 null,然而它們並沒有這樣做,引用仍然存活,而且不會被置為 null,使得它不會被回收。
1、例如HandlerThread中,Looper會不停的從阻塞隊列MessageQueue中取Message進行處理。當沒有可消費Message對象時,就會開始阻塞,而此時最后一個被取出的Message就會被本地變量引用,一直不會釋放引用,哪怕Message已經被recycler(僅僅是清理了內容並放回消息隊列)。其實到這一步,只是一個空殼的Message被泄漏,無法回收,畢竟Message實例的內容還是被清理了(demo中的SecondActivity模擬了沒有recycler時的泄漏情況,適用於自己實現類似HandlerThread時需要注意的情況)。
2、在Dialog源碼中,我們可以看到如下代碼片段,包括setOnCancelListener、setOnDismissListener在內的方法,其實都是將設置進來的listener對象(listener對象包含對Activity的引用)放到一個從消息隊列中拿到的Message實例中,將listener賦給了Message實例的obj變量。例如mShowMessage,mShowMessage會一直保存這個Message實例,不會再放回消息隊列中,因為在sendShowMessage時,Dialog是從消息隊列中再次obtain一個Message實例,復制mShowMessage內容進行發送。當然前面這些也不會存在什么問題,mShowMessage也會在Dialog銷毀時跟着銷毀。
綜合1與2,分開來看,一般情況下大家互不干擾。但兩者碰撞在一起時,問題就來了。Dialog從消息隊列中可能會恰巧取到一個“仍然被某個阻塞中的HandlerThread本地變量引用的Message實例”,然后把listener賦給Message的obj,並一直保存在Dialog實例中(例如mShowMessage),這樣內存泄漏就發生了。就算Dialog銷毀,本地變量仍然引用保持着對Message的引用,導致obj變量的指向的listener無法回收,listener又包含對Activity的引用,導致Activity也無法正確回收。
在這種情況下,除非HandlerThread收到新的Message處理,而給本地變量重新賦值從而切斷了對上一個Message引用,否則會一直內存泄漏。
public void setOnShowListener(@Nullable OnShowListener listener) { if (listener != null) { mShowMessage = mListenersHandler.obtainMessage(SHOW, listener); } else { mShowMessage = null; } }
private void sendShowMessage() { if (mShowMessage != null) { // Obtain a new message so this dialog can be re-used Message.obtain(mShowMessage).sendToTarget(); } }
解決方案:我們可以通過提供一個DialogInterface.OnCancelListener的包裝類(Dialog其他listener也一樣可行),僅包含對真正listener的引用,當Dialog退出后,解除對listener的引用。還有一個辦法就是在Handler空閑時發送一個空Message,當然處理Dialog Message的Handler我們無法直接控制(在Dialog內部的私有變量),所以采用包裝類方法解決。
public final class DetachableDialogCancelListener implements DialogInterface.OnCancelListener { public static DetachableDialogCancelListener wrap(DialogInterface.OnCancelListener delegate) { return new DetachableDialogCancelListener(delegate); } private DialogInterface.OnCancelListener delegateOrNull; private DetachableDialogCancelListener(DialogInterface.OnCancelListener delegate) { this.delegateOrNull = delegate; } @Override public void onCancel(DialogInterface dialog) { if (delegateOrNull != null) { delegateOrNull.onCancel(dialog); delegateOrNull = null; } } public void clearOnDetach(Dialog dialog) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { dialog.getWindow() .getDecorView() .getViewTreeObserver() .addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() { @Override public void onWindowAttached() { } @Override public void onWindowDetached() { if (delegateOrNull != null) { delegateOrNull.onCancel(dialog); delegateOrNull = null; } } }); } } }
如下圖所示,通過對內存進行快照,看到確實達到了我們的目的。
當然,問題並沒有因此而結束,當我將所有設置了setOnCancelListener等監聽事件的地方都用包裝類處理后,仍然收到了LeakCanary的內存泄漏通知。到底是怎么回事呢?通過一番debug,發現在DialogFragment的onActivityCreated中,設置過setOnCancelListener和setOnDismissListener,當自己再去設置時,還是會發生內存泄漏。其實問題就出在默認的設置,雖然我們重新設置了,但在執行默認設置時,仍然有可能會恰巧取到一個“仍然被某個阻塞中的HandlerThread本地變量引用的Message實例”,就算后面被重新設置了,但包含默認listener設置的Message仍然還被HandlerThread的本地變量引用,所以也就內存泄漏了。
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (!mShowsDialog) { return; } View view = getView(); if (view != null) { if (view.getParent() != null) { throw new IllegalStateException( "DialogFragment can not be attached to a container view"); } mDialog.setContentView(view); } final Activity activity = getActivity(); if (activity != null) { mDialog.setOwnerActivity(activity); } mDialog.setCancelable(mCancelable); mDialog.setOnCancelListener(this); mDialog.setOnDismissListener(this); if (savedInstanceState != null) { Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); if (dialogState != null) { mDialog.onRestoreInstanceState(dialogState); } } }
至此,問題既然出在DialogFragment的onActivityCreated默認設置上,那么如果能取消默認的設置,就不會發生內存泄漏。上面這段代碼是DialogFragment的源碼,不能修改,而super.onActivityCreated又必須調用。如何解決呢?看上面代碼的第5行,通過調用setShowsDialog將mShowDialog設置為false,這樣super.onActivityCreated就等於不會執行剩余代碼邏輯了。在自己的onActivityCreated中,自行實現super類中本應執行的代碼邏輯(copy即可),然后將setOnCancelListener和setOnDismissListener通過包裝類進行設置,我這里是直接刪除了這兩行代碼,由繼承自BaseDialogFragment的子類自行設置。
public class BaseDialogFragment extends DialogFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { boolean isShow = this.getShowsDialog(); this.setShowsDialog(false); super.onActivityCreated(savedInstanceState); this.setShowsDialog(isShow); View view = getView(); if (view != null) { if (view.getParent() != null) { throw new IllegalStateException( "DialogFragment can not be attached to a container view"); } this.getDialog().setContentView(view); } final Activity activity = getActivity(); if (activity != null) { this.getDialog().setOwnerActivity(activity); } this.getDialog().setCancelable(this.isCancelable()); if (savedInstanceState != null) { Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState"); if (dialogState != null) { this.getDialog().onRestoreInstanceState(dialogState); } } } else { super.onActivityCreated(savedInstanceState); } } }
至此,Dialog和DialogFragment在Android5.0以下的內存泄漏問題均得以解決。但該方案並不完美,能夠解決內存泄漏的關鍵,還是通過監聽OnWindowAttachListener,在Dialog退出時切斷Message實例與真正listener對象的關聯。但OnWindowAttachListener需要level18,所以。。。如果有什么好的低版本同樣實現,煩請告知,感謝!
如果是使用DialogFragment,可以在onDestory中切斷Message實例與真正listener對象的關聯。
補充,本文一直在重點分析Dialog如何因為Message產生內存泄漏。而事實上,自己寫的HandlerThread中,如果是Android5.0以下,一定要在取出Message用完后,將Message置為null,並且要防止被編譯器優化掉,否則也會因為HandlerThread阻塞后,導致Message無法正確釋放包含的內容,產生內存泄漏。(可運行本文給出的demo,重現問題)。
demo運行后,打開SecondActivity,發送Message,然后返回,此時Activity應該被銷毀,但LeakCanary會提示內存泄漏。將SecondActivity的Handler中取出的msg用完后置為null即可解決。而FourActivity模擬了HandlerThread發生泄漏的情況,可以嘗試用本文提出的辦法解決,Demo中給出了通過發送一個空消息,回收本地變量引用的Message實例。