弄清楚Android invalidate、postInvalidate、requestLayout作用

Invalidate 流程

一个小Demo
public class MyView extends View {

    private Paint paint;
    private @ColorInt int color = Color.RED;

    public MyView(Context context) {
        super(context);
        init();
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //涂红色
        canvas.drawColor(color);
    }

    public void setColor(@ColorInt int color) {
        this.color = color;
        invalidate();
    }
}

MyView 默认展示一块红色的矩形区域,暴露给外界的方法:setColor
用以改变绘制的颜色。颜色改变后,需要重新执行onDraw(xx)才能看到改变后的效果,通过invalidate()方法触发onDraw(xx)调用。
接下来看看invalidate()方法是怎么触发onDraw(xx)方法执行的。

invalidate() 调用栈

invalidate顾名思义:使某个东西无效。在这里表示使当前绘制内容无效,需要重新绘制。当然,一般来说常常简单称作:刷新。
invalidate()是View.java 里的方法。

#View.java
    public void invalidate() {
        invalidate(true);
    }

    public void invalidate(boolean invalidateCache) {
        //invalidateCache 使绘制缓存失效
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }


    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                            boolean fullInvalidate) {
        ...
        //设置了跳过绘制标记
        if (skipInvalidate()) {
            return;
        }

        //PFLAG_DRAWN 表示此前该View已经绘制过 PFLAG_HAS_BOUNDS表示该View已经layout过,确定过坐标了
        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) {
                //默认true
                mLastIsOpaque = isOpaque();
                //清除绘制标记
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            //需要绘制
            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                //1、加上绘制失效标记
                //2、清除绘制缓存有效标记
                //这两标记在硬件加速绘制分支用到
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }
            
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //记录需要重新绘制的区域 damge,该区域为该View尺寸
                damage.set(l, t, r, b);
                //p 为该View的父布局
                //调用父布局的invalidateChild
                p.invalidateChild(this, damage);
            }
            ...
        }
    }

从上可知,当前要刷新的View确定了刷新区域后即调用了父布局的invalidateChild(xx)方法。该方法为ViewGroup里的final方法。

#ViewGroup.java
    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            //1、如果是支持硬件加速,则走该分支
            onDescendantInvalidated(child, child);
            return;
        }
        //2、软件绘制
        ViewParent parent = this;
        if (attachInfo != null) {
            //动画相关,忽略
            ...
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
                ...
                parent = parent.invalidateChildInParent(location, dirty);
                //动画相关
            } while (parent != null);
        }
    }

由上可知,在该方法里区分了硬件加速绘制与软件绘制,分别来看看两者区别:

硬件加速绘制分支

如果该Window支持硬件加速,则走下边流程:

#ViewGroup.java
    public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
        mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
        
        if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
           //此处都会走
            mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
            //清除绘制缓存有效标记
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }
        
        if (mLayerType == LAYER_TYPE_SOFTWARE) {
            //如果是开启了软件绘制,则加上绘制失效标记
            mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
            //更改target指向
            target = this;
        }

        if (mParent != null) {
            //调用父布局的onDescendantInvalidated
            mParent.onDescendantInvalidated(this, target);
        }
    }

onDescendantInvalidated 方法的目的是不断向上寻找其父布局,并将父布局PFLAG_DRAWING_CACHE_VALID 标记清空,也就是绘制缓存清空。
而我们知道,根View的mParent指向ViewRootImpl对象,因此来看看它里面的onDescendantInvalidated()方法:

#ViewRootImpl.java
    @Override
    public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
        // TODO: Re-enable after camera is fixed or consider targetSdk checking this
        // checkThread();
        if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            mIsAnimating = true;
        }
        invalidate();
    }

    @UnsupportedAppUsage
    void invalidate() {
        //mDirty 为脏区域,也就是需要重绘的区域
        //mWidth,mHeight 为Window尺寸
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //开启View 三大流程
            scheduleTraversals();
        }
    }

做个小结:

1、invalidate() 对于支持硬件加速来说,目的就是寻找需要重绘的View。当前View肯定是需要重绘的,继续递归寻找其父布局直至到根View。
2、如果该View需要重绘,则加上PFLAG_INVALIDATED 标记。
3、设置重绘区域。

用图表示硬件加速绘制的invaldiate流程:

软件绘制分支

如果该Window不支持硬件加速,那么走软件绘制分支:
parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不为空那么一直调用invalidateChildInParent(xx),实际上这也是遍历ViewTree过程,来看看关键invalidateChildInParent(xx):

#ViewGroup.java
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        //dirty 为失效的区域,也就是需要重绘的区域
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            //该View绘制过或者绘制缓存有效
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                    != FLAG_OPTIMIZE_INVALIDATE) {
                //修正重绘的区域
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                    //如果允许子布局超过父布局区域展示
                    //则该dirty 区域需要扩大
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }
                final int left = mLeft;
                final int top = mTop;
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    //默认会走这
                    //如果不允许子布局超过父布局区域展示,则取相交区域
                    if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                        dirty.setEmpty();
                    }
                }
                //记录偏移,用以不断修正重绘区域,使之相对计算出相对屏幕的坐标
                location[CHILD_LEFT_INDEX] = left;
                location[CHILD_TOP_INDEX] = top;
            } else {
                ...
            }
            //标记缓存失效
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            if (mLayerType != LAYER_TYPE_NONE) {
                //如果设置了缓存类型,则标记该View需要重绘
                mPrivateFlags |= PFLAG_INVALIDATED;
            }
            //返回父布局
            return mParent;
        }
        return null;
    }

与硬件加速绘制一致,最终调用ViewRootImpl invalidateChildInParent(xx),来看看实现:

#ViewRootImpl.java
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
        if (dirty == null) {
            //脏区域为空,则默认刷新整个窗口
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
        ...
        invalidateRectOnScreen(dirty);
        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        //合并脏区域,取并集
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        ...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            //开启View的三大绘制流程
            scheduleTraversals();
        }
    }

做个小结:

1、invalidate() 对于软件绘制来说,目的就是寻找需要重绘的区域。
2、确定重绘的区域在Window里的位置,该区域需要重新绘制。

用图表示软件绘制invalidate流程:

上述分析了硬件加速绘制与软件绘制时invalidate的不同,它们的最终目的都是为了重走Draw过程。重走Draw过程通过调用scheduleTraversals() 触发的,来看看是如何触发的。

想了解更多硬件加速绘制请移步:Android 自定义View之Draw过程(中)

触发Draw过程

scheduleTraversals 详细分析在这篇文章:Android Activity创建到View的显示过程

三大流程真正开启在ViewRootImpl->performTraversals(),在该方法里根据一定的条件执行了Measure(测量)、Layout(摆放)、Draw(绘制)。
本次着重分析如何触发Draw过程。

#ViewRootImpl.java
    private void performDraw() {
        ...
        try {
            //调用draw 方法
            boolean canUseAsync = draw(fullRedrawNeeded);
            ...
        } finally {
            mIsDrawing = false;
        }
        ...
    }

    private boolean draw(boolean fullRedrawNeeded) {
        //mSurface 在ViewRootImpl 构建的时候创建
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return false;
        }
        ...
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            //invalidate 时,dirty就已经被赋值
            //满足其中一个条件即可,重点关注第一个条件
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
                ...
                //硬件加速绘制
                mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
            } else {
                ...
                //软件绘制
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
            }
        }
        ...
        return useAsyncReport;
    }

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                                 boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
        final Canvas canvas;
        try {
            //dirty 为需要绘制的区域
            //invalidate 指定的刷新区域会影响canvas绘制区域
            canvas = mSurface.lockCanvas(dirty);
        } catch (Surface.OutOfResourcesException e) {
            return false;
        } catch (IllegalArgumentException e) {
            return false;
        } finally {
        }
        try {
            //ViewTree 开始绘制
            mView.draw(canvas);
        } finally {
            ..
        }
        return true;
    }

可以看出,invalidate 最终触发了Draw过程。

1、不管是硬件加速绘制还是软件绘制,都会设置重绘的矩形区域。对于硬件加速绘制来说,重绘的区域为整个Window的大小。而对于软件绘制则是设置相交的矩形区域。
2、只要重绘区域不为空,那么当开启三大流程时,Draw过程必然被调用。
3、对于硬件加速绘制来说,通过绘制标记控制需要重绘的View,因此当我们调用view.invalidate()时,该view被设置了重绘标记,在Draw过程里该view draw(xx)被调用。当然如果其父布局设置了软件缓存,则其父布局也需要被重绘,父布局下的子布局也需要重绘。
4、对于软件绘制来说,整个ViewTree的Draw过程都会被调用,只是Canvas仅仅绘制重绘区域指定的矩形区域。

可以看出,启用硬件加速绘制可以避免不必要的绘制。
关于硬件加速绘制与软件绘制详细区别,请移步系列文章:
Android 自定义View之Draw过程(上)

最后,用图表示invalidate流程:

RequestLayout 流程

顾名思义,重新请求布局。
来看看View.requestLayout()方法:

#View.java
    public void requestLayout() {
        //清空测量缓存
        if (mMeasureCache != null) mMeasureCache.clear();
        ...
        //添加强制layout 标记,该标记触发layout
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        //添加重绘标记
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //如果上次的layout 请求已经完成
            //父布局继续调用requestLayout
            mParent.requestLayout();
        }
        ...
    }

可以看出,这个递归调用和invalidate一样的套路,向上寻找其父布局,一直到ViewRootImpl为止,给每个布局设置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED标记。
查看ViewRootImpl requestLayout()

#ViewRootImpl.java
    public void requestLayout() {
        //是否正在进行layout过程
        if (!mHandlingLayoutInLayoutRequest) {
            //检查线程是否一致
            checkThread();
            //标记有一次layout的请求
            mLayoutRequested = true;
            //开启View 三大流程
            scheduleTraversals();
        }
    }


很明显,requestLayout目的很单纯:

1、向上寻找父布局、并设置强制layout标记
2、最终开启三大绘制流程

和invalidate一样的配方,当刷新信号来到之时,调用doTraversal()->performTraversals(),而在performTraversals()里真正执行三大流程。

#ViewRootImpl.java
    private void performTraversals() {
        //mLayoutRequested 在requestLayout时赋值为true
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            //measure 过程
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }
        ...

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {
            //layout 过程
            performLayout(lp, mWidth, mHeight);
        }
        ...
    }

由此可见:

1、requestLayout 最终将会触发Measure、Layout 过程。
2、由于没有设置重绘区域,因此Draw 过程将不会触发。

之前设置的PFLAG_FORCE_LAYOUT标记有啥用呢?
回忆一下measure 过程:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        //requestLayout时,PFLAG_FORCE_LAYOUT 标记被设置
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        ...
        if (forceLayout || needsLayout) {
            ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            } else {
                ...
            }
            ...
            }
        }
    }


PFLAG_FORCE_LAYOUT 标记打上之后,会触发onMeasure()测量自身及其子布局。

试想一下,假设View的尺寸改变了,变大了,那么调用了requestLayout后因为走了Measure、Layout 过程,测量、摆放倒是重新设置了,但是不调用Draw出不来效果啊。实际上,View layout时候已经考虑到了。
在View.layout(xx)->setFrame(xx)里

#View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //尺寸发生改变 调用invalidate 传入true,否则传入false
            invalidate(sizeChanged);
            ...
        }
        ...
        return changed;
    }


也就是说:

1、requestLayout调用后,可能会触发invalidate。
2、若是触发了invalidate(),不管传入true还是false,都会走重绘流程。

关于measure、layout 过程更深入的分析,请移步:
Android 自定义View之Measure过程
Android 自定义View之Layout过程

用图表示requestLayout过程:


Invalidate/RequestLayout 使用场合
结合requestLayout和invalidate与View三大流程关系,有如下图:

总结一下:

1、invalidate调用后只会触发Draw 过程。
2、requestLayout 会触发Measure、Layout过程,如果尺寸发生改变,则会调用invalidate。
3、当涉及View的尺寸、位置变化时使用requestLayout。
4、当仅仅需要重绘时调用invalidate。
5、如果不确定requestLayout 是否触发invalidate,可在requestLayout后继续调用invalidate。

上面仅仅说明了单个布局Invalidate/RequestLayout联系,那么如果父布局调用了invalidate,那么子布局会走重绘过程吗?接下来列举这些关系。

子布局/父布局 Invalidate/RequestLayout 关系

子布局Invalidate

如果是软件绘制或者父布局开启了软件缓存绘制,父布局会走重绘过程(前提是WILL_NOT_DRAW标记没设置)。

子布局RequestLayout

父布局会重走Measure、Layout过程。

父布局Invalidate

如果是软件绘制,则子布局会走重绘过程。

父布局RequestLayout

如果父布局尺寸发生了改变,则会触发子布局Measure过程、Layout过程。

子线程真不能绘制UI吗

在Activity onCreate里创建子线程并展示对话框:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_group);

        new Thread(new Runnable() {
            @Override
            public void run() {
                TextView textView = new TextView(MainActivity.this);
                textView.setText("hello thread");
                Looper.prepare();
                Dialog dialog = new Dialog(MainActivity.this);
                dialog.setContentView(textView);
                dialog.show();
                Looper.loop();
            }
        }).start();
    }

答案是可以的,接下来分析为什么可以。

在分析ViewRootImpl里requestLayout/invalidate过程中,发现其内部调用了checkThread()方法:

#ViewRootImpl.java
    void checkThread() {
        //当前调用线程与mThread不是同一线程则会抛出异常
        if (mThread != Thread.currentThread()) {
            //简单翻译来说:只有创建了ViewTree的线程才能操作里边的View
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

问题的关键是mThread是什么?从哪里来?

#ViewRootImpl.java
    public ViewRootImpl(Context context, Display display) {
        ...
        //mThread 为Thread类型
        //也就是说哪个线程执行了构造ViewRootImpl对象,那么mThread就是指向那个线程
        mThread = Thread.currentThread();
        ...
    }

而创建ViewRootImpl对象是在调用WindowManager.addView(xx)过程中创建的。
关于WindowManager/Window 请移步:Window/WindowManager 不可不知之事

现在回过头来看Dialog创建就比较明朗了:

1、dialog.show() 调用WindowManager.addView(xx),此时是子线程调用,因此ViewRootImpl对象是在子线程调用的,进而mThread指向子线程。
2、当ViewRootImpl对象构建成功后,调用其setView(xx)方法,里面调用了requestLayout,此时还是子线程。
3、checkThread()判断是同一线程,因此不会抛出异常。

实际上,“子线程不能更新ui” 更合理的表述应为:View只能被构建了ViewTree的线程操作。只是通常来说,Activity 构建ViewTree的线程被称作UI(主)线程,因此才会有上述说法。

postInvalidate 流程

既然invalidate()只能主线程调用(硬件加速条件下,不调用checkThread()),那如果想在子线程调用呢?当然想到的是先通过Handler切换到主线程,再执行invalidate(),但是每次这么写有点冗余,幸好,View里提供了postInvalidate:

#View.java
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            //还是靠ViewRootImpl
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }


切到ViewRootImpl.java

#ViewRootImpl.java
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        //此处Message.obj = view
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_INVALIDATE:
                //obj 即为待刷新的View
                ((View) msg.obj).invalidate();
                break;
                ...
        }
    }


发现了真相:

postInvalidate 通过ViewRootImpl 里的handler切换到UI线程,最终执行invalidate()。
ViewRootImpl 里的hanlder绑定的线程即是UI线程。


Android 容易遗漏的刷新小细节

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

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

1、Measure/Layout/Draw 三者关联

Android 展示页面简略过程
以Activity 为例:

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

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

Measure/Layout/Draw 内在联系
假设页面布局结构如下:


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


上面的流程图表示调用的时间顺序,并不代表有直接调用的关系。
当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)只在进入页面的时候被执行一次。


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

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

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

刨根问底

先从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;
        ...
    }

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


简要概括:

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 执行过程:


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

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


简要概括:

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 提供了一系列的接口用来监听各个过程。

只需要添加对应的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地址:https://github.com/fishforest/JetpackDemo/tree/master/app/src/main/java/com/fish/jetpackdemo/uiflush

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值