原文链接

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

让我们再次查看代码:

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

从上述可知,我们可以把订阅的代码移动到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,无论多少次。

更多相关文章

  1. android EditText设置不可写
  2. android 使用html5作布局文件: webview跟javascript交互
  3. android studio调试c/c++代码
  4. Android(安卓)Wifi模块分析(三)
  5. Android中dispatchDraw分析
  6. IM-A820L限制GSM,WCDMA上网的原理(其他泛泰机型可参考)7.13
  7. 锁屏界面
  8. 关于Android(安卓)Studio3.2新建项目Android(安卓)resource link
  9. Android四大基本组件介绍与生命周期

随机推荐

  1. Android(安卓)RecyclerView网格布局
  2. Android左右滑动切换图片
  3. Android实现手势缩放图片
  4. Android上层怎样读写proc节点(示例)
  5. early_param("earlyprintk", setup_early
  6. android下为自己定义wifimanager类
  7. View 绘制机制 -- How Android(安卓)Draw
  8. 一维byte数组旋转
  9. 使用Notification发送消息通知
  10. Android(安卓)TextView 一些字体颜色、大