Android 自定义控件 requestLayout / invalidate

862 阅读3分钟

Read The Fucking Source Code

引言

Android自定义控件涉及View的绘制分发流程

源码版本(Android Q — API 29)

前置依赖 【Android 绘制流程】

1. requestLayout

1.1 requestLayout 过程详解

1.2 requestLayout 自底向上

1.2.1 我们来看 View 中的 requestLayout() 方法

public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        //View树正在进行布局流程
        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()) {
                //ViewRootImpl是否会截获处理布局(已经在不居过程中),后面会有说明
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        //View设置标记位,需要重新布局
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        //向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

1.2.2 我们来看 ViewRootImpl 中的 requestLayoutDuringLayout() 方法

//返回值决定请求布局发起方是否进行递归布局申请,fase:中断布局请求;ture:布局请求继续执行。
boolean requestLayoutDuringLayout(final View view) {
        if (view.mParent == null || view.mAttachInfo == null) {
            // Would not normally trigger another layout, so just let it pass through as usual
            return true;
        }
        //正在布局的视图包含请求view,则添加
        if (!mLayoutRequesters.contains(view)) {
            mLayoutRequesters.add(view);
        }
        //是否允许在不居中申请布局请求
        if (!mHandlingLayoutInLayoutRequest) {
            // Let the request proceed normally; it will be processed in a second layout pass
            // if necessary
            return true;
        } else {
            // Don't let the request proceed during the second layout pass.
            // It will post to the next frame instead.
            return false;
        }
    }

1.2.3 ViewGroup 中没有复写 requestLayout 方法,所以 ViewGroup 也是调用和 View 同样的方法

1.2.4 我们来看 ViewRootImpl 中的 requestLayout() 方法

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //线程检查
            checkThread();
            mLayoutRequested = true;
            //请求注册Vsync信号触发绘制
            scheduleTraversals();
        }
    }

1.2.5 requestLayout() 小结

 requestLayout 事件层层向上传递,直到 DecorView(即根 View),又会传递给 ViewRootImpl(ViewRootImpl 接管了 DecorView 的 ViewParent 功能),也即是说子 View 的requestLayout 事件,最终会被 ViewRootImpl 接收并得到处理。纵观这个向上传递的流程,其实是采用了责任链模式,即不断向上传递该事件,直到找到能处理该事件的上级,作为 View 的外交大管家,只有 ViewRootImpl 能够处理 requestLayout 事件。

2. invalidate

2.1 invalidate 过程概览

View绘制流程分发-invalidate

2.2 invalidate 自底向上

2.2.1 我们来看 View 中的 invalidate() 方法

public void invalidate() {
        invalidate(true);
    }

public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

2.2.2 我们来看 View 中的 invalidateInternal() 方法

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        //代码省略……

        //这里判断该子View是否可见或者是否处于动画中
        if (skipInvalidate()) {
            return;
        }

        // Reset content capture caches
        mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;
        mCachedContentCaptureSession = null;

        //根据View的标记位来判断该子View是否需要重绘,假如View没有任何变化,那么就不需要重绘
        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;
            }

            //设置PFLAG_DIRTY标记位(增加脏区标记位)
            mPrivateFlags |= PFLAG_DIRTY;

            // 如果缓存失效,则清空对应标记
            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            // Propagate the damage rectangle to the parent 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);
            }

            //代码省略……
        }
    }

2.2.3 我们来看 ViewGroup 中的 invalidateChild() 方法

@Deprecated(请注意这个废弃的标识,这个方法以后可能要废弃)
    @Override
    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        //如果硬件加速开启(默认硬件加速是开启的)
        //现在能理解为什么这个方法标记未废弃方法了吧,因为进行了硬件加速的优化,具体细节以后在讲。
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            // HW accelerated fast path
            //进一步处理,用的是新的代替废弃方法的执行方法。
            onDescendantInvalidated(child, child);
            return;
        }

        //代码省略……

        //如果硬件加速关闭,才会之前版本的递归遍历过程(此过程不讲了,网上大部分的博客还停留在这个版本阶段)
        parent = parent.invalidateChildInParent(location, dirty);

       //代码省略……

    }

2.2.4 我们来看 ViewGroup 中的 onDescendantInvalidated() 方法

//经过针对硬件加速优化后的代码,就显得清新脱俗了
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
        /*
         * HW-only, Rect-ignoring damage codepath
         *
         * We don't deal with rectangles here, since RenderThread native code computes damage for
         * everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area)
         */

        // if set, combine the animation flag into the parent
        mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);

        if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
            // We lazily use PFLAG_DIRTY, since computing opaque isn't worth the potential
            // optimization in provides in a DisplayList world.
            mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;

            // simplified invalidateChildInParent behavior: clear cache validity to be safe...
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        // ... and mark inval if in software layer that needs to repaint (hw handled in native)
        if (mLayerType == LAYER_TYPE_SOFTWARE) {
            // Layered parents should be invalidated. Escalate to a full invalidate (and note that
            // we do this after consuming any relevant flags from the originating descendant)
            mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
            target = this;
        }

        //递归调用父类的onDescendantInvalidated,最终会调用到ViewRootImpl中的这个方法。
        if (mParent != null) {
            mParent.onDescendantInvalidated(this, target);
        }
    }

2.2.5 我们来看 ViewRootImpl 中的 onDescendantInvalidated() 方法

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();
    }

2.2.6 我们来看 ViewRootImpl 中的 invalidate() 方法

void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //绘制请求,注册Vsync信号,等待上报绘制刷新。
            scheduleTraversals();
        }
    }

2.2.7 invalidate 小结

 当子 View 调用了 invalidate 方法后,会为该 View 添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到 ViewRootImpl 中,最终触发 performTraversals 方法,进行开始 View 树重绘流程(只绘制需要重绘的视图)。

2.2.8 postInvalidate 简介

 postInvalidate 和 invalidate 的作用是一样的,唯一的区别是,postInvalidate 可以在子线程中调用请求刷新 UI。为什么呢?因为请求重新布局和绘制,最终都会在 ViewRootImpl 中处理,而 ViewRootImpl 会在请求方法中,进行线程检查(是否是 UI 线程)。当子 view 中有请求绘制的需求怎么办,那么就用 postInvalidate。其实 postInvalidate 的功能就是 线程切换 + invalidate 调用。

3 问题思考

requestLayout 和 invaldate 有什么区别?

  • requestLayout 和 invalidate 都会触发整个绘制流程。但是在 measure 和 layout 过程中,只会对 flag 设置为 FORCE_LAYOUT 的情况进行重新测量和布局,而 draw 只会重绘 flag 为 dirty 的区域。
  • requestLayout 是用来设置 FORCE_LAYOUT 标志,invalidate 用来设置 dirty 标志。所以 requestLayout 只会触发 measure 和 layout,invalidate 只会触发 draw。
  • 所以一般都是组合使用。比如:只要刷新的时候就调用 invalidate,需要重新 measure 就调用 requestLayout,后面再跟个 invalidate(为了保证重绘)

小编的扩展链接

《Android 视图模块 全家桶》