1. 程式人生 > >小題大做 | Handler記憶體洩露全面分析

小題大做 | Handler記憶體洩露全面分析

## 前言 嗨,大家好,問大家一個“**簡單**”的問題: `Handler`記憶體洩露的原因是什麼? 你會怎麼答呢? ## 這是錯誤的回答 有的朋友看到這個題表示,就這?太簡單了吧。 "內部類持有了外部類的引用,也就是`Hanlder`持有了`Activity`的引用,從而導致無法被回收唄。" 其實這樣回答是**錯誤**的,或者說沒回答到**點子**上。 ## 記憶體洩漏 `Java`虛擬機器中使用**可達性分析**的演算法來決定物件是否可以被回收。即通過`GCRoot`物件為起始點,向下搜尋走過的路徑(引用鏈),如果發現某個物件或者物件組為不可達狀態,則將其進行回收。 而`記憶體洩漏`指的就是有些物件(短週期物件)沒有用了,但是卻被其他有用的類(長週期物件)所引用,從而導致無用物件佔據了記憶體空間,形成記憶體洩漏。 所以上面的問題,如果僅僅回答`內部類持有了外部類的引用`,沒有指出內部類被誰所引用,那麼按道理來說是不會發生記憶體洩漏的,因為內部類和外部類都是無用物件了,是可以被`正常回收`的。 所以這一題的關鍵在於,內部類被`誰`引用了?也就是Handler被誰引用了? 一起通過實踐研究下吧~ ## Handler發生記憶體洩漏的情況 ### 1、傳送延遲訊息 第一種情況,是通過`handler`傳送延遲訊息: ```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_handler) btn.setOnClickListener { //跳轉到HandlerActivity startActivity(Intent(this, HandlerActivity::class.java)) } } } class HandlerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_handler2) //傳送延遲訊息 mHandler.sendEmptyMessageDelayed(0, 20000) btn2.setOnClickListener { finish() } } val mHandler = object : Handler() { override fun handleMessage(msg: Message?) { super.handleMessage(msg) btn2.setText("2222") } } } ``` 我們在`HandlerActivity`中,傳送一個延遲20s的訊息。然後開啟`HandlerActivity`後,馬上finish。看看會不會記憶體洩漏。 ### 檢視記憶體洩漏並分析 現在檢視記憶體洩漏還是蠻方便的了,`AndroidStudio`自帶對堆轉儲(Heap Dump)檔案進行分析,並且會把記憶體洩漏點明確標出來。 我們執行專案,點選`Profiler——Memory`,就能看到以下圖片了,一個正在執行的記憶體情況實時圖: ![捕獲堆轉儲](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6c23b26c28034bc79ff31b150be9d07f~tplv-k3u1fbpfcp-watermark.image) 可以看到圖片中有兩個按鈕我標出來了: * `捕獲堆轉儲檔案按鈕`,也就是生成hprof檔案,這個檔案會展示Java堆的使用情況,點選這個按鈕後,AndroidStudio會幫我們生成這個堆轉儲檔案並且進行分析。 * `GC按鈕`,一般我們在我們捕獲堆轉儲檔案之前,點一下GC,就能把一些弱引用給回收,防止給我們分析帶來干擾。 所以我們開啟`HandlerActivity`後,馬上`finish`,然後點選GC按鈕,再點選捕獲堆轉儲檔案按鈕。`AndroidStudio`會自動跳轉到以下介面: ![分析堆轉儲](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47fdb69abd5b4cc5b15b77cf464b9ce0~tplv-k3u1fbpfcp-watermark.image) 可以看到左上角有一個`Leaks`,這就是你記憶體洩漏的點,點選就能看到記憶體洩漏的類了。右下角就是記憶體洩漏類的引用路徑。 從這張圖可以看到,我們的`HandlerActivity`發生了記憶體洩漏,從引用路徑來看,是被匿名內部類的例項`mHandler`持有引用了,而`Handler`的引用是被`Message`持有了,`Message`引用是被`MessageQueue`持有了... 結合我們所學的Handler知識和這次引用路徑分析,這次記憶體洩漏完整的引用鏈應該是: **主執行緒 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity** 所以這次引用的`頭頭`就是`主執行緒`,主執行緒肯定是不會被回收的,只要是`執行中的執行緒`都不會被JVM回收,跟`靜態變數`一樣被JVM特殊照顧。 這次記憶體洩漏的原因算是搞清楚了,當然`Handler`記憶體洩漏的情況不光這一種,看看第二種情況: ### 2、子執行緒執行沒結束 第二個例項,是我們常用到的,在子執行緒中工作,比如請求網路,然後請求成功後通過`Handler`進行UI更新。 ```kotlin class HandlerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_handler2) //執行中的子執行緒 thread { Thread.sleep(20000) mHandler.sendEmptyMessage(0) } btn2.setOnClickListener { finish() } } val mHandler = object : Handler() { override fun handleMessage(msg: Message?) { super.handleMessage(msg) btn2.setText("2222") } } } ``` 同樣執行後看看記憶體洩漏情況: ![子執行緒記憶體洩漏](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6354dd96e03b4629a4a0cdd34de002ca~tplv-k3u1fbpfcp-watermark.image) 可以發現,這裡的記憶體洩漏主要的原因是因為這個`執行中的子執行緒`,由於子執行緒這個`匿名內部類`持有了外部類的引用,而子執行緒本身是一直在執行的,剛才說過執行中的執行緒是不會被回收的,所以這裡記憶體洩漏的`引用鏈`應該是: **執行中的子執行緒 —> Activity** 當然,這裡的`Handler`也是持有了`Activity`的引用的,但主要引起記憶體洩漏的原因還是在於子執行緒本身,就運算元執行緒中不用`Handler`,而是呼叫`Activity`的其他變數或者方法還是會發生記憶體洩漏。 所以這種情況我覺得不能看作`Handler`引起記憶體洩漏的情況,其根本原因是因為子執行緒引起的,如果解決了子執行緒的記憶體洩漏,比如在`Activity`銷燬的時候停止子執行緒,那麼`Activity`就能正常被回收,那麼也不存在`Handler`的問題了。 ## 延伸問題1:內部類為什麼會持有外部類的引用 這是因為內部類雖然和外部類寫在同一個檔案中,但是編譯後還是會生成不同的`class`檔案,其中內部類的建構函式中會傳入外部類的例項,然後就可以通過`this$0`訪問外部類的成員。 其實也挺好理解的吧,因為在內部類中可以呼叫外部類的方法,變數等等,所以肯定會持有外部類的引用的。 貼一段內部類在編譯後用`JD-GUI`檢視的`class`程式碼,也許你能更好的理解: ```java //原始碼 class InnerClassOutClass{ class InnerUser { private int age = 20; } } //class程式碼 class InnerClassOutClass$InnerUser { private int age; InnerClassOutClass$InnerUser(InnerClassOutClass var1) { this.this$0 = var1; this.age = 20; } } ``` ## 延伸問題2:kotlin中的內部類與Java有什麼不一樣嗎 其實可以看到,在上述的程式碼中,我都加了一句 ```kotlin btn2.setText("2222") ``` 這是因為在`kotlin`中的`匿名內部類`分為兩種情況: * `在Kotlin中`,匿名內部類如果沒有使用到外部類的物件引用時候,是不會持有外部類的物件引用的,此時的匿名內部類其實就是個`靜態匿名內部類`,也就不會發生記憶體洩漏。 * `在Kotlin中`,匿名內部類如果使用了對外部類的引用,像我剛才使用了`btn2`,這時候就會持有外部類的引用了,就會需要考慮`記憶體洩漏`的問題。 所以我特意加了`這一句`,讓匿名內部類持有外部類的引用,復現記憶體洩漏問題。 同樣`kotlin`中對於內部類也是和`Java`有區別的: * Kotlin中所有的內部類都是預設靜態的,也就都是`靜態內部類`。 * 如果需要呼叫外部的物件方法,就需要用`inner`修飾,改成和Java一樣的內部類,並且會持有外部類的引用,需要考慮記憶體洩漏問題。 ## 解決記憶體洩漏 說了這麼多,那麼該怎麼解決`記憶體洩漏`問題呢?其實所有記憶體洩漏的解決辦法都大同小異,主要有以下幾種: * 不要讓`長生命週期物件`持有`短生命週期物件`的引用,而是用`長生命週期物件`持有`長生命週期物件`的引用。 比如`Glide`使用的時候傳的上下文不要用`Activity`而改用`Application`的上下文。還有單例模式不要傳入`Activity`上下文。 * 將物件的強引用改成`弱引用` >`強引用`就是物件被強引用後,無論如何都不會被回收。 >`弱引用`就是在垃圾回收時,如果這個物件只被弱引用關聯(沒有任何強引用關聯他),那麼這個物件就會被回收。 >`軟引用`就是在系統將發生記憶體溢位的時候,回進行回收。 >`虛引用`是物件完全不會對其生存時間構成影響,也無法通過虛引用來獲取物件例項,用的比較少。 所以我們將物件改成弱引用,就能保證在垃圾回收時被正常回收,比如`Handler`中傳入`Activity`的弱引用例項: ```kotlin MyHandler(WeakReference(this)).sendEmptyMessageDelayed(0, 20000) //kotlin中內部類預設為靜態內部類 class MyHandler(var mActivity: WeakR