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

96
作者 adison
2016.09.26 00:21* 字数 853

原文链接

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

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

让我们再次查看代码:

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图解的简单升级版本:


activity lifecycle

从上述可知,我们可以把订阅的代码移动到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();
  }
}

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


我的公众号
译文