前言
嗨,大家好,問大家一個“簡單”的問題:
Handler記憶體泄露的原因是什么?
你會怎么答呢?
這是錯誤的回答
有的朋友看到這個題表示,就這?太簡單了吧,
"內部類持有了外部類的參考,也就是Hanlder持有了Activity的參考,從而導致無法被回收唄,"
其實這樣回答是錯誤的,或者說沒回答到點子上,
記憶體泄漏
Java虛擬機中使用可達性分析的演算法來決定物件是否可以被回收,即通過GCRoot物件為起始點,向下搜索走過的路徑(參考鏈),如果發現某個物件或者物件組為不可達狀態,則將其進行回收,
而記憶體泄漏指的就是有些物件(短周期物件)沒有用了,但是卻被其他有用的類(長周期物件)所參考,從而導致無用物件占據了記憶體空間,形成記憶體泄漏,
所以上面的問題,如果僅僅回答內部類持有了外部類的參考,沒有指出內部類被誰所參考,那么按道理來說是不會發生記憶體泄漏的,因為內部類和外部類都是無用物件了,是可以被正常回收的,
所以這一題的關鍵在于,內部類被誰參考了?也就是Handler被誰參考了?
一起通過實踐研究下吧~
Handler發生記憶體泄漏的情況
1、發送延遲訊息
第一種情況,是通過handler發送延遲訊息:
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,就能看到以下圖片了,一個正在運行的記憶體情況實時圖:
可以看到圖片中有兩個按鈕我標出來了:
捕獲堆轉儲檔案按鈕,也就是生成hprof檔案,這個檔案會展示Java堆的使用情況,點擊這個按鈕后,AndroidStudio會幫我們生成這個堆轉儲檔案并且進行分析,GC按鈕,一般我們在我們捕獲堆轉儲檔案之前,點一下GC,就能把一些弱參考給回收,防止給我們分析帶來干擾,
所以我們打開HandlerActivity后,馬上finish,然后點擊GC按鈕,再點擊捕獲堆轉儲檔案按鈕,AndroidStudio會自動跳轉到以下界面:
可以看到左上角有一個Leaks,這就是你記憶體泄漏的點,點擊就能看到記憶體泄漏的類了,右下角就是記憶體泄漏類的參考路徑,
從這張圖可以看到,我們的HandlerActivity發生了記憶體泄漏,從參考路徑來看,是被匿名內部類的實體mHandler持有參考了,而Handler的參考是被Message持有了,Message參考是被MessageQueue持有了...
結合我們所學的Handler知識和這次參考路徑分析,這次記憶體泄漏完整的參考鏈應該是:
主執行緒 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
所以這次參考的頭頭就是主執行緒,主執行緒肯定是不會被回收的,只要是運行中的執行緒都不會被JVM回收,跟靜態變數一樣被JVM特殊照顧,
這次記憶體泄漏的原因算是搞清楚了,當然Handler記憶體泄漏的情況不光這一種,看看第二種情況:
2、子執行緒運行沒結束
第二個實體,是我們常用到的,在子執行緒中作業,比如請求網路,然后請求成功后通過Handler進行UI更新,
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")
}
}
}
同樣運行后看看記憶體泄漏情況:
可以發現,這里的記憶體泄漏主要的原因是因為這個運行中的子執行緒,由于子執行緒這個匿名內部類持有了外部類的參考,而子執行緒本身是一直在運行的,剛才說過運行中的執行緒是不會被回收的,所以這里記憶體泄漏的參考鏈應該是:
運行中的子執行緒 —> Activity
當然,這里的Handler也是持有了Activity的參考的,但主要引起記憶體泄漏的原因還是在于子執行緒本身,就算子執行緒中不用Handler,而是呼叫Activity的其他變數或者方法還是會發生記憶體泄漏,
所以這種情況我覺得不能看作Handler引起記憶體泄漏的情況,其根本原因是因為子執行緒引起的,如果解決了子執行緒的記憶體泄漏,比如在Activity銷毀的時候停止子執行緒,那么Activity就能正常被回收,那么也不存在Handler的問題了,
延伸問題1:內部類為什么會持有外部類的參考
這是因為內部類雖然和外部類寫在同一個檔案中,但是編譯后還是會生成不同的class檔案,其中內部類的建構式中會傳入外部類的實體,然后就可以通過this$0訪問外部類的成員,
其實也挺好理解的吧,因為在內部類中可以呼叫外部類的方法,變數等等,所以肯定會持有外部類的參考的,
貼一段內部類在編譯后用JD-GUI查看的class代碼,也許你能更好的理解:
//原代碼
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有什么不一樣嗎
其實可以看到,在上述的代碼中,我都加了一句
btn2.setText("2222")
這是因為在kotlin中的匿名內部類分為兩種情況:
在Kotlin中,匿名內部類如果沒有使用到外部類的物件參考時候,是不會持有外部類的物件參考的,此時的匿名內部類其實就是個靜態匿名內部類,也就不會發生記憶體泄漏,在Kotlin中,匿名內部類如果使用了對外部類的參考,像我剛才使用了btn2,這時候就會持有外部類的參考了,就會需要考慮記憶體泄漏的問題,
所以我特意加了這一句,讓匿名內部類持有外部類的參考,復現記憶體泄漏問題,
同樣kotlin中對于內部類也是和Java有區別的:
- Kotlin中所有的內部類都是默認靜態的,也就都是
靜態內部類, - 如果需要呼叫外部的物件方法,就需要用
inner修飾,改成和Java一樣的內部類,并且會持有外部類的參考,需要考慮記憶體泄漏問題,
解決記憶體泄漏
說了這么多,那么該怎么解決記憶體泄漏問題呢?其實所有記憶體泄漏的解決辦法都大同小異,主要有以下幾種:
- 不要讓
長生命周期物件持有短生命周期物件的參考,而是用長生命周期物件持有長生命周期物件的參考,
比如Glide使用的時候傳的背景關系不要用Activity而改用Application的背景關系,還有單例模式不要傳入Activity背景關系,
- 將物件的強參考改成
弱參考
強參考就是物件被強參考后,無論如何都不會被回收,
弱參考就是在垃圾回收時,如果這個物件只被弱參考關聯(沒有任何強參考關聯他),那么這個物件就會被回收,
軟參考就是在系統將發生記憶體溢位的時候,回進行回收,
虛參考是物件完全不會對其生存時間構成影響,也無法通過虛參考來獲取物件實體,用的比較少,
所以我們將物件改成弱參考,就能保證在垃圾回收時被正常回收,比如Handler中傳入Activity的弱參考實體:
MyHandler(WeakReference(this)).sendEmptyMessageDelayed(0, 20000)
//kotlin中內部類默認為靜態內部類
class MyHandler(var mActivity: WeakReference<HandlerActivity>):Handler(){
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
mActivity.get()?.changeBtn()
}
}
- 內部類寫成靜態類或者外部類
跟上面Hanlder情況一樣,有時候內部類被不正當使用,容易發生記憶體泄漏,解決辦法就是寫成外部類或者靜態內部類,
- 在短周期結束的時候將可能發生記憶體泄漏的地方移除
比如Handler延遲訊息,資源沒關閉,集合沒清理等等引起的記憶體泄漏,只要在Activity關閉的時候進行消除即可:
@Override
protected void onDestroy() {
//移除handler所有訊息
if(mHanlder != null){
mHandler.removeCallbacksAndMessages(null)
}
super.onDestroy();
}
總結
Handler記憶體泄露的原因是什么?
Handler導致記憶體泄漏一般發生在發送延遲訊息的時候,當Activity關閉之后,延遲訊息還沒發出,那么主執行緒中的MessageQueue就會持有這個訊息的參考,而這個訊息是持有Handler的參考,而handler作為匿名內部類持有了Activity的參考,所以就有了以下的一條參考鏈,
主執行緒 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
其根本原因是因為這條參考鏈的頭頭,也就是主執行緒,是不會被回收的,所以導致Activity無法被回收,出現記憶體泄漏,其中Handler只能算是導火索,
而我們平時用到的子執行緒通過Handler更新UI,其原因是因為運行中的子執行緒不會被回收,而子執行緒持有了Actiivty的參考(不然也無法呼叫Activity的Handler),所以就導致記憶體泄漏了,但是這個情況的主要原因還是在于子執行緒本身,
所以綜合兩種情況,在發生記憶體泄漏的情況中,Handler都不能算是罪魁禍首,罪魁禍首(根本原因)都是他們的頭頭——執行緒,
參考
Handler 記憶體泄露
參考介紹
Kotlin 記憶體泄漏
拜拜
有一起學習的小伙伴可以關注下?? 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識,公眾號回復111可獲得面試題《思考與解答》以往期刊,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/240449.html
標籤:Android

