美文网首页
invalidate、postInvalidate与reques

invalidate、postInvalidate与reques

作者: 维特or卡顿 | 来源:发表于2019-08-14 18:12 被阅读0次

    三者的区别

    先说三个方法的区别:

    1. invalidate只会调onDraw方法且必须在UI线程中调用
    2. postInvalidate只会调onDraw方法,可以再UI线程中回调
    3. requestLayout会调onMeasure、onLayout和onDraw(特定条件下)方法

    invalidate

    我们可以直接在view里使用invalidate()刷新页面,其内部实际是调用invalidate(true)。其方法调用为:

    /**
    * 如果View是visible状态,则重新绘制整个页面,
    * 该方法必须在UI线程中调用
    */
    public void invalidate() {
        invalidate(true);
    }
    /**
    * 这是invalidate()实际发生的地方,一个全刷新会刷新整个视图缓存,但也可部分刷新(只重绘大小不同的的View)
    * invalidateCache入参用来判断是全刷新还是只重绘部分View
    */
    public void invalidate(boolean invalidateCache) {
        //mLeft、mRigth、mTop、mBottom记录的是当前View边界距离其父布局View边界的距离
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    

    invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate)函数代码如下(有删减):

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                boolean fullInvalidate) {
        if (mGhostView != null) {
            //判断是否有图层,有则绘制图层
            mGhostView.invalidate(true);
            return;
        }
        if (skipInvalidate()) {
            return;
        }
        
        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)) {
                //如果需要全部重绘,invalidate()未传参调用时默认为true
                if (fullInvalidate) {
                    mLastIsOpaque = isOpaque();
                    mPrivateFlags &= ~PFLAG_DRAWN;
                }
    
                mPrivateFlags |= PFLAG_DIRTY;
    
                if (invalidateCache) {
                    mPrivateFlags |= PFLAG_INVALIDATED;
                    mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
                }
    
                // 将需要重绘的区域用Rect传递给父View
                final AttachInfo ai = mAttachInfo;
                final ViewParent p = mParent;
                if (p != null && ai != null && l < r && t < b) {
                    final Rect damage = ai.mTmpInvalRect;
                    damage.set(l, t, r, b);
                    p.invalidateChild(this, damage);
                }
    
                // Damage the entire projection receiver, if necessary.
                if (mBackground != null && mBackground.isProjected()) {
                    final View receiver = getProjectionReceiver();
                    if (receiver != null) {
                        receiver.damageInParent();
                    }
                }
            }
    }
    

    在当前View内:

    1. invalidateInternal内首先判断View是否有图层,图层视图在View刷新时也进行刷新,而不是作为当前View的父View的一部分进行刷新
    2. skipInvalidate:确认该View是否需要跳过绘制,需要跳过绘制的条件包括:View不是可见的,存在动画对象,父视图不是ViewGroup或者不是过渡态。
    private boolean skipInvalidate() {
            return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                    (!(mParent instanceof ViewGroup) ||
                            !((ViewGroup) mParent).isViewTransitioning(this));
        }
    
    1. 接着便是判断是否需要重绘,需要确保当前View没有正在执行该方法:满足如下任一一个条件即可
    • 正在动画或者View大小不是0
      
    • 需要完整绘制绘制并且绘制缓存可用
      
    • 未重绘过
      
    • 透明度和上次比较有了变化
      
    1. 根据入参判断是否需要完全重绘,设置绘制缓存不可用的标识到mPrivateFlags。在绘制的时候会根据该标识决定是否使用绘制缓存,如果是完整重绘就跳过绘制缓存和使用绘制缓存的步骤直接去重绘View到canvas。
    2. 接下来的通过调用mParent的invalidateChild方法,来触发父类对重绘区域的调整(可能会调整可能还是原区域)及改区域相对坐标的调整。
      AttachInfo类中存储着View挂载到父View上时的信息、其中的InvalidateInfo包含着当前View要处理的信息,包括要重绘的View和区域的坐标。
      View会调用父View的invalidateChild方法,并将重绘区域传递过去。

    在ViewGroup中

     public final void invalidateChild(View child, final Rect dirty) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null && attachInfo.mHardwareAccelerated) {
                onDescendantInvalidated(child, child);
                return;
            }
    
            ViewParent parent = this;
            if (attachInfo != null) {
                //drawAnimation记录调用该方法的子View是否正在执行动画
                final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
                //调用该方法的子View是否不透明:处于不透明状态且没有在执行动画且变化矩阵没有变化
            //Matrix可以用于View的平移、缩放、扩放、旋转等操作,比如某些应用上的双指缩放功能
                Matrix childMatrix = child.getMatrix();
                final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                        child.getAnimation() == null && childMatrix.isIdentity();
                //标记子View需要重新绘制
                //确保同一时间内不会设置两个标注
                int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
    
                if (child.mLayerType != LAYER_TYPE_NONE) {
                    mPrivateFlags |= PFLAG_INVALIDATED;
                    mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
                }
                //记录子View边界距离父View左边界和上边界的距离到Location中,用于下一段代码中的计算
                final int[] location = attachInfo.mInvalidateChildLocation;
                location[CHILD_LEFT_INDEX] = child.mLeft;
                location[CHILD_TOP_INDEX] = child.mTop;
                //如果子View设置了变换矩阵,则根据变换矩阵调整要刷新区域
                if (!childMatrix.isIdentity() ||
                        (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                    RectF boundingRect = attachInfo.mTmpTransformRect;
                    boundingRect.set(dirty);
                    Matrix transformMatrix;
                    if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                        Transformation t = attachInfo.mTmpTransformation;
                        boolean transformed = getChildStaticTransformation(child, t);
                        if (transformed) {
                            transformMatrix = attachInfo.mTmpMatrix;
                            transformMatrix.set(t.getMatrix());
                            if (!childMatrix.isIdentity()) {
                                transformMatrix.preConcat(childMatrix);
                            }
                        } else {
                            transformMatrix = childMatrix;
                        }
                    } else {
                        transformMatrix = childMatrix;
                    }
                    transformMatrix.mapRect(boundingRect);
                    dirty.set((int) Math.floor(boundingRect.left),
                            (int) Math.floor(boundingRect.top),
                            (int) Math.ceil(boundingRect.right),
                            (int) Math.ceil(boundingRect.bottom));
                }
                //从当前的布局View向上不断遍历当前布局View的父布局,最后遍历到ViewRootImpl
                do {
                    View view = null;
                    //parent可能为ViewGroup类型,也可能为ViewRootImpl类型
                    //最后一次循环执行时为ViewRootImpl类型
                    if (parent instanceof View) {
                        view = (View) parent;
                    }
                    //如果子View正在执行动画,设置遍历的父布局View的动画标识
                    if (drawAnimation) {
                        if (view != null) {
                            view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                        } else if (parent instanceof ViewRootImpl) {
                            ((ViewRootImpl) parent).mIsAnimating = true;
                        }
                    }
                    //设置当前ViewGroup的重绘标识,表示当前的ViewGroup需要重绘
                    if (view != null) {
                        if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                                view.getSolidColor() == 0) {
                            opaqueFlag = PFLAG_DIRTY;
                        }
                        if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                            view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                        }
                    }
                    //调用当前布局View的invalidateChildParent()方法,返回的值为当前布局View的父布局
               //通过循环向上调用,最后返回的根布局是ViewRootImpl对象
                    parent = parent.invalidateChildInParent(location, dirty);
                    if (view != null) {
                        Matrix m = view.getMatrix();
                        if (!m.isIdentity()) {
                            RectF boundingRect = attachInfo.mTmpTransformRect;
                            boundingRect.set(dirty);
                            m.mapRect(boundingRect);
                            dirty.set((int) Math.floor(boundingRect.left),
                                    (int) Math.floor(boundingRect.top),
                                    (int) Math.ceil(boundingRect.right),
                                    (int) Math.ceil(boundingRect.bottom));
                        }
                    }
                } while (parent != null);
            }
        }
    

    在ViewGroup中去执行invalidateChild主要有以下内容:

    1. 初始化变量:
    • 是否在动画中
    • 是否不透明:根据是否需要重绘子View的不透明度,无动画以及子View重绘区域无变化
    • 记录子View相对于父ViewGroup的相对坐标:为了将子View重绘区域与ViewGroup可显示矩阵做相交或者相并处理
    1. 处理子View的舒心区域与ViewGroup变换对象对重绘矩阵的影响:
    • 对ViewGroup设置的静态变换对象做处理,获取设置的变换对象,然后获取其变换矩阵
    • 将child的原矩阵和获取的矩阵做合并处理
    • 将处理之后的矩阵应用到dirty矩阵,同时也将child的变化矩阵应用到dirty矩阵。
    1. do-while循环设置ViewGroup属性同时处理该child的dirty矩阵与ViewGroup可显示矩阵的关系:
      invalidateChildInParent

    循环中有两种实现方式:其中ViewRootImplViewGroup都实现了ViewParent接口,才使得invalidateChildInParent有了两种实现。

    ViewGroup中实现
     public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
            if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
                //如果ViewGroup有没有动画执行或者动画已经完成
                if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                        != FLAG_OPTIMIZE_INVALIDATE) {
                     //dirty记录的是最开始调到invalidate()的View的区域
                    //dirty的四个坐标值值在执行下面代码是相对于当前循环到上一个ViewGroup来确定的
                //这里做了一个偏移动作,偏移的量是当前上一个ViewGroup相对于现在ViewGroup的偏移值
               //做完下面的偏移操作后,dirty的四个坐标就是想对于当前ViewGroup的坐标值了
                    dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                            location[CHILD_TOP_INDEX] - mScrollY);
                     //如果当前ViewGroup需要裁剪View
                //则将当前ViewGroup的区域与View的区域做求并集的操作
                    if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                        dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                    }
    
                    final int left = mLeft;
                    final int top = mTop;
                    //如果当前ViewGroup需要裁剪View,且ViewGroup区域与View区域没有并集,则dirty置空
                    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 {//如果当前ViewGroup中有动画要执行
                    //如果需要对子View裁剪则设置dirty为当前ViewGroup区域
                    //如果不需要则求当前ViewGroup区域与原ditry区域并集
                    if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                        dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
                    } else {
                        // in case the dirty rect extends outside the bounds of this container
                        dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                    }
                    location[CHILD_LEFT_INDEX] = mLeft;
                    location[CHILD_TOP_INDEX] = mTop;
    
                    mPrivateFlags &= ~PFLAG_DRAWN;
                }
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
                if (mLayerType != LAYER_TYPE_NONE) {
                    mPrivateFlags |= PFLAG_INVALIDATED;
                }
    
                return mParent;
            }
    
            return null;
        }
    
    • 首先判断是否存在动画或者动画缓存可用,只有该条件满足才会执行否则直接返回null
    • 已完成动画或者是没有动画时:将dirty位置坐标偏移至相对于父View可视区域原点的坐标,更新location为该ViewGroup相对于其ViewParent的相对坐标。最后返回其ViewParent。
    • 正在动画中:更新location为该ViewGroup相对于其ViewParent的相对坐标
      以上两点的处理是VIew是否动画为主,子View动画会完全影响到其ViewGroup绘制。
      invalidateChildInParent()主要是完成了dirty区域在调用该方法的ViewGroup中的更新,dirty指示的区域就是需要重绘制的区域。如果ViewGroup没有动画在执行,则dirty区域还是原来的区域,只需要通过偏移操作更改该区域的坐标值从相对于上一个ViewGroup(父ViewGroup),到相对于当前ViewGroup;如果有动画要执行,则表示当前整个ViewGroup都需要重绘,更改dirty值为当前ViewGroup 区域。
    ViewRootImpl中实现

    do-while最后一次循环最后会调用到ViewRootImpl.invalidateChildInParent()方法:

    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            //检查是否是UI线程
            checkThread();
            if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
    
            if (dirty == null) {
                //如果重绘区域是null,直接设置dirty为整个大小重绘
                invalidate();
                return null;
            } else if (dirty.isEmpty() && !mIsAnimating) {
                //如果重绘区域是空并且不在动画直接返回null
                return null;
            }
    
            if (mCurScrollY != 0 || mTranslator != null) {
                mTempRect.set(dirty);
                dirty = mTempRect;
                if (mCurScrollY != 0) {
                    //如果ViewRoot存在偏移mCurScrollY转化dirty位置到相对于ViewRoot可视区域的坐标
                    dirty.offset(0, -mCurScrollY);
                }
                if (mTranslator != null) {
                    mTranslator.translateRectInAppWindowToScreen(dirty);
                }
                if (mAttachInfo.mScalingRequired) {
                    dirty.inset(-1, -1);
                }
            }
            invalidateRectOnScreen(dirty);
    
            return null;
        }
    

    这段代码主要做了以下事情:

    • 检查是否是UI线程,如果不是直接抛出异常。
    • 对重绘区域检查处理。
    • 将重绘区域添加到待重绘区域。
    • 重绘任务。
      注意:invalidate()方法介绍中用到的刷新所表达的意思等同于重绘
      改方法调用了:invalidateRectOnScreen方法:
    private void invalidateRectOnScreen(Rect dirty) {
            final Rect localDirty = mDirty;
            if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
                mAttachInfo.mSetIgnoreDirtyState = true;
                mAttachInfo.mIgnoreDirtyState = true;
            }
    
            // Add the new dirty rect to the current one
            localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
            // Intersect with the bounds of the window to skip
            // updates that lie outside of the visible region
            final float appScale = mAttachInfo.mApplicationScale;
            final boolean intersected = localDirty.intersect(0, 0,
                    (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
            if (!intersected) {
                localDirty.setEmpty();
            }
          
            if (!mWillDrawSoon && (intersected || mIsAnimating)) {
                scheduleTraversals();
            }
        }
    

    最后会调用到scheduleTraversals()方法,后续在请求到Vsync信号后,便会调用到peformTraversals()方法。
    view的invalidate不会导致ViewRootImpl的invalidate被调用,而是递归调用父view的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent,然后触发peformTraversals
    更详细的源码分析可以查看大神博客invalidate()源码分析

    postInvalidate

    紧接着来看一下postInvalidate,其使用方法和invalidate类似,直接在View中调用即可,区别是postInvalidate可以再非UI线程里调用。需要注意的是:只有当此视图附加到window时,可以从UI外部调用此方法。

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    //delayMilliseconds为重绘延迟等待的时间
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    

    postInvalidate会调用postInvalidateDelayed方法,该方法接受延迟重绘的时间。而且只有确保视图被添加到窗口的时候才会通知view树重绘,因为这是一个异步方法,如果在视图还未被添加到窗口就通知重绘的话会出现错误。紧接着调用dispatchInvalidateDelayed,该方法定义在ViewRootImpl中:

    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
    

    dispatchInvalidateDelayed实现了一个消息机制,发送了一个异步消息MSG_INVSLIDSTE到主线程,接下来看一下mHandler的实现逻辑:

    final class ViewRootHandler extends Handler {
        public String getMessageName(Message message) {
            switch (message.what) {
                case MSG_INVALIDATE:
                return "MSG_INVALIDATE";
                ......
                return super.getMessageName(message);
            }
        }
         @Override
        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
            if (msg.what == MSG_REQUEST_KEYBOARD_SHORTCUTS && msg.obj == null) {
                    // Debugging for b/27963013
                throw new NullPointerException(
                        "Attempted to call MSG_REQUEST_KEYBOARD_SHORTCUTS with null receiver:");
            }
            return super.sendMessageAtTime(msg, uptimeMillis);
        }
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                break;
                ...
             }   
        }
    }
    

    到此,一切都变得明朗了,参数message传递过来一个View视图的实例,然后直接调用了invalidate方法,继续invalidate流程。
    其实invalidate和postInvalidate的刷新机制是一样的,只不过postInvalidate通过Handler调用了invalidate,使得它可以从非UI线程中调用。

    requestLayout

    requesetLayout只会执行measure和layout流程,不会调用到draw流程来触发重画动作。

    public void requestLayout() {
            if (mMeasureCache != null) mMeasureCache.clear();
            //如果当前的整个View树在进行布局流程的话,则调用requestLayoutDuringLayout()让这次的布局延时执行
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
                // Only trigger request-during-layout logic if this is the view requesting it,
                // not the views in its parent hierarchy
                ViewRootImpl viewRoot = getViewRootImpl();
                if (viewRoot != null && viewRoot.isInLayout()) {
                    if (!viewRoot.requestLayoutDuringLayout(this)) {
                        return;
                    }
                }
                mAttachInfo.mViewRequestingLayout = this;
            }
    
            //PFLAG_FORCE_LAYOUT会在执行View的measure()和layout()方法时判断
            //只有设置过该标志位,才会执行measure()和layout()流程
            mPrivateFlags |= PFLAG_FORCE_LAYOUT;
            mPrivateFlags |= PFLAG_INVALIDATED;
    
            if (mParent != null && !mParent.isLayoutRequested()) {
                //向父容器请求布局
                mParent.requestLayout();
            }
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
                mAttachInfo.mViewRequestingLayout = null;
            }
        }
    

    整个方法流程如下:首先判断当前View树是否正在布局流程,接着为当前子View设置标记位,该标记位的作用就是标记了当前的View是需要进行重新布局的。最后调用父View的requestLayout方法,不断调用父View,直至ViewRootImpl。
    直至ViewRootImpl中的代码如下:

      public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }
    

    首先检查线程,之后和invalidate一样调用scheduleTraversals方法。紧接着便是View绘制流程三大方法,测量、布局、绘制。

    总结

    • invalidate()和postInvalidate()能够触发View的重画,这两个方法最终会调用到performTraversals()中的performDraw()来完成重绘制。
    • invalidate()与postInvalidate()都是用于被调用来触发View的更新(重画)动作,区别在于invalidate()方法是在UI线程自身中使用,而postInvalidate()是非UI线程中使用
    • requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制(onMeasure,onLayout,onDraw)
    • requestLayout会直接递归调用父窗口的requestLayout,直到ViewRootImpl,然后触发peformTraversals,由于mLayoutRequested为true,会导致onMeasure和onLayout被调用。不一定会触发OnDraw
    • requestLayout在layout过程中发现l,t,r,b和以前不一样,就会触发一次invalidate,这种情况下回调用onDraw方法
    • 只要刷新的时候就调用invalidate,需要重新measure就调用requestLayout

    最后,三个方法的源码分析只到scheduleTraversals(),其中的原理涉及到View绘制流程。关于View的绘制流程,关注另一篇文章《Android View的绘制流程》。

    相关文章

      网友评论

          本文标题:invalidate、postInvalidate与reques

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