自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android中導(dǎo)致內(nèi)存泄漏的竟然是它----Dialog

移動(dòng)開(kāi)發(fā) Android
Android 中 Activity 代表一個(gè)頁(yè)面,擁有一段生命周期,生命周期結(jié)束后,Activity 對(duì)象應(yīng)當(dāng)在之后某個(gè)合適的時(shí)機(jī)被 VM 回收內(nèi)存。出現(xiàn)了泄漏就意味著 Activity 生命周期結(jié)束后,VM發(fā)現(xiàn) Activity 一直被持有,沒(méi)有回收這些無(wú)用的內(nèi)存。

一. 內(nèi)存泄漏的 Bug 猛增

最近在 App 進(jìn)行 mokey 測(cè)試的時(shí)候檢測(cè)到一些內(nèi)存泄漏問(wèn)題。在前天的測(cè)試中,樓主一瞬間收到了4個(gè)這樣的 Bug 單,瞬間心理無(wú)比糾結(jié),真有千萬(wàn)只羊駝向我奔來(lái)。

 

 

 

 

登錄頁(yè)面出現(xiàn)內(nèi)存泄漏??!!樓主的代碼是如此的***而無(wú)懈可擊,這么可能出現(xiàn)這么多泄漏的問(wèn)題?

插播什么是 Activity 泄漏:Android 中 Activity 代表一個(gè)頁(yè)面,擁有一段生命周期,生命周期結(jié)束后,Activity 對(duì)象應(yīng)當(dāng)在之后某個(gè)合適的時(shí)機(jī)被 VM 回收內(nèi)存。出現(xiàn)了泄漏就意味著 Activity 生命周期結(jié)束后,VM發(fā)現(xiàn) Activity 一直被持有,沒(méi)有回收這些無(wú)用的內(nèi)存。

按照以往的經(jīng)驗(yàn),大部分 Activity 泄漏的原因都是由于 Handler 內(nèi)部類長(zhǎng)時(shí)間掛在線程中導(dǎo)致的。而這塊我們 App 已經(jīng)考慮便處理了。究竟是哪泄漏了?

二. WebView 導(dǎo)致內(nèi)存泄漏眾所周知

帶著懷疑的心態(tài)并且為了證明清白,我一個(gè)個(gè)點(diǎn)進(jìn)去看了,總共有三條不同的引用鏈。為了后續(xù)說(shuō)明,這里取了個(gè)名字:

① AuthDialog 引用鏈

 

 

 

 

② BrowserFrame 引用鏈

 

 

 

 

③ IClipboradDataPaste 引用鏈

 

 

 

 

看來(lái)這次情況有點(diǎn)不同!由于 Monkey 測(cè)試的機(jī)型比較少,這里所有的 Bug 都來(lái)自一部三星 GT-I9300@android+4.3 手機(jī)。

為了快速解決問(wèn)題,樓主詢問(wèn)了其他同事和 StackOverflow,發(fā)現(xiàn)這其中有三個(gè)類 CookieSyncManager, WebView, WebViewClassic 已經(jīng)被很多人提起過(guò),它們會(huì)導(dǎo)致內(nèi)存泄漏!初步有如下的結(jié)論如下:

1.CookieSyncManager 是個(gè)全局靜態(tài)單例,操作系統(tǒng)內(nèi)部使用了 App 的 Activity 作為 Context 構(gòu)造了它的實(shí)例。我們應(yīng)該在 App 啟動(dòng)的時(shí)候,搶先幫系統(tǒng)創(chuàng)建這個(gè)單例,而且要用 applicationContext,讓它不會(huì)引用到 Activity。

 

 

 

 

2.使用 WebView 的頁(yè)面(Activity),在生命周期結(jié)束頁(yè)面退出(onDestory)的時(shí)候,需要主動(dòng)調(diào)用 WebView.onPause()以及 WebView.destory()以便讓系統(tǒng)釋放 WebView 相關(guān)資源。

 

 

 

 

4.WebView 內(nèi)存泄漏是眾所周知的,建議另外啟動(dòng)一個(gè)進(jìn)程專門(mén)運(yùn)行 WebView。不要9998,不要9999,我們要100%!WebView 用完之后就把進(jìn)程殺死,即使泄漏了也無(wú)礙。

按照以上的種種結(jié)論,我們都認(rèn)定了這里面就是 WebView 引起的。

但是!我們的應(yīng)用主進(jìn)程 LoginActivity 根本沒(méi)有用到 WebView 啊!!!

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

根據(jù)以上的 AuthDialog 引用鏈,樓主把目標(biāo)鎖定了某sdk:

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

c 跟 j 都是 SDK 中繼承于 WebView 的一個(gè)子類,k 是登錄接口的輸入?yún)?shù) Activity。這里創(chuàng)建了 c 對(duì)象之后向上塑形賦給了 j 。

 

 

 

 

網(wǎng)上已經(jīng)有很多例子表明,直接用 Activity 作為參數(shù)構(gòu)建 WebView 就非常有可能導(dǎo)致 Activity 泄漏。

 

 

 

 

不過(guò)也看到了代碼中,有調(diào)用了 WebView 的 destory()方法釋放資源。但是這里似乎無(wú)法保證 dismiss()一定會(huì)被執(zhí)行。

 

 

 

 

問(wèn)題到這里發(fā)現(xiàn)比較麻煩了,SDK 對(duì)我們來(lái)說(shuō)是第三方包,我們沒(méi)法讓第三方包不用 WebView,或者讓第三方包把 WebView 放在另外一個(gè)進(jìn)程中運(yùn)行啊!于是,在 App 上面做規(guī)避暫時(shí)不好實(shí)現(xiàn)。于是找了 SDK 的童鞋一起分析了。

最終,大家都有了一個(gè)初步的共識(shí),在 Android4.3 以下的舊版本,使用 Activity 對(duì)象創(chuàng)建 WebView,確實(shí)有可能導(dǎo)致內(nèi)存泄漏。非常高興能得到 SDK 童鞋的大力支持,一起分析,問(wèn)題到這里有了初步的進(jìn)展。

四. 心結(jié)未解,翻看WebView源碼了解根源

不過(guò),問(wèn)題到這里樓主心理還是有個(gè)很嚴(yán)重的疑惑沒(méi)有解開(kāi)(是什么疑惑呢?)。于是拿了 Android4.3 的源碼又翻了一遍希望找尋這里頭的根本原因,做了一點(diǎn)記錄,針對(duì) WebView 在 Java 層的結(jié)構(gòu)畫(huà)了一個(gè)不嚴(yán)謹(jǐn)?shù)念悎D:源碼來(lái)源:http://androidxref.com/4.3_r2.1/ (請(qǐng)復(fù)制以上鏈接到瀏覽器打開(kāi))

 

 

 

 

大概情況是這樣:WebView 這套結(jié)構(gòu)中,有一個(gè)工廠類 WebViewFactory 提供靜態(tài)方法。

Android4.3(JellyBean) 版本通過(guò) WebViewFactory 工廠類創(chuàng)建了一個(gè)全局單例對(duì)象 WebViewClassic$Factory,然后使用這個(gè) Factory 創(chuàng)建了一整套實(shí)現(xiàn)的代碼(XXXClassic):WebViewClassic, CookieManagserClassic, WebViewDatabaseClassic。

WebViewClassic 才是真正地實(shí)現(xiàn) WebView 的各種 API。WebViewClassic 創(chuàng)建并維護(hù)了 WebViewCore 對(duì)象。

WebViewCore 創(chuàng)建了一個(gè)子線程“WebViewCoreThread”,這里是一個(gè)全局的單例的而且一旦啟動(dòng)就不會(huì)停止的 Thread!WebViewCore 會(huì)在這個(gè)子線程中創(chuàng)建維護(hù)并調(diào)用 BrowserFrame 的方法。

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

BrowserFrame 還會(huì)調(diào)用 CookieSyncManager.createIntance(),這也是系統(tǒng)框架中唯一一處調(diào)用的地方!

 

 

 

 

看到這里之后,樓主發(fā)現(xiàn)以上所說(shuō)的,提前幫系統(tǒng)調(diào)用

CookieSyncManager.createInstance(contenxt.getApplicationContext()) 可能是沒(méi)有效果的,因?yàn)橄到y(tǒng)本來(lái)就是這么做的。手機(jī)廠商修改這里的可能性不大。

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

這里我們比較關(guān)心為什么 Activity 會(huì)泄漏,所以關(guān)鍵看看哪些類對(duì)象中持有了 Activity(Context) 引用:WebViewClassic, WebViewCore, BrowserFrame。

這套結(jié)構(gòu)中有很多靜態(tài)單例,還有子線程,想想也挺惡心的。而且三個(gè)關(guān)鍵的類都持有 Activity 引用。不過(guò)我們發(fā)現(xiàn),其實(shí) WebViewClassic, WebViewCore 這兩個(gè)個(gè)對(duì)象跟 WebView 對(duì)象的生命周期是一致的,Activity 銷毀于是 WebView 銷毀了,WebView 銷毀了另外兩個(gè)對(duì)象也跟著銷毀。煙消云散。。。

留下兩個(gè)孤獨(dú)的子線程還在跑,還有全局靜態(tài)的釘子戶對(duì)象。

但是!BrowserFrame 本身是 Handler,假如它因?yàn)?native 層的調(diào)用往”WebViewCoreThread”掛了一個(gè)消息,那么便可以建立一條引用鏈:

Thread->MessageQueue->Message->Handler(BrowserFrame)->Activity

 

 

 

 

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

五. ***的疑惑

我們?cè)賮?lái)看看 AuthDialog 的引用鏈。

 

換成 MAT 看會(huì)比較清晰:

 

樓主發(fā)現(xiàn),這里 CookieSyncManager 線程,居然直接引用了 Message 對(duì)象!這是什么鬼?一般情況下,HandlerThread 持有一個(gè) MessageQueue 對(duì)象,MessageQueue 才持有 Message 隊(duì)列。

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 線程中存在某個(gè) Message 的局部變量,而由于線程一直沒(méi)有結(jié)束,所以局部變量一直沒(méi)有被釋放。而這個(gè) Message.obj 成員引用了 AuthDialog$3 對(duì)象。

這是一個(gè)內(nèi)部類,樓主發(fā)現(xiàn)內(nèi)部類混淆之后的命名規(guī)則就是:第幾個(gè)出現(xiàn)就命名為幾。

AuthDialog 里面有很多內(nèi)部類:

 

 

 

 

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

 

 

 

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

于是,我們心中生成的一條引用鏈?zhǔn)沁@樣的:

Thread(main) -> MessageQueue->Message -> obj(OnDismissListener) -> AuthDialog -> Activity

 

 

 

 

可是不對(duì)啊,我們所能找到的引用鏈跟 CookieSyncManager 子線程一點(diǎn)關(guān)系都沒(méi)有!

再對(duì)比一下:

 

 

 

 

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

好吧,樓主承認(rèn)研究了一個(gè)晚上沒(méi)有任何進(jìn)展。。。

六. 原來(lái)是它!—Dialog

注:以下的分析感悟來(lái)自Github上面的一篇文章:《一個(gè)內(nèi)存泄漏引發(fā)的血案》

https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-25/%E4%B8%80%E4%B8%AA%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88-Square.md

有興趣的童鞋請(qǐng)☞☞ 復(fù)制鏈接到瀏覽器打開(kāi) ☜☜,進(jìn)行詳細(xì)閱讀!

這里簡(jiǎn)要說(shuō)明一下,作者的結(jié)論是:在 Android Lollipop 之前使用 AlertDialog 可能會(huì)導(dǎo)致內(nèi)存泄漏!

作者發(fā)現(xiàn),局部變量的生命周期在 Dalvik VM 跟 ART/JVM 中有區(qū)別。在 DVM 中,假如線程死循環(huán)或者阻塞,那么線程棧幀中的局部變量假如沒(méi)有被置為 null,那么就不會(huì)被回收。

如下代碼使用阻塞隊(duì)列說(shuō)明問(wèn)題:

 

 

 

 

子線程中調(diào)用 loop()死循環(huán),不停地從阻塞隊(duì)列中取出一個(gè) MyMessage 對(duì)象并且將對(duì)象的引用賦值給局部變量 message,一次 while 循環(huán)之后,虛擬機(jī)應(yīng)當(dāng)結(jié)束 while 花括號(hào)中的局部變量的生命周期,并且釋放對(duì)應(yīng)的堆內(nèi)存中的 MyMessage 對(duì)象??墒牵珼VM 沒(méi)有這么做!!

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

這種場(chǎng)景不就是 Android Handler 消息機(jī)制的處理方式么?!

 

 

 

 

Looper 不停地從阻塞隊(duì)列 MessageQueue 中取出下一條消息 Message 并將引用賦給本地變量 msg。一旦一次循環(huán)結(jié)束了,msg 沒(méi)有被置為 null,對(duì)應(yīng)的 Message 對(duì)象沒(méi)有被回收,于是就泄漏了。

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

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

 

 

 

 

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

拿到之后,Dialog 居然據(jù)為己有!!作為一個(gè)成員寵愛(ài)著!

 

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

 

但是,這個(gè) Message 依然占據(jù)著堆內(nèi)存,而且被一個(gè)“游離”著的子線程局部變量 msg 引用著!!于是有了這條引用鏈:

Thread(CookieSyncManager) -> Message -> AuthDialog$3(OnDismissListener) -> AuthDialog -> Activity

七. 總結(jié)一些注意點(diǎn)

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

  1. 使用 WebView 的時(shí)候,需要注意確保調(diào)用 destroy()
  2. 考慮是否使用 applicationContext()來(lái)構(gòu)建 WebView 實(shí)例
  3. 調(diào)用 Dialog 設(shè)置 OnShowListener、OnDismissListener、OnCancelListener 的時(shí)候,注意內(nèi)部類是否泄漏 Activity 對(duì)象
  4. 盡量不要自己持有 Message 對(duì)象.
責(zé)任編輯:龐桂玉 來(lái)源: 騰訊Bugly
相關(guān)推薦

2020-08-19 09:23:10

傳輸網(wǎng)絡(luò)WDM網(wǎng)絡(luò)技術(shù)

2024-06-17 00:04:00

JavaScriptWebRust開(kāi)發(fā)

2024-01-05 08:37:41

前端項(xiàng)目開(kāi)發(fā)

2020-10-19 06:49:18

內(nèi)存String

2015-08-24 10:31:14

Windows 10功能

2020-09-29 06:45:49

JDK

2015-06-18 11:04:58

2020-12-15 08:05:40

路由器服務(wù)器網(wǎng)絡(luò)層

2021-07-28 06:51:08

Nacos代理模式

2024-08-05 01:28:26

2024-09-27 11:38:49

2021-10-18 13:42:52

加密貨幣金融工具

2023-03-13 08:09:03

Protobuffeature分割

2018-07-06 00:09:47

2016-09-25 14:34:10

蘋(píng)果谷歌亞馬遜

2022-03-01 14:48:03

IP地址網(wǎng)絡(luò)路由振蕩

2020-10-20 17:18:00

戴爾

2020-11-12 09:15:16

GitHubPython開(kāi)發(fā)

2021-04-08 22:31:20

編程語(yǔ)言PythonC語(yǔ)言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)