- 原文鏈接: Android leak pattern: subscriptions in views
- 原文出自: Pierre-Yves Ricau
- 譯文出自: 小鄧子的簡書
- 譯者: 小鄧子
- 狀態(tài): 完成
我們通過一些自定義的view來構建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的絕佳時機。所以我們決定在這個地方處理綁定視圖的邏輯蓉坎,并訂閱用戶名的變化肿孵。
上面的代碼存在一個非常嚴重的bug:沒有解除訂閱唠粥。當嘗試回收view時优炬,Action1
始終處于訂閱狀態(tài)。因為Action1
是一個匿名內部類厅贪,它持有外部類的引用蠢护,也就是持有對HeaderView的引用。現在整個視圖層級結構都發(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();
}
}
問題被修復了嗎贯吓?不完全是懈凹!我最近看了LeakCanary的報告,由一段類似代碼所引發(fā)的內存泄露:
讓我們再看一遍代碼:
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()
沒有被調用悄谐,這就是造成泄露的原因介评。
在調試的過程中,我發(fā)現View.onAttachedToWindow()
同樣沒有被調用爬舰。如果一個View沒有被Attach過们陆,那么理所應當的也不會發(fā)生Detach。所以情屹,View.onFinishInflate()
被調用了坪仇,而View.onAttachedToWindow()
則沒有。
讓我們多了解一些這個View.onAttachedToWindow()
:
當view被添加到一個已經加載到window的父view中時垃你,
addView()
的內部會立即調用onAttachedToWindow()
椅文。當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()
的回調通知,而不是View.onAttachedToWindow()
凌摄,而原因是:
View.onAttachedToWindow()
只在第一次view遍歷時被調用羡蛾,將發(fā)生在Activity.onStart()
之后。
<u>ViewRootImpl</u>執(zhí)行了onAttachedToWindow()
的分發(fā)操作:
public class ViewRootImpl {
private void performTraversals() {
// ...
if (mFirst) {
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
// ...
}
}
所以說望伦,我們不能在onCreated()
中得到Attach結果林说,那么在onStart()
之后就一定能嗎?它總是在onCreated()
之后被調用嗎屯伞?
不一定!<u>Activity.onCreate()</u>的文檔給出了答案:
你可以在這個函數內直接調用
finish()
豪直,這種情況下onDestroy()
會被立即調用劣摇,那么將不再執(zhí)行剩余的生命周期回調(onStart()
,onResume()
,onPause()
等等)。
我終于頓悟了弓乙!
我們在onCreated()
中判斷intent末融,如果intent的內容失效了钧惧,則立即調用finish()
并返回一個代表錯誤信息的結果。
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ā)生。
那么根據這種情況巧婶,這里有一張更新后的Activity生命周期圖表:
因此乾颁,有了這些認識之后,我們應該將訂閱的代碼移至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();
}
}
這是為了更好的解決問題:保證對稱訪問是好的艺栈。與之前的實現方式不同英岭,現在我們可以任意次數的添加或者移除那個view了。