为什么View的 onDetachedFromWindow 方法不一定执行

3,048 阅读3分钟

屏幕截图 2024-06-04 084118.png

在开发过程中,发现一个偶现的内存泄漏问题,经排查发现是 onDetachedFromWindow 没有执行造成的。项目背景是一个自定义的头像控件内部需要监听头像变更,因此在创建 View 时注册了监听,然后在 onDetachedFromWindow 时取消注册。伪代码如下:

class UserAvatarImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) :
    AppCompatImageView(context, attrs, defStyleAttr){

    init {
        // 注册监听用户头像的变更
        registerAvatarChangeListener()
    }


    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // 取消监听用户头像的变更
        unregisterAvatarChangeListener()
    }
}

可以看到,只有当 onDetachedFromWindow 没有执行的时候才会发生内存泄漏。那为什么 onDetachedFromWindow 没有执行呢,这个只能从源码中找答案了。

onDetachedFromWindow 的源码如下所示:

// View 的 onDetachedFromWindow 方法,内部是空实现
protected void onDetachedFromWindow() {  
}

// onDetachedFromWindow 是被 dispatchDetachedFromWindow 内部调用
void dispatchDetachedFromWindow() {
    ...

    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    ...
}

从源码中可以看到,View 的 onDetachedFromWindow 方法其内部是由 View 的 dispatchDetachedFromWindow 调用的。那 dispatchDetachedFromWindow 又是在哪里被调用的呢?实际上,它是在 ViewRootImpl 中被调用的,这个涉及到了,Android View的绘制流程,如果对该流程不清楚,可以看金三银四,Android View的绘制流程看这篇就够了

ViewRootImpl 的相关源码如下:

void dispatchDetachedFromWindow() {
    ...
    // 只有当 mView 和 mView.mAttachInfo 不为 null 时,dispatchDetachedFromWindow 才会被调用
    if (mView != null && mView.mAttachInfo != null) {
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
        // dispatchDetachedFromWindow 方法在这里被调用
        mView.dispatchDetachedFromWindow();
    }

    ...
}

通过源码可以看到,dispatchDetachedFromWindow 方法是否被调用是根据 mViewmView.mAttachInfo 是否为 null 来判断的。那么 mViewmView.mAttachInfo 又是什么时候创建的呢?

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
        int userId) {
    synchronized (this) {
        if (mView == null) {
            // 在 ViewRootImpl 的 setView 中会给 mView 赋值
            mView = view;
            ...
    }
}

从源码中可以看到,mView 属性会在 ViewRootImplsetView 方法中赋值。而 setView 方法是在什么时候被调用的呢?其实 setView 是在 onResume 方法之后,内部流程是通过 PhoneWindow 获取 WindowManagerImpl 来调用 addView 方法,其内部会调用 WindowManagerGlobal.addView 方法,最后调到 ViewRootImplsetView 方法。

至此内存泄漏的原因就清楚了,这是因为 onAttachedToWindow 是在 onResume 后执行的,因此如果在 onResume 之前如果 finish activity 就会导致 ViewRootImplsetView 方法不会被执行。由于 mView 属性为 null,activity 在 onDestroy 后也不执行 onDetachedFromWindow 方法,在View 中的 unregisterAvatarChangeListener 就无法执行了,这就造成了内存泄漏的问题。

解决方案很简单,就是不再创建 View 时创建监听,而是在 onAttachedToWindow 时监听。代码示例如下:

class UserAvatarImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) :
    AppCompatImageView(context, attrs, defStyleAttr){
        
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // 在 onAttachedToWindow 中注册监听用户头像的变更
        registerAvatarChangeListener()
    }
    

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // 取消监听用户头像的变更
        unregisterAvatarChangeListener()
    }

}

为什么在 onAttachedToWindow 时监听就可以了,这是因为 onAttachedToWindow 是在 setView 方法之后执行的。onAttachedToWindow 方法可以确保调用它时, mView 属性不为 null,即在 activity onDestroy 时,onDetachedFromWindow 方法一定会被调用。

总结一下就是,onDetachedFromWindowonAttachedToWindow 两个方法是配对的。如果 onAttachedToWindow 没有被执行,那么 onDetachedFromWindow 也不会被执行。在开发过程中一定要注意,onDetachedFromWindowonAttachedToWindow 两个方法是可能不执行的。最好将把这两个方法配套使用,不然就有可能产生我这样的内存泄漏的问题。