Android洩漏模式:View中的訂閱

發表於2016-10-21

我們通過一些自定義的view來構建Square的註冊模組。有時候這些view需要監聽一個比他們自身宣告週期還要長的物件。

例如,一個HeaderView(譯者注:類似於頭像控制元件)可能需要監聽使用者名稱的改變,而這個使用者名稱來自於一個Authentic單例。

onFinishInflate()是一個用來填充自定義view,並試圖找到其子view的絕佳時機。所以我們決定在這個地方處理繫結檢視的邏輯,並訂閱使用者名稱的變化。

上面的程式碼存在一個非常嚴重的bug:沒有解除訂閱。當嘗試回收view時,Action1始終處於訂閱狀態。因為Action1是一個匿名內部類,它持有外部類的引用,也就是持有對HeaderView的引用。現在整個檢視層級結構都發生了洩露,無法被回收。

修復這個bug,我們可以在view從window中分離的時候取消訂閱

 

問題被修復了嗎?不完全是!我最近看了LeakCanary的報告,由一段類似程式碼所引發的記憶體洩露:

268450-dae2688bc0588a52

讓我們再看一遍程式碼:

不知為什麼View.onDetachedFromWindow()沒有被呼叫,這就是造成洩露的原因。

在除錯的過程中,我發現View.onAttachedToWindow()同樣沒有被呼叫。如果一個View沒有被Attach過,那麼理所應當的也不會發生Detach。所以,View.onFinishInflate()被呼叫了,而View.onAttachedToWindow()則沒有

讓我們多瞭解一些這個View.onAttachedToWindow()

  • 當view被新增到一個已經載入到window的父view中時addView()的內部會立即呼叫onAttachedToWindow()
  • 當View被新增到一個還沒有載入至window的父view中時onAttachedToWindow()將會在父view被載入到window後執行

我們用Android中的慣用方式來填充view層級:

這時,檢視層級中的每一個view都會收到View.onFinishInflate()的回撥通知,而不是View.onAttachedToWindow(),而原因是:

View.onAttachedToWindow()只在第一次view遍歷時被呼叫,將發生在Activity.onStart()之後

ViewRootImpl執行了onAttachedToWindow()的分發操作:

所以說,我們不能在onCreated()中得到Attach結果,那麼在onStart()之後就一定能嗎?它總是在onCreated()之後被呼叫嗎?

不一定!Activity.onCreate()的文件給出了答案:

你可以在這個函式內直接呼叫finish(),這種情況下onDestroy()會被立即呼叫,那麼將不再執行剩餘的生命週期回撥(onStart(),onResume(),onPause()等等)。

我終於頓悟了!

我們在onCreated()中判斷intent,如果intent的內容失效了,則立即呼叫finish()並返回一個代表錯誤資訊的結果。

雖然整個層級檢視都被填充了,但是Attach至window還沒有發生,因此Detach的動作也不會發生。

那麼根據這種情況,這裡有一張更新後的Activity生命週期圖表:

268450-30ebee4fd34d2222

這是為了更好的解決問題:保證對稱訪問是好的。與之前的實現方式不同,現在我們可以任意次數的新增或者移除那個view了。

相關文章