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

原文链接
在Square Register中,我们依赖于自定义View来构建我们的应用程序。有时,View监听某个对象的变化,但对象的生命周期往往比该View还要长。
举个例子,HeaderView可能需要从一个授权验证器单例监听用户名变化。
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() { @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()
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() { @Override public void call(String username) {...} }); }@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); usernameSubscription.unsubscribe(); } }

问题解决?其实并没完全解决。我最近看到一个LeakCanary报告,一段非常相似代码也引起该问题。
【android|[译]Android 泄露范例: 视图订阅】android|[译]Android 泄露范例: 视图订阅
文章图片

让我们再次查看代码:
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一般如下:
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()分发的地方:
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。
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图解的简单升级版本:
android|[译]Android 泄露范例: 视图订阅
文章图片

从上述可知,我们可以把订阅的代码移动到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() { @Override public void call(String username) {...} }); }@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); usernameSubscription.unsubscribe(); } }

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

    推荐阅读