在开发过程中,发现一个偶现的内存泄漏问题,经排查发现是 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 方法是否被调用是根据 mView 和 mView.mAttachInfo 是否为 null 来判断的。那么 mView 和 mView.mAttachInfo 又是什么时候创建的呢?
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
// 在 ViewRootImpl 的 setView 中会给 mView 赋值
mView = view;
...
}
}
从源码中可以看到,mView 属性会在 ViewRootImpl 的 setView 方法中赋值。而 setView 方法是在什么时候被调用的呢?其实 setView 是在 onResume 方法之后,内部流程是通过 PhoneWindow 获取 WindowManagerImpl 来调用 addView 方法,其内部会调用 WindowManagerGlobal.addView 方法,最后调到 ViewRootImpl 的 setView 方法。
至此内存泄漏的原因就清楚了,这是因为 onAttachedToWindow 是在 onResume 后执行的,因此如果在 onResume 之前如果 finish activity 就会导致 ViewRootImpl 的 setView 方法不会被执行。由于 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 方法一定会被调用。
总结一下就是,onDetachedFromWindow 和 onAttachedToWindow 两个方法是配对的。如果 onAttachedToWindow 没有被执行,那么 onDetachedFromWindow 也不会被执行。在开发过程中一定要注意,onDetachedFromWindow 和 onAttachedToWindow 两个方法是可能不执行的。最好将把这两个方法配套使用,不然就有可能产生我这样的内存泄漏的问题。