Android invalidate/postInvalidat

作者: 小鱼人爱编程 | 来源:发表于2021-11-12 13:19 被阅读0次

    前言

    前几篇分析了Measure、Layout、Draw 过程,这三个过程在第一次展示View的时候都会调用。那之后更改了View的属性呢?比如更改颜色、更换文字内容、更换图片等,还会走这三个过程吗?循着这个思路,来分析Invalidate/RequestLayout流程。
    通过本篇文章,你将了解到:

    1、Invalidate 流程
    2、RequestLayout 流程
    3、Invalidate/RequestLayout 使用场合
    4、子线程真不能绘制UI吗
    5、postInvalidate 流程

    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流程:


    image.png

    上述分析了硬件加速绘制与软件绘制时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流程:


    image.png

    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 过程更深入的分析,请移步:

    用图表示requestLayout过程:


    image.png

    Invalidate/RequestLayout 使用场合

    结合requestLayout和invalidate与View三大流程关系,有如下图:


    image.png

    总结一下:

    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 10.0

    如果您喜欢,请点赞/关注,您的鼓励是我前进的动力。

    相关文章

      网友评论

        本文标题:Android invalidate/postInvalidat

        本文链接:https://www.haomeiwen.com/subject/djxozltx.html