入职阿里巴巴的正确姿势你需要了解下Android View的更新 requestLayout 与重绘 invalidate

在大家都了解过Android View的测量、布局、绘制机制后,我们来细化地分析一下关于View的重绘invalidate与更新requestLayout

现象

public class CustomEmptyView extends View {
    public CustomEmptyView(Context context) {
        super(context);
    }

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

    public CustomEmptyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("CustomEmptyView", "onMeasure");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i("CustomEmptyView", "onLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i("CustomEmptyView", "onDraw");
    }
}

从View的绘制机制可知,View从测量、布局、绘制的步骤中会对应执行该View#onMeasure()、View#onLayout()、View#onDraw()。那么我们今天讨论的View#invalidate()和View#requestLayout()呢?我们打印一下数据。

View#invalidate()的执行步骤是:

2019-03-26 17:32:34.739 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw

View#requestLayout()的执行步骤是:

2019-03-26 17:33:13.497 8075-8075/com.example.myapplication I/CustomEmptyView: onMeasure
2019-03-26 17:33:13.501 8075-8075/com.example.myapplication I/CustomEmptyView: onLayout
2019-03-26 17:33:13.503 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw

从打印数据来推测就是View#invalidate()只会执行View#onDraw();而View#requestLayout()则会重新走View的绘制流程。接下来我们从源码的角度来分析一下。

下面的源码分析基于android-28

View#requestLayout()

我们分析一下View#requestLayout()。我们定位到对应的源码

    /**
     * Call this when something has changed which has invalidated the
     * layout of this view. This will schedule a layout pass of the view
     * tree. This should not be called while the view hierarchy is currently in a layout
     * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
     * end of the current layout pass (and then layout will run again) or after the current
     * frame is drawn and the next layout occurs.
     *
     * 当某内容发生更改,导致该视图的布局重绘时调用此函数。这将安排视图树的布局传递。
     * 当视图层次结构当前处于布局Layout事件时,不会执行该函数.
     * 如果正在进行布局,可以在当前布局传递结束时(然后布局将再次运行)或在绘制当前帧并执行下一个布局之后执行请求。
     * 

Subclasses which override this method should call the superclass method to * handle possible request-during-layout errors correctly.

*/
@CallSuper public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); 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()方法时判断,这个是以前的文章看到的。 // 但是在当前源码的View.class和ViewRootImpl.class,全局搜索PFLAG_FORCE_LAYOUT,并没有直接的判断,导致View#requestLayout()不执行测量和布局方法 mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; // isLayoutRequested()对应是mLayoutRequested字段,该字段在默认为false if (mParent != null && !mParent.isLayoutRequested()) { // 执行父容器的requestLayout()方法 mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } }

当我们点击ViewGroup#requestLayout(),发现它是一个空实现,我们可知ViewParent是interface类,我们通过之前的View的分析,可以去ViewRootImpl类看看ViewGroup#requestLayout()的实现方法。

    @Override
    public void requestLayout() {
        // mHandlingLayoutInLayoutRequest这个参数,通过全局变量定位,在performLayout()开始时为true,结束时为false,与之前说的,不在布局期间执行相对应
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查是否UI线程
            checkThread();
            // 这里将mLayoutRequested设为true
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }    

看到scheduleTraversals(),我相信大家都觉得快触摸到真相,但是发现点击该方法里的实现,并不能找到我们想要的,此时我们想一下之前的打印数据,View#requestLayout()会重新执行View的绘制步骤,View的绘制步骤最核心是ViewRootImpl#performTraverals,按照这个思路我们继续寻找。

从上面的源码中,我们看到mChoreographer这个对象,我们曾经在分析滑动流程度的时候,提及过Choreographer编舞者这个对象,我们最后从mChoreographer这个对象中的mTraversalRunnable参数找到线索。

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }    

最终我们发现mTraversalRunnable这个是一个Runnable对象,在scheduleTraversals()中传入mTraversalRunnable,就会执行doTraversal(),在doTraversal()中我们也如愿地找到我们想要的核心方法ViewRootImpl#performTraverals().当调用ViewRootImpl#performTraverals就重新开始该控件的测量、布局、绘制步骤。符合了我们一开始的打印数据。

View#invalidate()

接着我们看一下View#invalidate的源码。

    /**
     * Invalidate the whole view. If the view is visible, 
     * 重绘整个View,如果View是可视的。
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * 

* This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. * 这个方法必须被使用在UI线程,用在非UI线程的方法为postInvalidate() */ public void invalidate() { invalidate(true); } /** * This is where the invalidate() work actually happens. A full invalidate() * causes the drawing cache to be invalidated, but this function can be * called with invalidateCache set to false to skip that invalidation step * for cases that do not need it (for example, a component that remains at * the same dimensions with the same content). * 这就是invalidate()方法工作发生的地方,一个完整的invalidate()方法会引起绘制 * 缓存失效,但是这个函数能设置参数invalidateCache为false来跳过重绘步骤,由于 * 该方法不被需要,例如一个组件保持相同的尺寸和相同的内容 * * @param invalidateCache Whether the drawing cache for this view should be * invalidated as well. This is usually true for a full * invalidate, but may be set to false if the View's contents or * dimensions have not changed. * 这绘制缓存是否应被重绘.一个完整的重绘通常为true,但是可能设置为false,如果View的内容和尺寸没有被改变。 * * @hide */ public void invalidate(boolean 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 (mGhostView != null) { mGhostView.invalidate(true); return; } // 跳过重绘。从方法描述可知,该方法判断该View不被重绘,当它处于不可见和没有处于动画中 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)) { if (fullInvalidate) { mLastIsOpaque = isOpaque(); mPrivateFlags &= ~PFLAG_DRAWN; } mPrivateFlags |= PFLAG_DIRTY; // 如果需要全部重绘,invalidate()未传参调用时默认为true if (invalidateCache) { // 记住这个PFLAG_INVALIDATED标志位 mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } // Propagate the damage rectangle to the parent view. // 从下面参数mParent可知,应该是将需要重绘这个事件通知给父容器 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); // 这个就是重点,我们去看一下父容器的invalidateChild()方法 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(); } } } }

从上面的分析可知,经过invalidate()的重载方法,最终会调用invalidateInternal(),在这个方法里头,要判断是否需要重绘,如果需要重绘,就对该View进行标识,然后将该View的Rect信息传递给父容器的invalidateChild().

与之前的View#requestLayout()相似,最终同样是执行ViewRootImpl#invalidateChild(),然后我继续分析ViewRootImpl#invalidateChild()的实现。


    @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    // 由于没有注释,我们从方法名去分析源码,重载至这个最终的方法,意思为在父容器中重绘子控件
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        // 检查线程,里面判断为,该方法需要执行在UI线程,验证了之前View#invalidate()的描述
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
        // 从上一步可知,dirty是不为空的
        if (dirty == null) {
            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);
            }
        }
        // 从方法的命名,这里应该是我们需要的方法,重绘
        invalidateRectOnScreen(dirty);

        return null;
    }

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

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }    

看到这里,我们马上联想起之前分析的View#requestLayout(),同样是scheduleTraversals(),但是从打印数据可知,View#invalidate()是不会执行测量与布局的,但是目前来看它们最终调用的方法可是一致的。

我们可以留意一下之前的ViewRootImpl#requestLayout()方法中,主动将一个全局变量mLayoutRequested设置为true;那么大胆猜测这个对象肯定会影响到performMeasure()与performLayout(),我们翻一下ViewRootImpl#performTraversals()

    private void performTraversals() {

    ···
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); 
    ···
        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));
    ···
        if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
                ···
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ···
            }
        ···
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
        }
        ···
    }

对于复杂的ViewRootImpl#performTraversals(),我们抽取一些关键的代码,的确可以验证mLayoutRequested对象是会影响测量和布局对应的方法的,因此可以验证我们一开始的打印数据,View#invalidate()是不会执行测量和布局的。

View#postInvalidate()

从View#invalidate()的注释描述可知,View#invalidate()需要执行在UI线程中,如果在非UI线程需要使用View#postInvalidate(),我们简单地分析一下源码。

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

    public void postInvalidate(int left, int top, int right, int bottom) {
        postInvalidateDelayed(0, left, top, right, bottom);
    }

    public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
            int right, int bottom) {

        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
            info.target = this;
            info.left = left;
            info.top = top;
            info.right = right;
            info.bottom = bottom;

            attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
        }
    }    

ViewRootImpl#dispatchInvalidateRectDelayed()

    public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info, long delayMilliseconds) {
        final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_INVALIDATE_RECT:
                final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
                break;
                ···
        }  
    }    

从源码可知,View#postInvalidate()会发送一个MSG_INVALIDATE_RECT的Handler消息,在接收消息后,同样是执行View#invalidate()方法。

##疑问:requestLayout不执行performDraw()?

在翻阅资料的时候,很多地方都会描述到View#requestLayout不执行performDraw(),但是自己的打印结果是会显示执行performDraw()的。我们带着问题翻一下源码。直接定位到ViewRootImpl#performTraversals的performDraw()

    private void performTraversals() {
        ···
        // dispatchOnPreDraw()返回注释是:如果当前绘制应该被取消和重新调度,则为True,否则为false。
        // final boolean isViewVisible = viewVisibility == View.VISIBLE; 只要是显示的View,cancelDraw为true
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

        // cancelDraw通过dispatchOnPreDraw()的注释解析和isViewVisible,得出cancelDraw应该为false
        // newSurface默认为false,在测量判断逻辑中,在判断是否新的Surface会设置为true,这里应该是false
        // 因为会执行performDraw()
        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        }

    }

ViewRootImpl#performDraw()

    private void performDraw() {
        // Display.STATE_OFF表示显示状态:显示关闭。
        // mReportNextDraw对象默认false,可在ViewRootImpl#reportNextDraw()中设置为true,但是第一个判断已经为false,不影响判断
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        } else if (mView == null) {// 必然不为null
            return;
        }

        final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

        boolean usingAsyncReport = false;
        if (mReportNextDraw && mAttachInfo.mThreadedRenderer != null
                && mAttachInfo.mThreadedRenderer.isEnabled()) {
            usingAsyncReport = true;
            mAttachInfo.mThreadedRenderer.setFrameCompleteCallback((long frameNr) -> {
                // TODO: Use the frame number
                pendingDrawFinished();
            });
        }

        try {
            // 最重点是这句,draw()方法的执行,能影响这段代码的执行,只有上面的两个return逻辑。
            boolean canUseAsync = draw(fullRedrawNeeded);
            if (usingAsyncReport && !canUseAsync) {
                mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
                usingAsyncReport = false;
            }
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        ···
    }    

从上面的判断可以推断到draw()是会被执行,与网上得出结论不一致,暂时没得出什么原因,如有分析错误和不足的地方,希望指定一下。

总结

步骤图

View 绘制步骤是 performMeasure()、performLayout()、performDraw(),我们经过对 invalidate 和 requestLayout的源码分析,可以得出。

invalidate()方法只会执行performDraw()方法;而requestLayout()方法会执行performMeasure()、performLayout()、performDraw()。在对应的应用场景,如果只是View的显示内容发生变化且不改变View的大小和位置,则使用invalidate(),如果大小、位置、内容都发生改变则调用requestLayout()。

更多资料分享欢迎Android工程师朋友们加入安卓开发技术进阶互助:856328774免费提供安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于Android面试的题目汇总。

你可能感兴趣的:(入职阿里巴巴的正确姿势你需要了解下Android View的更新 requestLayout 与重绘 invalidate)