我們通過一些自定義的view來構建Square的註冊模組。有時候這些view需要監聽一個比他們自身宣告週期還要長的物件。
例如,一個HeaderView(譯者注:類似於頭像控制元件)可能需要監聽使用者名稱的改變,而這個使用者名稱來自於一個Authentic單例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class HeaderView extends FrameLayout { private final Authenticator authenticator; public HeaderView(Context context, AttributeSet attrs) {...} @Override protected void onFinishInflate() { final TextView usernameView = (TextView) findViewById(R.id.username); authenticator.username().subscribe(new Action1<String>() { @Override public void call(String username) { usernameView.setText(username); } }); } } |
onFinishInflate()
是一個用來填充自定義view,並試圖找到其子view的絕佳時機。所以我們決定在這個地方處理繫結檢視的邏輯,並訂閱使用者名稱的變化。
上面的程式碼存在一個非常嚴重的bug:沒有解除訂閱。當嘗試回收view時,Action1
始終處於訂閱狀態。因為Action1
是一個匿名內部類,它持有外部類的引用,也就是持有對HeaderView的引用。現在整個檢視層級結構都發生了洩露,無法被回收。
修復這個bug,我們可以在view從window中分離的時候取消訂閱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class HeaderView extends FrameLayout { private final Authenticator authenticator; private Subscription usernameSubscription; public HeaderView(Context context, AttributeSet attrs) {...} @Override protected void onFinishInflate() { final TextView usernameView = (TextView) findViewById(R.id.username); usernameSubscription = authenticator.username().subscribe(new Action1<String>() { @Override public void call(String username) {...} }); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); usernameSubscription.unsubscribe(); } } |
問題被修復了嗎?不完全是!我最近看了LeakCanary的報告,由一段類似程式碼所引發的記憶體洩露:
讓我們再看一遍程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HeaderView extends FrameLayout { private final Authenticator authenticator; private Subscription usernameSubscription; public HeaderView(Context context, AttributeSet attrs) {...} @Override protected void onFinishInflate() {...} @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); usernameSubscription.unsubscribe(); } } |
不知為什麼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層級:
1 2 3 4 5 6 |
public class MyActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_activity); } } |
這時,檢視層級中的每一個view都會收到View.onFinishInflate()
的回撥通知,而不是View.onAttachedToWindow()
,而原因是:
View.onAttachedToWindow()
只在第一次view遍歷時被呼叫,將發生在Activity.onStart()
之後。
ViewRootImpl執行了onAttachedToWindow()
的分發操作:
1 2 3 4 5 6 7 8 9 |
public class ViewRootImpl { private void performTraversals() { // ... if (mFirst) { host.dispatchAttachedToWindow(mAttachInfo, 0); } // ... } } |
所以說,我們不能在onCreated()
中得到Attach結果,那麼在onStart()
之後就一定能嗎?它總是在onCreated()
之後被呼叫嗎?
不一定!Activity.onCreate()的文件給出了答案:
你可以在這個函式內直接呼叫
finish()
,這種情況下onDestroy()
會被立即呼叫,那麼將不再執行剩餘的生命週期回撥(onStart()
,onResume()
,onPause()
等等)。
我終於頓悟了!
我們在onCreated()
中判斷intent,如果intent的內容失效了,則立即呼叫finish()
並返回一個代表錯誤資訊的結果。
1 2 3 4 5 6 7 8 9 10 |
public class MyActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_activity); if (!intentValid(getIntent()) { setResult(Activity.RESULT_CANCELED, null); finish(); } } } |
雖然整個層級檢視都被填充了,但是Attach至window還沒有發生,因此Detach的動作也不會發生。
那麼根據這種情況,這裡有一張更新後的Activity生命週期圖表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class HeaderView extends FrameLayout { private final Authenticator authenticator; private Subscription usernameSubscription; public HeaderView(Context context, AttributeSet attrs) {...} @Override protected void onAttachedToWindow() { final TextView usernameView = (TextView) findViewById(R.id.username); usernameSubscription = authenticator.username().subscribe(new Action1<String>() { @Override public void call(String username) {...} }); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); usernameSubscription.unsubscribe(); } } |