Android 容易遗漏的刷新小细节

前言

系列文章:
Android Activity创建到View的显示过程
Android Activity 与View 的互动思考
Android invalidate/postInvalidate/requestLayout-彻底厘清
Android 容易遗漏的刷新小细节

之前的文章断断续续有分析过刷新(requestLayout/invalidate)相关的知识,只是那会儿侧重点不同,主要是着眼于整体流程。本篇将着重分析刷新关联的一些小细节。
通过本篇文章,你将了解到:

1、Measure/Layout/Draw 三者关联。
2、requestLayout/Invalidate 作用。
3、Measure/Layout/Draw 阶段执行requestLayout 会发生什么?
4、Measure/Layout/Draw 阶段执行invalidate 会发生什么?
5、如何监听 Measure/Layout/Draw 各个流程?
6、总结

1、Measure/Layout/Draw 三者关联

Android 展示页面简略过程

以Activity 为例:

1、创建Activity并关联Window。
2、构建ViewTree(View 布局形成的树形结构)并关联Window。
3、注册监听屏幕刷新信号。
4、当屏幕刷新信号到来时执行Measure/Layout/Draw 过程。

显而易见,我们应该从第4步着手。

Measure/Layout/Draw 内在联系

假设页面布局结构如下:


image.png

当屏幕刷新信号到来时会执行ViewRootImpl.doTraversal()方法,于是先对整个ViewTree 执行Measure过程,也即是:


image.png

上面的流程图表示调用的时间顺序,并不代表有直接调用的关系。
当ViewTree 完成Measure 过程,说明ViewTree里的每个View(ViewGroup) 的宽、高被确定了。

此时再执行Layout 过程,因为宽、高已知,因此只需要知道摆放的起点,那么终点也将确定。
当ViewTree 完成Layout 过程,说明ViewTree里的每个View(ViewGroup)的四个顶点值确定了(left、top、right、bottom)。

既然位置都确定了,下面的就交个Draw过程,在确定的位置绘制内容即为Draw的主要工作。

三者关系:

1、Measure 为了确认布局的宽、高。
2、Layout 在Measure的基础上确定了布局的起始点、终点。
3、Draw 在Layout 确定的布局边界里绘制指定的内容。

2、requestLayout/invalidate 作用。

requestLayout 作用

上面探讨的是Activity 初次进入时页面的显示过程,可以看出必然要经过Measure/Layout/Draw 过程,此时想要更新页面里的某个元素,那么该如何操作呢?

答案是:requestLayout。
还是以上面的图为例,假若已经更改了View3的尺寸,想要其生效只需要调用View3.requestLayout(),而后将会触发View3.measure(xx),最后触发View3.layout(xx)。
此过程中,View3.onMeasure(xx)/View3.onLayout(xx) 将会被执行。

此时重走了Measure/Layout 过程,因为尺寸发生了改变,因此将会走Draw过程,最终改变尺寸的View3 将会展示在屏幕上。

invalidate 作用

requestLayout 仅仅只是针对布局的宽、高改变、顶点位置发生变化后的刷新,若是想要内容也要刷新,则需要借助invalidate。
当调用View3.invalidate()后,最终将会执行Draw 过程,重新绘制内容。
此过程中,View3.onDraw(xx) 将会被执行。

两者区别:

1、想要重新测量、确定View 宽、高,可以使用requestLayout。
2、想要页面内容重新刷新,可以使用invalidate。
3、调用 requestLayout 后若是发现宽、高发生变化,那么将会触发invalidate。
4、调用invalidate 则不会触发reqeusetLayout(不走Measure/Layout 过程)。

3、Measure/Layout/Draw 阶段执行requestLayout 会发生什么?

一个小例子

public class MyView extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        requestLayout();
        Log.d("fish1", "onMeasure called");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
//        requestLayout();
        Log.d("fish1", "onLayout called");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        requestLayout();
        Log.d("fish1", "onDraw called");
    }
}

MyView 重写了onMeasure(xx)/onLayout(xx)/onDraw(xx) 方法,分别在里面调用requestLayout()。

现在有两个问题:
问题1:当在onMeasure(xx)/onLayout(xx) 里调用requestLayout()后,onMeasure(xx)/onLayout(xx) 还会被执行吗?

从日志结果反馈,onLayout(xx)只在进入页面的时候被执行一次。


image.png

因此,上面的答案是否定的。

问题2:当在onDraw(xx) 里调用requestLayout()后,onMeasure(xx)/onLayout(xx) 还会被执行吗?
日志如下:

image.png

从日志结果反馈,答案是肯定的。

刨根问底

先从onMeasure/onLayout 调用requestLayout()说起。

为啥在onMeasure(xx)/onLayout(xx)里执行requestLayout 没效果呢?这得从requestLayout 源码说起。

#View.java
    public void requestLayout() {
        ...
        //PFLAG_FORCE_LAYOUT 表示需要执行Layout 操作
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //如果父布局Layout 请求已经完成,则可以再次Layout
            mParent.requestLayout();
        }
        ...
    }

最顶级的mParent为ViewRootImpl.java:

#ViewRootImpl.java
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            //标记已经申请Layout
            mLayoutRequested = true;
            //开启三大流程
            scheduleTraversals();
        }
    }

关键之处在于mParent.isLayoutRequested():

#View.java
    public boolean isLayoutRequested() {
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }

问题的重点转为:View 的PFLAG_FORCE_LAYOUT 标记啥时候添加与清除?

View.requestLayout()时会添加PFLAG_FORCE_LAYOUT 标记,而移除的地方在View.layout里:

#View.java
    public void layout(int l, int t, int r, int b) { ;
        ...
        //设置4个顶点的值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //触发子布局Layout
            onLayout(changed, l, t, r, b);
            ...
        }

        final boolean wasLayoutValid = isLayoutValid();

        //清除标记
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        ...
    }

结合添加与移除 标记如下图:


image.png

简要概括:

1、首次执行requestLayout,将会给ViewTree里的各个View 添加标记,表示需要做测量/布局动作。
2、当屏幕刷新信号到来后触发三大流程,进行测量/布局 动作。
3、而我们此时重写了onMeasure(xx)/onLayout(xx),在该方法里调用requestLayout,因为Layout 过程未结束,因此标记没有被清除,最终requestLayout 里判断mParent.isLayoutRequested()=true,说明上一次的Layout 未完成,没必要再次执行。

再看onDraw 里调用requestLayout
当执行onDraw(xx)时,说明Layout 过程已经结束,PFLAG_FORCE_LAYOUT 标记已经被清除,表示Layout 过程已经结束,可以重新开始新的一轮Measure/Layout 过程,因此在onDraw(xx)里执行requestLayout 有效果。

4、Measure/Layout/Draw 阶段执行invalidate 会发生什么?

一个小例子

public class MyView extends View {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onMeasure called");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onLayout called");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onDraw called");
    }
}

直接说结论,从日志反馈分析可知:

1、onMeasure(xx)/onLayout(xx) 里执行invalidate 不会触发Draw 过程。
2、在onDraw(xx) 里执行invalidate 会触发Draw 过程(这也是实现动画的关键)。

寻本溯源

从View.invalidate()开始分析:

#View.java
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                            boolean fullInvalidate) {
        ...
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            ...
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                //清空标记
                mPrivateFlags &= ~PFLAG_DRAWN;
            }
            if (p != null && ai != null && l < r && t < b) {
                ...
                //往上调用,直至ViewRootImpl
                p.invalidateChild(this, damage);
            }
        }
        //不满足,则不会开启Draw 过程
    }

重点查看PFLAG_DRAWN 标记,此处的判断:若是该View 已经绘制过,那么先清除PFLAG_DRAWN标记,然后再往上传递刷新意图,最后执行Draw过程。
若是判断没有PFLAG_DRAWN 标记,说明上一次的Draw 过程没有结束,则无需再次刷新。
问题重点转到PFLAG_DRAWN 标记的清除与添加,其中清除过程已经明了,剩下的看标记啥时候添加上的。
Draw 执行过程:


image.png

在View.draw(x1,x2,x3)里清空标记。

结合添加与移除标记,如下图:


image.png

简要概括:

1、在onMeasure(xx)/onLayout(xx)里调用invalidate,因为此时还没走Draw 过程,因此PFLAG_DRAWN 标记没被添加,在invalidate()内部判断的时候不会再向上传递动作,因此最终没有执行Draw过程。
2、在onDraw(xx)里执行invalidate,因为在View.draw(x1,x2,x3)里已经将PFLAG_DRAWN 标记添加(而后会执行onDraw(xx)),因此在invalidate()内部判断通过,最终将会触发Draw 过程。

5、如何监听 Measure/Layout/Draw 各个流程?

以上是我们通过重写onMeasure(xx)/onLayout(xx)/onDraw(xx)来实现View三大流程监听,在很多时候我们并不需要重写前两者、甚至第三者,或者说我们只关注ViewTree 当前所处的过程而不需要知道具体某个View 所处的过程,因此需要一个外部的机制来监听。
ViewTreeObserver 提供了一系列的接口用来监听各个过程。


image.png

只需要添加对应的Listener 到ViewTreeObserver里,当ViewTree 走到对应的流程时将会回调给Listener,外界可以据此做一些操作。
比如,想要监听Layout 过程:

   textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //可以拿到ViewTree 里所有布局的宽度、起点、终点
                //invalidate()
                //requestLayout()
            }
        });

大家有兴趣可以猜猜此时执行invalidiate()、requestLayout() 会重走三大流程吗?
如果你答对了,说明对刷新的细节之处已经明了。

6、总结

可以看出Android 在设计刷新机制时是做了一些限制的,通过成对的标记来限制一些无意义的频繁调用invalidate/requestLayout。

看完上面的内容,有些同学可能会说:"了解了这有啥用?看了就容易忘~"。
确实,上面的细节分析以前我也接触过一些,后面确实忘了。
后面写了如下代码:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 500) {
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            layoutParams.width = 1000;
            setLayoutParams(layoutParams);
            Log.d("fish1", "onDraw called");            
        }
    }

本意是想在宽度>500时,将宽度提高一倍,最终没能如愿。如果当时知道了这些细节,就会明白setLayoutParams(xx)里调用了requestLayout(),而此时调用requestLayout()是不会触发Measure/Layout 过程的。
原因是:

View.Layout--->View.onSizeChanged(xx)--->View.onLayout(xx)--->OnLayoutChangeListener.onLayoutChange(xx)--->清除PFLAG_FORCE_LAYOUT 标记--->ViewTree的Layout 执行完成后---> OnGlobalLayoutListener. onGlobalLayout()。

以上是各个流程调用的时序。

解决方法:当然是放到标记清除之后的步骤,比如在OnGlobalLayoutListener. onGlobalLayout() 处理重设宽高的逻辑。

其实想表达的意思是:

虽然是一些不起眼的小细节,若是提前知道了可能就会避坑。

本文基于Android 10.0。

刷新演示Demo

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易懂易学系列

你可能感兴趣的:(Android 容易遗漏的刷新小细节)