[译]Android 泄露范例: 视图订阅

原文链接

Square Register中,我们依赖于自定义View来构建我们的应用程序。有时,View监听某个对象的变化,但对象的生命周期往往比该View还要长。

举个例子,HeaderView可能需要从一个授权验证器单例监听用户名变化。

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的好地方,所以我们在此查找其子View,然后订阅用户名的变化。

上面的代码有一个严重的bug:我们没有退订操作。当View被移除,Action1仍然处于订阅状态。因为Action1是一个匿名内部类,它持有外部类的引用— HeaderView。整个View树现在被泄露了,而且不能被GC回收。

修复这个bug,一般做法是在该View detached Window时退订,亦即onDetachedFromWindow()

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报告,一段非常相似代码也引起该问题。

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从来没有attached,显然它就没有detached一说了。所以,View.onFinishInflate()被调用了,但View.onAttachedToWindow()没有被调用

让我们再了解一下View.onAttachedToWindow():

  • 当一个View通过Window操作添加进其父View,onAttachedToWindow()会立即调用,如addView()
  • 当一个View不是通过Window操作添加进其父View,onAttachedToWindow()会在父View attached进Window时调用

我们加载一个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都会接收到View.onFinishInflate() 回调,但不一定接收View.onAttachedToWindow() 回调。这是因为:View.onAttachedToWindow() 会在第一次遍历时被调用,有时会在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);
}
// ...
}
}

译者注:从源码分析来说,View.onAttachedToWindow()应该在onResume之后调用,因为第一次遍历即ViewRootImpl执行performTraversals的时机是在WindowManager.addView()之后,而WindowManager.addView()从ActivityThread源码可以得知是在handleResumeActivity()中调用的

当然,由于知识和翻译水平有限,不排除有别的场景或者我误解了作者意思

这就是为啥我们不能在onCreate()接收attached回调,那么在onStart() 之后呢?是否attached回调总在onCreate()后被调用?

并不是!我们可以从Activity.onCreate() 文档说明中找到答案:

You can call finish() from within this function, in which case onDestroy() will be immediately called without any of the rest of the activity lifecycle*(onStart(), onResume(), onPause(), etc) executing.

我们曾经在onCreate()中验证Activity intent,如果intent 内容无效,立即调用finish()并发送error result。

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();
}
}
}

view被加载,但没有attached到window,所以不会出现detached操作。

这是原来的Activity lifecycle图解的简单升级版本:

activity lifecycle

从上述可知,我们可以把订阅的代码移动到onAttachedToWindow()中:

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();
}
}

无论如何,这样实现更好:代码是对称的— onAttachedToWindow()和onDetachedFromWindow()成对出现;而且不像原来的实现,我们可以随意添加和删除View,无论多少次。

Adison wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!