Android中導致內存泄漏的竟然是它----Dialog

AntonyMacle 7年前發布 | 21K 次閱讀 安卓開發 Android開發 移動開發

一,內存泄漏的 Bug 猛增

最近接入了公司組件分析云,在 App 進行 mokey 測試的時候順便檢測內存泄漏問題。于是,就在前天的測試中,樓主一瞬間收到了4個這樣的 Bug 單,瞬間心理無比糾結,真有千萬只羊駝向我奔來。

登錄頁面出現內存泄漏??!!樓主的代碼是如此的完美而無懈可擊,這么可能出現這么多泄漏的問題?分析云測漏的工具有問題吧!?

插播什么是 Activity 泄漏:Android 中 Activity 代表一個頁面,擁有一段生命周期,生命周期結束后,Activity 對象應當在之后某個合適的時機被 VM 回收內存。出現了泄漏就意味著 Activity 生命周期結束后,VM 發現 Activity 一直被持有,沒有回收這些無用的內存。

按照以往的經驗,大部分 Activity 泄漏的原因都是由于 Handler 內部類長時間掛在線程中導致的。而這塊我們 App 已經考慮便處理了。究竟是哪泄漏了?

二,WebView導致內存泄漏眾所周知

帶著懷疑的心態并且為了證明清白,我一個個點進去看了,總共有三條不同的引用鏈。為了后續說明,這里取了個名字:

① AuthDialog 引用鏈

② BrowserFrame 引用鏈

③ IClipboradDataPaste 引用鏈

看來這次情況有點不同!由于Monkey測試的機型比較少,這里所有的Bug都來自一部三星GT-I9300@android+4.3手機。 為了快速解決問題,樓主詢問了其他同事和StackOverflow,發現這其中有三個類CookieSyncManager, WebView, WebViewClassic已經被很多人提起過,它們會導致內存泄漏!初步有如下的結論如下:

1.CookieSyncManager是個全局靜態單例,操作系統內部使用了App的Activity作為Context構造了它的實例。我們應該在App啟動的時候,搶先幫系統創建這個單例,而且要用applicationContext,讓它不會引用到Activity。

2.使用WebView的頁面(Activity),在生命周期結束頁面退出(onDestory)的時候,需要主動調用WebView.onPause()以及WebView.destory()以便讓系統釋放WebView相關資源。

3.IClipboardDataPasteEventImpl是三星手機才有的類,這個東西還會讓EditText也發生內存泄漏!

4.WebView內存泄漏是眾所周知的,建議另外啟動一個進程專門運行WebView。不要9998,不要9999,我們要100%!WebView用完之后就把進程殺死,即使泄漏了也無礙。

按照以上的種種結論,我們都認定了這里面就是WebView引起的。 但是!我們的應用主進程LoginActivity根本沒有用到WebView啊!!!

三,第三方jar包使用WebView這可如何是好

根據以上的AuthDialog引用鏈,樓主把目標鎖定了某SDK:

翻了一陣子惡心的混淆后的代碼,找到下面這么一段。SDK確實創建了WebView實例,并且用的是客戶程序的Activity對象作為WebView的Context如下:

c 跟 j 都是SDK中繼承于WebView的一個子類,k 是登錄接口的輸入參數Activity。這里創建了 c 對象之后向上塑形賦給了 j 。

網上已經有很多例子表明,直接用Activity作為參數構建WebView就非常有可能導致Activity泄漏。

不過也看到了代碼中,有調用了WebView的destory()方法釋放資源。但是這里似乎無法保證dismiss()一定會被執行。

問題到這里發現比較麻煩了,openSDK對我們來說是第三方包,我們沒法讓第三方包不用WebView,或者讓第三方包把WebView放在另外一個進程中運行啊!于是,在App上面做規避暫時不好實現。于是找了SDK的童鞋一起分析了。

最終,大家都有了一個初步的共識,在Android4.3以下的舊版本,使用Activity對象創建WebView,確實有可能導致內存泄漏。非常高興能得到SDK童鞋的大力支持,一起分析,問題到這里有了初步的進展。

四,心結未解,翻看WebView源碼了解根源

不過,問題到這里樓主心理還是有個很嚴重的疑惑沒有解開(是什么疑惑呢?)。于是拿了Android4.3的源碼又翻了一遍希望找尋這里頭的根本原因,做了一點記錄,針對WebView在Java層的結構畫了一個不嚴謹的類圖:源碼來源: http://androidxref.com/4.3_r2.1/

大概情況是這樣:WebView這套結構中,有一個工廠類WebViewFactory提供靜態方法。

Android4.3(JellyBean)版本通過WebViewFactory工廠類創建了一個全局單例對象WebViewClassic$Factory,然后使用這個Factory創建了一整套實現的代碼(XXXClassic):WebViewClassic, CookieManagserClassic, WebViewDatabaseClassic。

WebViewClassic才是真正地實現WebView的各種API。WebViewClassic創建并維護了WebViewCore對象。

WebViewCore創建了一個子線程“WebViewCoreThread”,這里是一個全局的單例的而且一旦啟動就不會停止的Thread!WebViewCore會在這個子線程中創建維護并調用BrowserFrame的方法。

BrowserFrame本身是一個屬于“WebViewCoreThread”線程的Handler子類。BrowserFrame會被native(c++)層調用,然后將這些調用切換到”WebViewCoreThread”線程中去執行,比如刷新進度或者處理屏幕旋轉事件等等。

BrowserFrame還會調用CookieSyncManager.createIntance(),這也是系統框架中唯一一處調用的地方!

看到這里之后,樓主發現以上所說的,提前幫系統調用 CookieSyncManager.createInstance(contenxt.getApplicationContext()) 可能是沒有效果的,因為系統本來就是這么做的。手機廠商修改這里的可能性不大。

CookieSyncManager又是什么東西?同樣的,它自己也創建一個子線程,線程名就叫“CookieSyncManager”,又是全局單例不會停!這個線程每過5分鐘就會把緩存在內存中的Cookie進行持久化syncFromRamToFlash()。

這里我們比較關心為什么Activity會泄漏,所以關鍵看看哪些類對象中持有了Activity(Context)引用:WebViewClassic, WebViewCore, BrowserFrame。 這套結構中有很多靜態單例,還有子線程,想想也挺惡心的。而且三個關鍵的類都持有Activity引用。不過我們發現,其實WebViewClassic, WebViewCore這兩個個對象跟WebView對象的生命周期是一致的,Activity銷毀于是WebView銷毀了,WebView銷毀了另外兩個對象也跟著銷毀。煙消云散。。。

留下兩個孤獨的子線程還在跑,還有全局靜態的釘子戶對象。

但是!BrowserFrame本身是Handler,假如它因為native層的調用往”WebViewCoreThread”掛了一個消息,那么便可以建立一條引用鏈: Thread->MessageQueue->Message->Handler(BrowserFrame)->Activity

好了,樓主的疑惑是什么?

五,最后的疑惑

我們再來看看AuthDialog的引用鏈。

換成MAT看會比較清晰:

樓主發現,這里CookieSyncManager線程,居然直接引用了Message對象!這是什么鬼?一般情況下,HandlerThread持有一個MessageQueue對象,MessageQueue才持有Message隊列。

Java Local : A local variable. For example, input parameters, or locally created objects of methods that are still in the stack of a thread. Native stack. Input or output parameters in native code, for example user-defined JNI code or JVM internal code. Many methods have native parts, and the objects that are handled as method parameters become garbage collection roots. For example, parameters used for file, network, I/O, or reflection operations. 這里表明,CookieSyncManager線程中存在某個Message的局部變量,而由于線程一直沒有結束,所以局部變量一直沒有被釋放。而這個Message.obj成員引用了AuthDialog$3對象。 這是一個內部類,樓主發現內部類混淆之后的命名規則就是:第幾個出現就命名為幾。

AuthDialog里面有很多內部類:

如上圖,MAT中的引用鏈中的AuthDialog$3指的就是這里的OnDismissListener匿名內部類!接著我們來看看Dialog.setOnDismissListener里面做了什么勾搭:

納尼!OnDismissListener居然被賦給了Message.obj成員!

于是,我們心中生成的一條引用鏈是這樣的: Thread(main) -> MessageQueue->Message -> obj(OnDismissListener) -> AuthDialog -> Activity

可是不對啊,我們所能找到的引用鏈跟CookieSyncManager子線程一點關系都沒有! 再對比一下:

子線程CookieSyncManager拿到了主線程的Message!! Oh no !! 這是什么情況???這個Message被某處地方錯誤引用了?子線程通過JNI在native中拿到Java層的對象?

好吧,樓主承認研究了一個晚上沒有任何進展。。。

有時候熬夜跟進問題真的很容易卡死在某個地方,腦子遲鈍了短路了,效率降低了。。。今天還要黑著眼圈來上班。中午跟同事討論了一下排除了一些不可能存在的情況,然后繼續在StackOverFlow上面游蕩(假如有其他不錯的程序員社區類似StackOverFlow這種的,麻煩親們分享給樓主,感激不盡!)

六,原來是它!–Dialog

注:以下的分析感悟來自Github上面的一篇文章:《一個內存泄漏引發的血案》 有興趣的童鞋請閱讀原文

這里簡要說明一下,作者的結論是:在 Android Lollipop 之前使用 AlertDialog 可能會導致內存泄漏! 作者發現,局部變量的生命周期在Dalvik VM跟ART/JVM中有區別。在DVM中,假如線程死循環或者阻塞,那么線程棧幀中的局部變量假如沒有被置為null,那么就不會被回收。

如下代碼使用阻塞隊列說明問題:

子線程中調用loop()死循環,不停地從阻塞隊列中取出一個MyMessage對象并且將對象的引用賦值給局部變量message,一次while循環之后,虛擬機應當結束while花括號中的局部變量的生命周期,并且釋放對應的堆內存中的MyMessage對象。可是,DVM沒有這么做!!

在 VM 中,每一個棧幀都是本地變量的集合,而垃圾回收器是保守的:只要存在一個存活的引用,就不會回收它。在每次循環結束后,本地變量不再可訪問,然而本地變量仍持有對 Message 的引用,interpreter/JIT 理論上應該在本地變量不可訪問時將其引用置為 null,然而它們并沒有這樣做,引用仍然存活,而且不會被置為 null,使得它不會被回收!!

這種場景不就是Android Handler消息機制的處理方式么?!

Looper不停地從阻塞隊列MessageQueue中取出下一條消息Message并將引用賦給本地變量msg。一旦一次循環結束了,msg沒有被置為null,對應的Message對象沒有被回收,于是就泄漏了。

不過,Message是自帶回收機制的,而且任何線程共用,從上面源碼可以看到,每個Message被Handler處理完之后都會recycle(),置空所有的成員變量,并且放到回收池中。

好了,被CookieSyncManager子線程的Looper輪過一次的Message對象也跟其他人一樣,被回收并放在了回收池中。這個時候,剛好遇到了Dialog!!

這家伙剛剛好通過obtainMessage()從回收池中拿到了這個Message(被CookieSyncManager線程的本地變量引用住了),而且Message.obj變量就是OnDismissListener。

拿到之后,Dialog居然據為己有!!作為一個成員寵愛著!

Dialog自從擁有了mDismissMessage對象之后就不會讓它掛到消息隊列中了,每次要用都是拷貝一份而已。Message.obtain(mDismissMessage),所以這個Message再也不會回到回收池中,直到Dialog被銷毀,mDismissMessage變量也被置為null了。

但是,這個Message依然占據著堆內存,而且被一個“游離”著的子線程局部變量msg引用著!!于是有了這條引用鏈: Thread(CookieSyncManager) -> Message -> AuthDialog$3(OnDismissListener) -> AuthDialog -> Activity

七,總結一些注意點

針對Android4.3及以下版本,或者使用DVM的Android版本

  1. 使用WebView的時候,需要注意確保調用destroy()
  2. 考慮是否使用applicationContext()來構建WebView實例
  3. 調用Dialog設置OnShowListener、OnDismissListener、OnCancelListener的時候,注意內部類是否泄漏Activity對象
  4. 盡量不要自己持有Message對象

 

來自:http://dev.qq.com/topic/59267f94eb3f11cc2e12f503

 

 本文由用戶 AntonyMacle 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!