美文网首页Android技术知识Android自定义View
View—requestLayout、invalidate 和

View—requestLayout、invalidate 和

作者: SharryChoo | 来源:发表于2018-05-29 15:56 被阅读43次

    先说结论

    1. View 的 requestLayout 会回调 onMeasure、onLayout 和 onDraw(ViewGroup.setWillNotDraw(false)的情况下) 方法
    2. invalidate 只会回调 onDraw 方法
    3. postInvalidate 只会回调 onDraw 方法(可以在非 UI 线程中回调)

    梳理一下 scheduleTraversals

    在分析三个方法之前, 先梳理一下 ViewRootImpl.scheduleTraversals, 如果不清楚该方法的作用的话, 请先阅读这篇博文
    https://www.jianshu.com/p/2aac4e679549

    /**
     * ViewRootImpl.performTraversals
     */
    public void performTraversals() {
        // 1.  通过 mLayoutRequested 和 (!mStopped || mReportNextDraw) 构造 layoutRequested 变量
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        
        // 2. 判断 window 是否需要改变, 若 layoutRequested 为 false, windowShouldResize 也为 false
        boolean windowShouldResize = layoutRequested && windowSizeMayChange
            && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
                || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.width() < desiredWindowWidth && frame.width() != mWidth)
                || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.height() < desiredWindowHeight && frame.height() != mHeight));
    
        // 3. 其中一个满足即可进入这个 if 语句
        if (mFirst || windowShouldResize || insetsChanged ||
                    viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            /// 3.1 判断 View 的宽高是否有改变, 没有的话, 不会调用 performMeasure
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                            || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                            updatedConfiguration) {
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                // 3.1.1 执行 performMeasure
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                // 3.1.2 改变了 layoutRequested 的值
                layoutRequested = true;
            }
        }
        
        // 4. 若执行了 performMeasure, 一般情况下也会执行 performLayout
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
        }
        
        // 5. 判断是否取消了绘制
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            // 5.1 满足条件则执行 performDraw()
            performDraw();
        } else {
            if (isViewVisible) {
                // Try again
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }
        // Traversals 结束
        mIsInTraversal = false;
    }
    

    梳理了 ViewRootImpl.performTraversals 之后, 可以看到对于 View 绘制三大流程中的前两个,

    • performMeasure 和 performLayout 限制条件是非常多的
      • 起到决定性作用的是 mLayoutRequested 这个 Flag
      • performMeasure 执行后会将 layoutRequested 变更为 true, 不出意外的话, performLayout 会紧接着执行, 都重新测量了, 显然要重新进行控件摆放
    • performDraw(); 的执行不受 mLayoutRequested 的影响, 这可能是 invalidate 只会执行 onDraw() 的原因

    这里大胆猜想一下 View.requestLayout 时 mLayoutRequested 这个 Flag 会变更为 true, 接下来带着问题来看看 View.requestLayout 的过程

    View.requestLayout

    1. 为了防止直接看 requestlayout 导致身体不适, 这里分析 requestLayout 之前, 先分析 ViewRootImpl.performLayout() 搞清楚 layout 的两个阶段
        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            /******************performLayout 的第一阶段*******************/
            mLayoutRequested = false;
            mScrollMayChange = true;
            // layout 执行标记位
            mInLayout = true;
            final View host = mView;//mView 为 DecorView
            if (host == null) return;
            try {
                // 1.1 执行第一次 layout 
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                // layout结束标记位
                mInLayout = false;
                
                /******************performLayout 的第二阶段*******************/
                // 检查在进行 layout 的过程中, 是否有 View 调用了 requestLayout 方法
                int numViewsRequestingLayout = mLayoutRequesters.size();
                if (numViewsRequestingLayout > 0) {
                    // mLayoutRequesters 就是在 mInLayout = true 的过程中 requestLayout 的 view 集合
                    // 在 ViewRootImpl.requestLayoutDuringLayout 代码中有所体现
                    ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false);
                    if (validLayoutRequesters != null) {
                        // 2.1 将 mHandlingLayoutInLayoutRequest 标记为 true
                        mHandlingLayoutInLayoutRequest = true;
                        // 处理新的布局请求,然后测量和布局
                        int numValidRequests = validLayoutRequesters.size();
                        for (int i = 0; i < numValidRequests; ++i) {
                            final View view = validLayoutRequesters.get(i);
                            // 在第一次 layout 执行结束后, 运行第二次 layout 请求
                            view.requestLayout();
                        }
                        // 2.2 执行二次 Measure
                        measureHierarchy(host, lp, mView.getContext().getResources(),
                                desiredWindowWidth, desiredWindowHeight);
                        // 2.3 执行二次 layout
                        mInLayout = true;
                        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                        // 2.4 将 mHandlingLayoutInLayoutRequest 标记为 false
                        mHandlingLayoutInLayoutRequest = false;
                        // 2.5 第三次检查 view 的 requestLayout 请求
                        validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                        if (validLayoutRequesters != null) {
                            final ArrayList<View> finalRequesters = validLayoutRequesters;
                            getRunQueue().post(new Runnable() {
                                @Override
                                public void run() {
                                    int numValidRequests = finalRequesters.size();
                                    for (int i = 0; i < numValidRequests; ++i) {
                                        final View view = finalRequesters.get(i);
                                        // 2.5.1 在第二次 layout 的过程中, 将请求 post 出去
                                        view.requestLayout();
                                    }
                                }
                            });
                        }
                    }
    
                }
            }
            // 2.5 第二次 layout 结束的标志
            mInLayout = false;
        }
    

    可以看到在 performLayout 的过程中,

    • 第一阶段: 进行正常的 layout 操作
    • 第二阶段
      • 检查 mLayoutRequesters 集合, 逐一调用 view.requestLayout()
      • 进行二次 Measure
      • 进行二次 Layout
      • 再检查 mLayoutRequesters 集合, 若还有值, 则 post 到下一帧执行
    1. 再分析 View 的 requestLayout
        /**
         * View.requestLayout
         */
        public void requestLayout() {
            if (mMeasureCache != null) mMeasureCache.clear();
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout  == null) {
                ViewRootImpl viewRoot = getViewRootImpl();
                // 1. 判断当前 ViewRootImpl 是否正在 Layout
                if (viewRoot != null && viewRoot.isInLayout()) {// isInLayout() 为 true 的条件是 mInLayout = true
                    // 1.1 判断当前 View 是否可以在 ViewRootImpl 正在进行 Layout 时, 继续执行发起 requestLayout
                    if (!viewRoot.requestLayoutDuringLayout(this)) {
                        // 返回 false , 则说明不允许, 则直接让此次 View.requestLayout() return
                        return;
                    }
                }
                // 2 将自己的状态标记为正在 requestLayout
                mAttachInfo.mViewRequestingLayout = this;
            }
            
            // 3. 给当前 View 打上标记 PFLAG_FORCE_LAYOUT | PFLAG_INVALIDATED
            mPrivateFlags |= PFLAG_FORCE_LAYOUT;
            mPrivateFlags |= PFLAG_INVALIDATED;
            
            // 4. 尝试调用父容器的 requestLayout
            if (mParent != null && !mParent.isLayoutRequested()) {
                // 4.1 调用 ViewParent 的 requestLayout, 即调用父容器的 requestLayout
                // ViewGroup 中并没有重写 requestLayout 方法, 即还会调用到 View 的 requestLayout 方法中去
                // 此方法会回溯到 当前 Window 的 ViewRootImpl 中的 requestLayout 中去
                mParent.requestLayout();
            }
            
            // 5. 执行到这里, 说明当前 View 的 requestLayout 已经完成, 将 mAttachInfo.mViewRequestingLayout 置空
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
                mAttachInfo.mViewRequestingLayout = null;
            }
        }
        
        /**
         * ViewRootImpl.requestLayoutDuringLayout
         */
        boolean requestLayoutDuringLayout(final View view) {
            // 1.1.1 说明传入的 view 即为 ViewRootImpl, 因为只有它的 mParent 为null
            if (view.mParent == null || view.mAttachInfo == null) {
                // Would not normally trigger another layout, so just let it pass through as usual
                return true;
            }
            // 1.1.2 将请求 Layout 的 View 添加到 ViewRootImpl 中维护的集合 mLayoutRequesters 中
            if (!mLayoutRequesters.contains(view)) {
                mLayoutRequesters.add(view);
            }
            if (!mHandlingLayoutInLayoutRequest) {
                // 1.1.3  mHandlingLayoutInLayoutRequest 为 false
                // 说明当前 ViewRootImpl 的 performLayout() 没有进行 second layout, 它将会在第二次 layout 中执行
                return true;
            } else {
                // 1.1.4  mHandlingLayoutInLayoutRequest 为 true
                // 说明当前 ViewRootImpl 的 performLayout() 正在进行 second layout, 此时 view 的 requestLayout 会被 post 到下一帧
                // return false; 由上面代码可知 View 的 requestLayout() 请求会被直接 return;
                return false;
            }
        }
        
        /**
         * View.isLayoutRequested
         */
        public boolean isLayoutRequested() {
            // 当 View 进行过 requestLayout() 时, mPrivateFlags |= PFLAG_FORCE_LAYOUT;
            // 则会返回 true
            return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        }
        
        /**
         * ViewRootImpl.requestLayout
         * View.requestLayout() 最终的走向
         */
        @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                // 将当前 mLayoutRequested 标记为 true, 该 Flag 会直接影响到 performTraversals 是否执行 measure 和 layout 
                mLayoutRequested = true;
                // 这里又重新开启了 View 绘制的三大流程
                scheduleTraversals();
            }
        }
    

    从上述代码可知 View.requestLayout 方法主要做了以下事情

    • mAttachInfo.mViewRequestingLayout == null 成立时
      • 给自己打上标签, 即让 mAttachInfo.mViewRequestingLayout == this;
      • 判断当前 ViewRootImpl 是否正在处理 Layout (mInLayout 是否为 true, 该变量在 performLayout 中会被改变)
        • 调用 ViewRootImpl.requestLayoutDuringLayout 方法来分配 view 的 requestLayout 走向
          • 将 requestLayout 的 view 添加到 ViewRootImpl 的集合 mLayoutRequesters 中
          • 根据 ViewRootImpl.mHandlingLayoutInLayoutRequest 判断处于 performLayout() 的第几阶段
            • 若是第二阶段, 则返回 false, 此时 View.reqeustlayout 请求则会被抛到下一帧执行
            • 若不是第二阶段, 则返回 true, 此时 View.requestLayout 请求会在 performLayout 的第二阶段执行
    • 给 View 设置 PFLAG_FORCE_LAYOUT | PFLAG_INVALIDATED 两个 Flags
    • 尝试调用 parent 的 requestLayout() 方法
      • mLayoutRequested = true; 果不其然, 这里将这个 Flag 设置为了 true, 也印证了上面的猜想
      • 回溯到顶部, 最终会调用 ViewRootImpl 中重写的 requestLayout 发起 scheduleTraversals();

    View.invalidate

    View.invalidate 方法最会调用到 ViewGroup.invalidateChild 中

        /** 
         * ViewGroup.invalidateChild
         */
        public final void invalidateChild(View child, final Rect dirty) {
            final AttachInfo attachInfo = mAttachInfo;
            // child 的 parent 指定为自身
            ViewParent parent = this;
            if (attachInfo != null) {
                do {
                    View view = null;
                    if (parent instanceof View) {
                        view = (View) parent;
                    }
                    // 会递归的调用 parent 的 invalidateChildInParent 方法 
                    parent = parent.invalidateChildInParent(location, dirty);
                } while (parent != null);
            }
        }
    

    可以看到 View.invalidate 方法会递归的调用 parent.invalidateChildInParent 方法, 直至回溯到 ViewRootImpl 中, 与 requestLayout 如出一辙, ViewRootImpl 重写了 invalidateChildInParent 方法, 接下来看看, 它做了什么

        /** 
         * ViewRootImpl.invalidateChildInParent
         */
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            checkThread();
            if (dirty == null) {
                // 1. 直接调用了 invalidate 方法
                invalidate();
                return null;
            } else if (dirty.isEmpty() && !mIsAnimating) {
                return null;
            }
    
            if (mCurScrollY != 0 || mTranslator != null) {
                mTempRect.set(dirty);
                dirty = mTempRect;
                if (mCurScrollY != 0) {
                    dirty.offset(0, -mCurScrollY);
                }
                if (mTranslator != null) {
                    mTranslator.translateRectInAppWindowToScreen(dirty);
                }
                if (mAttachInfo.mScalingRequired) {
                    dirty.inset(-1, -1);
                }
            }
            // 2.调用了 invalidateRectOnScreen 方法, 刷新区域内的视图
            invalidateRectOnScreen(dirty);
    
            return null;
        }
        
        /** 
         * ViewRootImpl.invalidate
         */
        void invalidate() {
            mDirty.set(0, 0, mWidth, mHeight);
            if (!mWillDrawSoon) {
                // 看到了最熟悉的 scheduleTraversals
                scheduleTraversals();
            }
        }
        
        /** 
         * ViewRootImpl.invalidateRectOnScreen
         */
        private void invalidateRectOnScreen(Rect dirty) {
            final Rect localDirty = mDirty;
            if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
                mAttachInfo.mSetIgnoreDirtyState = true;
                mAttachInfo.mIgnoreDirtyState = true;
            }
            localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
            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();
            }
        }
    

    看到 ViewRootImpl 中 invalidateChildInParent 最终都回调了 scheduleTraversals 方法, 开启了 View 绘制的三大流程

    postInvalidate

        /**
         * View.postInvalidate
         */
        public void postInvalidate() {
            postInvalidateDelayed(0);
        }
        
        /**
         * View.postInvalidateDelayed
         */
        public void postInvalidateDelayed(long delayMilliseconds) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                // 到 ViewRootImpl 中分发
                attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
            }
        }
    

    可以看到 View 调用 postInvalidate 时, 最终会流入 ViewRootImpl 中, 接下里看看 ViewRootImpl 做了哪些操作

        /**
         * ViewRootImpl.dispatchInvalidateDelayed
         */
        public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
            Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
            mHandler.sendMessageDelayed(msg, delayMilliseconds);
        }
    
    
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                // 在 mHandler 绑定的线程中调用了 View 的 invalidate
                ((View) msg.obj).invalidate();
                break;
        }
    

    可以看到 View.postInvalidate 本质上还是调用了 View.invalidate(), 它在调用之前会加入消息队列, 投递到 Handler 创建线程去执行, 也就是在非 UI 线程我们想重新绘制时, 我们可以采用 postInvalidate 这种方式

    总结

    1. View 的 requestLayout 和 invalidate 处理的方式很相似, 都是从当前 View 回溯到 ViewRootImpl 中去调用 scheduleTraversals 重新开启 View 绘制的三大流程
    2. requestLayout 会通过 mLayoutRequested 这个 Flag 来控制是否执行 performMeasure 和 performLayout
    3. invalidate 时 mLayoutRequested 为 false, 故不会执行 performMeasure 和 performLayout, 只会执行 performDraw, 当然 performDraw 的执行也是右条件限制的, 不过与 mLayoutRequested 无关
    4. postInvalidate 与 invalidate 本质上没什么不同, 只不过 postInvalidate 可以在非 UI 创建线程中去通知 View 重绘, 当然了原理及是 Android 消息机制

    不得不叹服SDK开发者的技巧, 其中有很多细节都没有兼顾到, 只是为了厘清 requestLayout, invalidate, postInvalidate 这三者的工作流程, 与部分细节, 可能有很多不到位的地方, 希望能够批评指出

    相关文章

      网友评论

      • 过期的薯条:可以,分析的很细
      • wethereornot:很强,可以从反面去验证mLayoutRequested 这个Flag 起到的作用,受教了!

      本文标题:View—requestLayout、invalidate 和

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