- 原文鏈接: Android leak pattern: subscriptions in views
- 原文出自: Pierre-Yves Ricau
- 譯文出自: 小鄧子的簡書
- 譯者: 小鄧子
- 狀態(tài): 完成
我們通過一些自定義的view來構(gòu)建Square register模塊。有時候這些view需要監(jiān)聽一個比他們自身聲明周期還要長的對象。
例如,一個HeaderView(譯者注:類似于頭像控件)可能需要監(jiān)聽用戶名的改變,而這個用戶名來自于一個Authentic單例。
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的絕佳時機(jī)。所以我們決定在這個地方處理綁定視圖的邏輯,并訂閱用戶名的變化。
上面的代碼存在一個非常嚴(yán)重的bug:沒有解除訂閱。當(dāng)嘗試回收view時,Action1
始終處于訂閱狀態(tài)。因為Action1
是一個匿名內(nèi)部類,它持有外部類的引用,也就是持有對HeaderView的引用。現(xiàn)在整個視圖層級結(jié)構(gòu)都發(fā)生了泄露,無法被回收。
修復(fù)這個bug,我們可以在view從window中分離的時候取消訂閱:
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();
}
}
問題被修復(fù)了嗎?不完全是!我最近看了LeakCanary的報告,由一段類似代碼所引發(fā)的內(nèi)存泄露:
讓我們再看一遍代碼:
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()
沒有被調(diào)用,這就是造成泄露的原因。
在調(diào)試的過程中,我發(fā)現(xiàn)View.onAttachedToWindow()
同樣沒有被調(diào)用。如果一個View沒有被Attach過,那么理所應(yīng)當(dāng)?shù)囊膊粫l(fā)生Detach。所以,View.onFinishInflate()
被調(diào)用了,而View.onAttachedToWindow()
則沒有。
讓我們多了解一些這個View.onAttachedToWindow()
:
當(dāng)view被添加到一個已經(jīng)加載到window的父view中時,
addView()
的內(nèi)部會立即調(diào)用onAttachedToWindow()
。當(dāng)View被添加到一個還沒有加載至window的父view中時,
onAttachedToWindow()
將會在父view被加載到window后執(zhí)行。
我們用Android中的慣用方式來填充view層級:
public class MyActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
}
}
這時,視圖層級中的每一個view都會收到View.onFinishInflate()
的回調(diào)通知,而不是View.onAttachedToWindow()
,而原因是:
View.onAttachedToWindow()
只在第一次view遍歷時被調(diào)用,將發(fā)生在Activity.onStart()
之后。
<u>ViewRootImpl</u>執(zhí)行了onAttachedToWindow()
的分發(fā)操作:
public class ViewRootImpl {
private void performTraversals() {
// ...
if (mFirst) {
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
// ...
}
}
所以說,我們不能在onCreated()
中得到Attach結(jié)果,那么在onStart()
之后就一定能嗎?它總是在onCreated()
之后被調(diào)用嗎?
不一定!<u>Activity.onCreate()</u>的文檔給出了答案:
你可以在這個函數(shù)內(nèi)直接調(diào)用
finish()
,這種情況下onDestroy()
會被立即調(diào)用,那么將不再執(zhí)行剩余的生命周期回調(diào)(onStart()
,onResume()
,onPause()
等等)。
我終于頓悟了!
我們在onCreated()
中判斷intent,如果intent的內(nèi)容失效了,則立即調(diào)用finish()
并返回一個代表錯誤信息的結(jié)果。
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還沒有發(fā)生,因此Detach的動作也不會發(fā)生。
那么根據(jù)這種情況,這里有一張更新后的Activity生命周期圖表:
因此,有了這些認(rèn)識之后,我們應(yīng)該將訂閱的代碼移至onAttachedToWindow()
中:
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();
}
}
這是為了更好的解決問題:保證對稱訪問是好的。與之前的實現(xiàn)方式不同,現(xiàn)在我們可以任意次數(shù)的添加或者移除那個view了。