美文网首页Android自定义Viewoh-my-androidAndroid
Android 源码分析 - View的requestLayou

Android 源码分析 - View的requestLayou

作者: 琼珶和予 | 来源:发表于2018-11-03 01:00 被阅读14次

      前面写了一篇文章专门的分析了View的measure、layout和draw三大流程,在那篇文章中,多次提到了requestLayoutinvalidatepostInvalidate方法,因为这三个方法会影响三大流程。今天我们来看看这三个方法到底做了什么,平时只管在用,却不知道它们得实现,难免感觉有些遗憾。本文也是为RecyclerView源码分析打基础的第二篇文章。
      本文参考资料:

    1. Android View 深度分析requestLayout、invalidate与postInvalidate
    2. Android 源码分析 - View的measure、layout、draw三大流程
    3. Choreographer 解析

      注意,本文源码来自Api 27

    1. 概述

      我们在分析源码之前,首先从大概上的了解这三个方法到底是干嘛的,它们之间的区别又是什么。我们对此先有一个大概的了解。
      首先来看看这三个方法的整个执行流程。


      从图中,我们可以得到,requestLayout方法会重新走一遍三大流程。不过这里会涉及到一些问题,比如,我们在三大流程调用requestLayout方法会不会导致死递归?不过我们在实际开发过程中确实没有导致死递归,那Google爸爸是怎么为我们解决这个问题的呢?
      同时,invalidate方法和postInvalidate方法又有什么区别?我们知道,invalidate方法是在UI线程调用的,postInvalidate方法是用于非UI线程调用的。在非UI线程里面更新UI肯定使用到Handler,但是我们知道它是怎么通过Handler来实现的,实际上也是非常的简单😂。
      以上的几个问题都是本文重点关注的点。

    1. requestLayout

      首先我们来看第一个方法--requestLayout方法。在正式分析requestLayout方法之前,我们先对Android的View树型结构。


      从上图中,我们可以得到两点。
    1. 整个View树结构是一个双向指针结构。其实这个不难理解,因为View的三大流程的分发、事件分发机制都是基于责任链模式,而责任链模式分为先上传递和向下传递,所以拥有childparent两个指针并不奇怪。
    2. 我们知道获取一个ViewParent得到的是一个ViewParent对象,可能大家都有一个疑问为什么不是ViewGroup呢?因为View只会在一个ViewGroup啊。从这里,我们可以得到答案,DecorViewParent并不是View,更不是ViewGroup,所以一个ViewParent不一定是ViewGroup

     对整个View结构有了一个整个的认识之后,现在我们来看一下requestLayout方法,看看这个方法为我们做了那些事情。

        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;
            }
    
            mPrivateFlags |= PFLAG_FORCE_LAYOUT;
            mPrivateFlags |= PFLAG_INVALIDATED;
    
            if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
                mAttachInfo.mViewRequestingLayout = null;
            }
        }
    

      整个requestLayout方法比较简单,我将分为3步。

    1. 如果此时,View结构还处于laoyut阶段之前,直接调用ViewRootImplrequestLayoutDuringLayout方法,将当前View对象传递进行,至于这里做什么,待会我们回去看ViewRootImplperformLayout方法就知道了。从这里,我们就可以得到为什么requestLayout方法不会导致死递归了。
    2. 更新了mPrivateFlags变量。注意,mPrivateFlagsPFLAG_FORCE_LAYOUTPFLAG_INVALIDATED这两个变量。了解三大流程的同学,相信对PFLAG_FORCE_LAYOUT变量不陌生🤓。
    3. 调用ParentrequestLayout,让整个责任链模式动起来。

      关于requestLayout方法,我打算从两点开始讲解。

    1. 从责任链的模式来看看,看看整个责任链是怎么传递,最后又是怎么走三大流程。
    2. 从三大流程角度上来看看requestLayout方法的调用流程。

    (1).requestLayout的责任链

      requestLayout的责任链就是从调用ParentrequestLayout方法开始的,一层一层递归上去,最终会调用到ViewRootImplrequestLayout方法里面。

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

      ViewRootImplrequestLayout方法比较简单。首先是check了一下线程,如果当前线程不是主线程的话,那么就会抛出异常。
      其次,就是调用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();
            }
        }
    

      如果来详细的看scheduleTraversals方法的话,肯定会非常的懵逼。这里我简单解释一下,这个方法做了什么吧。实际上,就是向Choreographer的消息队列post一个CallBack对象,用于下一次绘制。至于Choreographer是什么,本文不做过多的解释,可以参考这篇文章:Choreographer 解析。这里,我们只需要知道,往Choreographer的消息队列post一个CallBack对象,表示下次绘制信号来的时候,会执行我们的任务。那要执行什么呢?当然是mTraversalRunnable任务。
      同时,这里还需要注意一点就是,我们通过调用如下代码先HandlerMessageQueue里面插入了一个同步屏障,来表示当前的任务不可被打断:

                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    

      至于什么是同步屏障,相信熟悉JVM的同学并不陌生。在JVM里面,当一个变量被volatile关键字修饰时,JVM会在这个变量初始化之前插入一个屏障,表示当前操作不可被打断。这里的打断有两个意思:1.不允许JVM进行命令重排序;2.保证当前的操作是原子性(感觉这两个意思是同一个意思😂)。这里Handler的同步屏障表达的意思也差不多
      哎呀,一不小心就跑了题,其实是我故意的🙄🙄。我们继续来看看代码吧。
      表示当前任务不可被打断中的任务是什么任务呢?当然是我们的mTraversalRunnable任务呢。我们来看看mTraversalRunnable任务究竟做了啥。

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

      感觉TraversalRunnable非常的简单,只是调用了doTraversal方法,我们来看看简单:

        void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

      看到doTraversal方法时,你们开不开心?激不激动?哈哈,在doTraversal方法里面,调用了performTraversals方法。还记得performTraversals方法是干嘛的吗?哈哈,它就是三大流程的开始。

    (2).从requestLayout角度上来三大流程

      在这之前,我们已经详细的分析了View的三大流程。在本篇文章中,我们再来简单看一下,这次,我们的重点放在Viewmeasure方法。

            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
    
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }
    

      这里将mPrivateFlagsPFLAG_FORCE_LAYOUT做了一个与运算,用来判断当前是否进行强制布局。从这里看出来可以知道是否进行强制布局呢?我们看最后一行代码:

                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    

      这里,mPrivateFlagsPFLAG_LAYOUT_REQUIRED做了一个异或操作。这一步有什么意义呢?我们可以从Viewlayout找到答案:

            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                // ······
            }
    

      如果mPrivateFlagsPFLAG_LAYOUT_REQUIRED的标记,那么肯定会调用onLayout方法,这就是所谓的强制布局。
      强制布局之后,肯定会强制绘制,也就是会调用drawonDraw方法来进行绘制。这些都比较简单,这里就不过多的分析了。

    (3). 为什么在ViewGroup的onLayout方法调用requestLayout方法不会导致死递归?

      按照现在的思路来说,如果在我们onLayout里面调用了requestLayout方法会重新走三大流程,从而导致又会回调到onLayou方法里面来,进而导致死递归。但是实际的效果并不是我们猜想的效果,我们来看看究竟是为什么。
      首先,我先解释一个认知上的误区,在View树layout阶段中,调用requestLayout方法申请重新三大流程,这个是真的会导致死递归。而Google爸爸是怎么解决的呢?我们来看看requestLayout方法就知道真相了:

        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;
            }
            // ······
        }
    

      如果当前View树处于layout阶段,也就是说DecorViewlayout方法还没有执行完毕,此时如果调用requestLayout方法的话,不会立即进行三大流程,而是将这个任务放在ViewRootImpl的一个任务队列里面,那这个任务队列在什么会执行呢?这个就得看ViewRootImplperformLayout方法了。

        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            mLayoutRequested = false;
            try {
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
                mInLayout = false;
                int numViewsRequestingLayout = mLayoutRequesters.size();
                if (numViewsRequestingLayout > 0) {
                    ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                            false);
                    if (validLayoutRequesters != null) {
                        mHandlingLayoutInLayoutRequest = true;
                        int numValidRequests = validLayoutRequesters.size();
                        for (int i = 0; i < numValidRequests; ++i) {
                            final View view = validLayoutRequesters.get(i);
                            view.requestLayout();
                        }
                        measureHierarchy(host, lp, mView.getContext().getResources(),
                                desiredWindowWidth, desiredWindowHeight);
                        mInLayout = true;
                        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
                        mHandlingLayoutInLayoutRequest = false;
                    }
    
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            mInLayout = false;
        }
    

      看到没,再执行了DecorViewlayout方法之后,就开始执行任务队列的任务了。但是这里又有一个问题了,我们发现在执行任务队列中的任务时,又调用了DecorViewlayout方法,这会导致又会去执行我们的onLayout方法,从而又会调用requestLayout方法,进而导致任务队列的任务无穷无尽,这还是没有将上面的问题解释明白啊?
      纳尼?又会调用我们的onLayout方法 ? 既然Viewlefttoprightbottom都没有变,为什么还有回去调用onLayout方法进行重新布局呢?是不是还嫌咱们Android卡的不够吗?😂😂
      当然,说归说,我们还是必须找到依据来证明我们的猜想。答案在哪里呢?当然在layout方法里面:

        public void layout(int l, int t, int r, int b) {
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                // ······
            }
        }
    

      看到changed变量没?这个变量就是用来判断咱们四个属性是否变了。如果变了才会调用onLayout方法进行重新布局,反之则不会调用。
      requestLayout方法到此就分析完毕了。接下来,我们继续分析invalidatepostInvalidate方法。

    2. invalidate

      invalidate方法分为全局刷新和局部刷新。顾名思义,全局刷新表示整个View树都会刷新一遍,局部刷新只会以当前View为根开始刷新。我们来看看invalidate方法:

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

      其中invalidate方法里面传入一个true,表示当前只做局部刷新。invalidate方法最终调用到invalidateInternal方法里面来。
      invalidateInternal方法显示判断当前是否跳过刷新操作,主要是通过skipInvalidate方法来判断的。我们来看看skipInvalidate方法究竟是怎么进行判断的:

        private boolean skipInvalidate() {
            return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                    (!(mParent instanceof ViewGroup) ||
                            !((ViewGroup) mParent).isViewTransitioning(this));
        }
    

      如果当前View不可见,并且自己的动画为空,同时父View也没在动画,那么就跳过刷新。这个判断逻辑我们非常容易的得到。
      同时!(mParent instanceof ViewGroup)又是什么鬼东西,前面我已经介绍了,一个ViewParent不一定是ViewGroup,还有可能是ViewRootImpl。如果一个ViewParent是什么一个情况呢?表示当前ViewDecorView,而DecorView不可见是什么一个情况呢?那就相当于整个View树都不可见,是不是很刺激???整个View树都不可见,那么刷新操作自然是多余的。
      在invalidateInternal方法里面,接下来的操作就是判断mPrivateFlags是否符合刷新操作的要求,具体的细节就不去讨论,反正可能是调用了View其中一个方法导致mPrivateFlags被更新。
      最后就是调用ParentinvalidateChild方法。正常来说一个ViewParent应该是一个ViewGroup,我们来看看ViewGroupinvalidateChild方法:

        public final void invalidateChild(View child, final Rect dirty) {
            final AttachInfo attachInfo = mAttachInfo;
            // ······
            ViewParent parent = this;
            if (attachInfo != null) {
                // ······
                do {
                    View view = null;
                    if (parent instanceof View) {
                        view = (View) parent;
                    }
                    // ······
                    parent = parent.invalidateChildInParent(location, dirty);
                    if (view != null) {
                        // Account for transform on current parent
                        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);
            }
        }
    

      这个invalidateChild方法比较长,这里将它简化了一下。其实简化的那些代码表达意思比较简单,就是Rect矩阵的计算。然后通过递归遍历将整个操作传递上去。其中我们会发现,invalidateChild方法里面通过invalidateChildInParent方法来寻找一个Parent,其实invalidateChildInParent方法也没做什么,就是location的计算和mPrivateFlags的更新,最后会返回当前ViewParent
      经过一层一层的递归,最终也会调用到ViewRootImplinvalidateChildInParent方法里面来。

        @Override
        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;
            }
    
            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;
        }
    

      在invalidateChildInParent方法里面,不管是调用invalidate方法,还是最后调用的invalidateRectOnScreen方法都会调用到scheduleTraversals方法。调用到scheduleTraversals方法表示着什么呢?表示又要开始三大流程了。咦,好像不对哦,前面的图不是说invalidate方法只进行draw流程吗?怎么会进行三大流程呢?是我们的图画错了吗?不是的,实际上在走三大流程是,View会通过mPrivateFlags来判断是否进行measurelayout操作。而在调用invalidate方法时,更新了mPrivateFlags,所以最终只会走draw流程。
      如上,就是整个invalidate方法的执行流程,还是比较简单的。接下来,我们将分析最后一个方法--postInvalidate方法。

    3. postInvalidate

      postInvalidate方法的执行流程跟invalidate方法的差不多,所以本文重点关注它们两个方法的区别。
      从两个方法的名字上,我们就可以看出来,invalidate方法只能在UI线程里面里面调用,而postInvalidate方法的存在是为了解决在非UI线程不能调用invalidate方法刷新的问题。实际上,postInvalidate方法在UI线程和非UI线程都可以调用,因为postInvalidate方法实现刷新的原理是通过Handler来实现的。
      我们来看看具体的源码。通过调用postInvalidate方法,最后会调用到postInvalidateDelayed方法,我们来看看这个方法。

        public void postInvalidateDelayed(long delayMilliseconds) {
            // 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) {
                attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
            }
        }
    

      特么的,这个方法真特么的直接,直接调用了ViewRootImpl的dispatchInvalidateDelayed方法。这个方法像是掌握缩地成寸的法术一样,直接一步跨到了最高层,不像其他的方法,还需要一层一层通过责任链方式传递,我只能给这个方法写一个大写的牛逼
      好吧,好像废话有点多。我们来看看ViewRootImpldispatchInvalidateDelayed方法:

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

      意料之中,果不其然,postInvalidate方法是通过Handler实现在非UI线程里面更新UI的。我们来看看这个Handler,这里我们只看一下这个HandlerhandleMessage方法:

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                // ······
                }
            }
    

      我们在HandlerhandleMessage方法里面,直接调用了Viewinvalidate方法,这就回到上面讲解的invalidate方法的原理。这里就不重复的讲解了。
      哎,以为postInvalidateDelayed方法掌握缩地成寸的法术有多厉害呢,结果修的是残篇😂😂。

    4. 总结

      到此,我们将View的三个方法分析完毕了,理解了这三个方法实现原理,同时也了解这三个方法是怎么跟三大流程配合工作的。这里,我做一个简单的总结。

    1. requestLayout方法会重新走三大流程,所以比较耗性能,所以尽可能的不要频繁的调用requestLayout方法。
    2. layout阶段调用requestLayout方法不会立即执行,而是要等当前的layout阶段执行完毕才会执行。这就解决了requestLayout死递归的问题。
    3. invalidate方法和postInvalidate方法的区别在于, invalidate方法只能在UI线程里面调用,而postInvalidate方法是无所谓线程。
    4. requestLayout方法、invalidate方法和postInvalidate任务的驱动,最后依靠一个叫Choreographer类实现的。我们只是往Choreographer的任务队列post一个Callback对象,就能保证下次屏幕刷新时能执行。

    相关文章

      网友评论

        本文标题:Android 源码分析 - View的requestLayou

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