ViewTreeObserver在RecyclerView中的使用

4,994 阅读4分钟
关键字:OnPreDrawListener、ViewTreeObserver、正确移除、RecyclerView

背景

特定ViewHolder中有自定义组合控件,且自定义组合控件内部的子控件要按照父控件的宽高去设置自己的属性值的时候,但是由于各种原因,不能在外面定义好自定义组合控件的宽高值,你可能会想到各种方式去获取在View内部获取宽高,然后通过setLayoutParams完成你想要的设置,很遗憾,经过我多次实验,重写onMeasure,在onSizeChanged或者onWindowFocusChanged方法内部获取宽高,设置属性值,都有不同的问题,要么首次不生效,要么复用的时候不生效。只有通过getViewTreeObserver().addOnPreDrawListener添加监听才能完成需求,但是在RecyclerView中使用会有问题出现,在特定ViewHolder没有显示出来,且通过notifyDataSetChanged等方法刷新触发了特定ViewHolder的create和bind方法,那么就是出现onPreDraw方法不断调用且无法移除的情况出现。找到两篇文章关于这方面的介绍,文章一提出慎用,并且做出了测试,介绍了对象不一致可能会无法移除;文章二应该是和我遇到的问题类似,通过标记位解决,但是如果你在标记为外面打印log的话你会发现不停的在打印,同样是无法移除OnPreDrawListener监听。

先说结论

重写onAttachedToWindow方法,获取ViewTreeObserver实例对象,但是注意不要用它去addOnPreDrawListener,因为create和bindViewHolder的时候有可能在onAttachedToWindow方法之前,所以要通过getViewTreeObserver方法去调用addOnPreDrawListener,然后移除的时候使用在onAttachedToWindow方法中获取的实例对象,就可以正常移除,不用担心实例不一致的问题,后面会说到,代码如下:

    private ViewTreeObserver mObserver;

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mObserver = getViewTreeObserver();
    }

    private void addPreDrawListener(){
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                if (mObserver.isAlive()) {
                    mObserver.removeOnPreDrawListener(this);
                }
                // 获取宽高执行后面的代码
                return true;
            }
        });
    }

这些代码就可以满足你的需要,到这里就结束了,下面开始分析一下过程。

现象

日志截图.jpeg

如图中所示在不可见的时候会添加一次,可见的时候会再次添加,这个应该是RecyclerView的预取功能出现的,然后可以看到,虽然remove方法也不断调用,在不可见的时候还是会不断调用onPreDraw方法,严重的话会阻塞线程,当特定ViewHolder显示的时候再次调用bindView才会正常移除,停止打印。

原因分析

在removeOnPreDrawListener方法的时候打断点,进入方法查看:

    public void removeOnPreDrawListener(OnPreDrawListener victim) {
        checkIsAlive();
        if (mOnPreDrawListeners == null) {
            return;
        }
        mOnPreDrawListeners.remove(victim);
    }

由于实体机和api版本不太一致所以没有办法准到哪一行,但是还可以看到是由于mOnPreDrawListeners==null直接return然后导致remove失败的,然后搜索整个ViewTreeObserver对象内部没有发现给mOnPreDrawListeners赋值null的地方出现,猜测应该是一个没有添加过OnPreDrawListener对象的新实例,但是为什么,并且怎么解决呢,继续向上查找源码。 我们知道View的绘制从performTraversals方法开始,这个方法接近1000行,直接摘出重点代码查看:

private void performTraversals() {
    // cache mView since it is used so much below...
    final View host = mView;
    if (mFirst) {
	    // 第一步
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
    } else {
        ...
    }
		...
	// 第二步
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || 
!isViewVisible;
    if (!cancelDraw && !newSurface) {
        ...
        performDraw();
    } else {
        ...
    }
    mIsInTraversal = false;
}

先看第一步中的dispatchAttachedToWindow进入View中搜索查看,同样摘出重点代码:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    ...
    if (mFloatingTreeObserver != null) {
        info.mTreeObserver.merge(mFloatingTreeObserver);
        mFloatingTreeObserver = null;
    }
    ...
    onAttachedToWindow();
    ...
}

我们看到在调用View的onAttachedToWindow方法之前会先对mFloatingTreeObserver进行merge操作,最终留下的对象就是mAttachInfo中的ViewTreeObserver对象,这也是为什么我们返回的不是一个对象也可以remove成功的原因,因为做了merge操作,会把所有的集合赋值到新的对象中。 但是回想上面提到的在removeOnPreDrawListener失败打断点的时候mOnPreDrawListeners == null 并且没有找到赋值为null的情况,所以应该是新创建的ViewTreeObserver没有添加OnPreDrawListener,至于为什么会在onPreDraw里面调用getViewTreeObserver()方法创建新的对象,可以看一下内部实现:

public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
        return mAttachInfo.mTreeObserver;
    }
    if (mFloatingTreeObserver == null) {
        mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }
    return mFloatingTreeObserver;
}

猜测极大可能是因为mAttachInfo==null导致,然后搜索代码看到:

void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
	...
    onDetachedFromWindow();
    onDetachedFromWindowInternal();
	...
    mAttachInfo = null;
    if (mOverlay != null) {
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }
    ...
}

这里执行了mAttachInfo=null的操作,所以如果onPreDraw在这个方法之后的话就会重新创建ViewTreeObserver对象,发生上面remove失败的情况,可以在重写onDetachedFromWindow打印验证,日志截图如下,可以看到回先经过dispatchDetachedFromWindow这个方法,然后mAttachInfo = null;这就是为什么在onPreDraw里面getViewTreeObserver都是新的且集合为null的对象,

日志截图.jpeg
解决方案可以参考上面的结论,参考dispatchAttachedToWindow源码可以发现,mTreeObserver.merge操作一定在onAttachedToWindow之前,所以这里面拿到的ViewTreeObserver对象就是最终的,后面返回也是这个,分析完毕。