从绘制时机深入浅出View.post

55 阅读3分钟

前言:

最近,我接手了一个新项目的 Bug 修复任务,其中遇到了一个有趣的问题:在页面中有一个 ImageView,它的宽度或高度根据传入的图片自动拉伸。然后根据 ImageView 的大小,通过代码绘制一个新的 View。但问题出现在 onCreate 方法中绘制时,布局总是错乱。而当将其放入 onResume 方法后,从后台重新进入应用时,绘制就能正常工作。

图片

一、初步怀疑是绘制时机问题

最开始,我以为是绘制时机的原因。因为在 onResume 后,重新进入应用时一切正常,而这个页面频繁地设置 View 的 LayoutParams,于是我猜测可能是某个 LayoutParams 设置的时机不对,导致了绘制的视图位置错乱。这个思路其实是接近的,但并没有进一步深入,导致走了一些弯路。尽管将其放入 onResume 后,问题依旧存在。

二、实际上是视图的绘制算法有问题

第二次绘制是正常的,甚至在重新创建的干净类中也能正确绘制。这时,我开始怀疑,是否是视图的绘制算法本身出了问题。

三、反思两次绘制过程中发生了什么

由于这个类频繁修改 LayoutParams,我将注意力集中在了这方面。查看了 LayoutParams 的源码:

public void setLayoutParams(ViewGroup.LayoutParams params) {    if (params == null) {        throw new NullPointerException("params == null");    }    mLayoutParams = params;    requestLayout();}

发现了 requestLayout() 方法。突然我想起来,修改了 View 的大小、位置等信息后,必须重新执行视图的测量(measure)和布局(layout)过程,才能更新视图的尺寸。而这个过程是异步的,系统会在下一个 UI 帧中更新视图的尺寸。因此,如果在修改 LayoutParams 后立即尝试获取视图的新尺寸,可能会得到旧的尺寸值。而我们绘制的 View 是新创建的,它的宽高是 0,这就导致了算法计算出错。

为验证这个问题,我写了个简单的测试:

view.postDelayed({    // 重新操作}, 500);

在所有布局设置完成后,延迟 500ms 再重新绘制,此时就能够正常绘制。这证明了我之前的猜测是对的,但显然,这样的解决方案并不优雅。

四、解决方案

我提出了两种解决方案:

1、在所有操作执行完成后,使用顶层 View.post() 方法,将绘制 View 的操作加入顶层 View 的最后一个操作。当顶层 View 绘制完成时,系统会自动执行绘制步骤。

binding.root.post {    // 操作}

2、在所有操作完成后,监听顶层布局的绘制。当顶层 View 绘制完成时,说明真实的宽高已经计算完成。

val observer: ViewTreeObserver = binding.root.viewTreeObserverobserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {    override fun onGlobalLayout() {        // 移除监听器,避免重复调用        observer.removeOnGlobalLayoutListener(this)        // 获取视图的新尺寸        // 在这里可以使用新的尺寸值    }})

我推荐使用 View.post() 的解决方案。那它到底是怎么做到的呢?让我们来看看源码:

public boolean post(Runnable action) {    final AttachInfo attachInfo = mAttachInfo;    if (attachInfo != null) {        return attachInfo.mHandler.post(action);    }    getRunQueue().post(action);    return true;}

首先,View.post() 方法通过 mAttachInfo 获取一个 Handler,如果 mAttachInfo 不为空,就使用它来提交任务。如果为空,就将任务提交到一个任务队列中。

AttachInfo 是 View 内部的一个静态类,持有一个 Handler 对象。这个 Handler 是由 ViewRootImpl 提供的。通过 ViewRootImpl 的 performTraversals() 方法,会调用 dispatchAttachedToWindow 方法,确保所有的 View 都能够获取到 mAttachInfo 对象。

为什么 onCreate 和 onResume 中无法直接获取真实宽高?

原因是,在 onCreate 和 onResume 被回调时,ViewRootImpl 的 performTraversals() 方法尚未执行。而 performTraversals() 负责启动视图树的测量、布局和绘制过程,只有执行完 performLayout() 后,View 才能确定自己的宽高信息。所以在 onCreate 和 onResume 中,无法直接获取到真实的宽高。这也就是开头说到的bug的原因

原文地址:mp.weixin.qq.com/s/gnd8TUs9-…