Android View事件分发、绘制流程

2,000 阅读12分钟

前言

复习、复习、复习

学习View事件分发,就像外地人上了黑车!

主要解决的问题

View的事件分发

滑动冲突

多次测量

绘制流程

环境:API 29

目录

一、窗口事件传递

事件从哪里来?又是怎么传递的?

首先,你触摸屏幕时,顶层PhoneWindow会捕获到该事件(事件从驱动传递到IMSIMSWMS通信,WMS通过ViewRootImpl传递给了目标窗口),然后调用ActivitydispatchTouchEvent,然后会调用DecorViewdispatchTouchEvent (DecorView继承FrameLayoutFrameLayout继承ViewGroup),最终事件到ViewGroupdispatchTouchEvent方法,如果返回false,都没有将事件进行消耗,那么就会调用ActivityonTouchEvent方法。

事件传到ViewGroup其实也符合 自下而上 的流程。一层层传递到View,然后处理事件返回到Activity

二、View事件传递

View事件传递,主要看ViewGroupdispatchTouchEvent方法,对这个方法抽象出来的伪代码如下

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

如果子ViewViewGroup,那么会继续调这个方法进行分发,然后层层往上返回。

这是一种自下而上的流程,使用了递归思想、责任链模式。

为什么如此设计?

  • View的排版规则,嵌套越深,显示层级越高,层级越高就越容易倍用户看见

  • 所见即所得,用户看到了什么,触摸到的也应该是什么,符合用户直觉

三、View事件分发

我们先大概了解一下事件分发(简易版)的流程

通过之前View事件的传递,我们可以了解到分发内部的核心点:

  • 当前View是否需要拦截touch事件

  • 是否需要将touch事件继续分发给子View

  • 如何将touch事件分发给子View

我们按照核心点分析,代码都是处于 dispatchTouchEvent

1、拦截事件

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
     || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
     if (!disallowIntercept) {
          intercepted = onInterceptTouchEvent(ev);
          ev.setAction(action); // restore action in case it was changed
     } else {
          intercepted = false;
     }
} else {
     intercepted = true;
}

当按下事件为ACTION_DOWN时,mFirstTouchTarget = null (当有子View捕获到了事件不为空) ,并且 mGroupFlags = FLAG_DISALLOW_INTERCEPT , 然后会先调用 自身的 onInterceptTouchEvent 方法来确定是否需要拦截。

2、分发事件

如果没有拦截事件,即 intercepted = false

if (!canceled && !intercepted) {
    ....
//注释1
if (actionMasked == MotionEvent.ACTION_DOWN
     || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
          || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
     if (newTouchTarget == null && childrenCount != 0) {
        .....
        //注释 2
          final View[] children = mChildren;
          for (int i = childrenCount - 1; i >= 0; i--) {
          final int childIndex = getAndVerifyPreorderedIndex(
                     childrenCount, i, customOrder);
          final View child = getAndVerifyPreorderedView(
                      preorderedList, children, childIndex);
            .....
            //注释  3
           if (!child.canReceivePointerEvents()
                   || !isTransformedTouchPointInView(x, y, child, null)) {
                  ev.setTargetAccessibilityFocus(false);
                  continue;
            }
        ....   
        //注释 4
          if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                 newTouchTarget = addTouchTarget(child, idBitsToAssign);
                 alreadyDispatchedToNewTouchTarget = true;
                 break;
           }
         .....
         //注释 5 
         if (mFirstTouchTarget == null) {
                //注释 5.1
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                    //注释  5.2
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                .......
    }
}
​

首次touch屏幕,触发的ACTION_DOWN事件,所以注释1处为true

注释 2 处是遍历所有的子View

注释 3 处是判断事件是否在子View坐标范围内,并且子View没有处于动画状态

注释 4 处是将事件分发给子View,如果子View捕获事件成功,会利用 mFirstTouchTarget 存储所有需要分发的子View,结构为链表结构,场景之一就是多点触控。

注释 5 处是如果mFirstTouchTargetnull,说明在处理ACTION_DOWN事件时拦截了子View或者说是你这个触摸没有触摸到任何一个子View,然后会触发到 注释 5.1,注释 5.1 内实际上会调用自身的 onTouchEvent 来处理事件。

注释 5.2 处会判断,如果此时拦截了子View分发,dispatchTransformedTouchEvent中会给子View传递ACTION_CANCEL事件

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
​
        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        .....
  }

那么注释 5.2 处的 intercepted不是为false么?毕竟刚开始ACTION_DOWN事件时并没有拦截。那有没有这种可能,在处理其它事件时 intercepted 改成了 true

if (actionMasked == MotionEvent.ACTION_DOWN
           || mFirstTouchTarget != null) {
           //注释 1
      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                  intercepted = onInterceptTouchEvent(ev);
                  ev.setAction(action); // restore action in case it was changed
             } else {
                  intercepted = false;
             }
} else {
      intercepted = true;
}

我们可以看到第一次没有拦截的情况下,会走到上述代码 注释 1 处,然后只要 disallowInterceptfasle,onInterceptToucheEventtrue,那么 intercepted 就会为 true。即场景是当父视图 onInterceptTouchEvent 先返回false,子View dispatchTouchEvent 返回 true,此时父视图 onInterceptTouchEvent 返回true导致 intercepted重置为 true ,子View就会收到ACTION_CANCELtouch事件。

流程大概就是先检查当前ViewGroup是否需要拦截事件,然后再将事件分发给子View,然后再根据mFirstTouchTarget再将事件分发给子View,已经分发的就不会再次分发。

如果 mFirstTouchTarget 不为 null,即子Viewtouch事件进行了捕获,则直接将当前及以后的事件(ACTION_MOVEACTION_UP等)交给mFirstTouchTarget 存放的View进行处理。

四、View滑动冲突

滑动冲突场景简单来说可以分为三个

  • 外部滑动方向和内部滑动方向不一致 ViewPager + Fragment

  • 外部滑动方向和内部滑动方向一致 ViewPager + 横向RecyclerView

  • 上面两种情况的嵌套 ViewPager + Fragment + RecyclerView

解决方式模板

  • 外部拦截法

    //重写父容器HorizontalScrollViewEx的拦截方法
    public boolean onInterceptTouchEvent (MotionEvent event){
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN://对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
             intercepted = false;
             break;
          case MotionEvent.ACTION_MOVE://对于ACTION_MOVE事件根据需要决定是否拦截
             if (父容器需要当前事件) {
                 intercepted = true;
             } else {
                 intercepted = flase;
             }
             break;
       }
          case MotionEvent.ACTION_UP://onClick事件在UP事件中调用,一旦拦截子View的onClick事件将不会触发
             intercepted = false;
             break;
          default : break;
       }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
       }
    
  • 内部拦截法

    //重写父类HorizontalScrollViewEx的onInterceptTouchEvent方法
    public boolean onInterceptTouchEvent (MotionEvent event) {
     int action = event.getAction();
     if(action == MotionEvent.ACTION_DOWN) {
         return false;
     } else {
         return true;
     }
    }
    //重写子类ListView的dispatchTouchEvent方法
    public boolean dispatchTouchEvent ( MotionEvent event ) {
      int x = (int) event.getX();
      int y = (int) event.getY();
      switch (event.getAction) {
          case MotionEvent.ACTION_DOWN:
             parent.requestDisallowInterceptTouchEvent(true);//为true表示禁止父容器拦截
             break;
          case MotionEvent.ACTION_MOVE:
             int deltaX = x - mLastX;
             int deltaY = y - mLastY;
             if (父容器需要此类点击事件) {
                 parent.requestDisallowInterceptTouchEvent(false);
             }
             break;
          case MotionEvent.ACTION_UP:
             break;
          default :
             break;        
     }
      mLastX = x;
      mLastY = y;
      return super.dispatchTouchEvent(event);
    }
    ​
    

对于内部拦截法中子View所调用的 parent.requestDisallowInterceptTouchEvent(true)

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
​
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }
​
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
     }

我们可以看到如果传递 true 并且 mGroupFlags & FLAG_DISALLOW_INTERCEPT = 0,然后mGroupFlags 值就变成了 FLAG_DISALLOW_INTERCEPT的值了。

我们再看第三节事件分发拦截那块的代码

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
     if (!disallowIntercept) {
          intercepted = onInterceptTouchEvent(ev);
          ev.setAction(action); // restore action in case it was changed
     } else {
          intercepted = false;
     }

可以看到这里 disallowIntercept 会被赋值为 true ,然后 intercepted就为 false了,以此达到禁止父视图拦截的目的。

总结

递归调用child.dispatchTouchEvent 然后再依次 return ,true代表消耗了事件,整个流程就是递归、回溯过程。

每次执行dispatchTouchEvent时都可以判断是否需要拦截当前事件或者传递给下一级

最后一个childdispatchTouchEvent内 判断是否view enabled 并实现了 onTouchListener,如果设置的listener内部return true,则消耗事件,不执行onTouchEvent 。否则执行onTouchEvent,内部会根据action来执行onClick或者是onLongClick

需要注意的是,在dispatchTouchEvent 内,当ACTION_DOWN被拦截后,往后的ACTION_MOVEACTION_UP都不会拦截,对应于onInterceptTouchEvent 只调用一次,如果拦截 ,mFirstTouchTarget标记为null,后续根据这个标记,都会被拦截。捕获到触摸的View都会放到mFirstTouchTarget

在父视图的onInterceptTouchEvent 返回false,如果在MOVE的过程中 onInterceptToucheEvent 返回了 true, 此时会resetTouchState 会传递给子控件ACTION_CANCEL事件

处理滑动冲突的话有内部拦截和外部拦截

外部拦截法主要是重写父ViewonInterceptTouche 方法,这里需要注意不能屏蔽ACTION_DOWN事件。

内部拦截发主要是重写父viewonInterceptTouchEvent,然后子View利用 requestDisallowInterceptTouchEvent 方法来改变 mGroupFlags 的值,如果包含 FLAG_DISALLOW_INTERCEPT则表示不允许拦截,进而调用父viewonInterceptTouchEvent方法

五、View的绘制流程

Activity的启动过程,我们从ContextstartActivity说明,其实现是ContextImplstartActivity,然后内部会通过Instrumentation来尝试启动Activity,这是一个跨进程过程,它会调用AMS的startActivity方法,当AMS校验Activity的合法性后,会通过ApplicationThread回调到我们的进程,这也是一次跨进程通信,而ApplicationThread就是一个Binder。回调逻辑是在Binder线程池中完成的,所以需要通过Handler H将其切回UI线程,第一个消息是LAUNCH_ACTIVITY,它对应这handleLaunchActivity。这个方法里面完成了Activity的创建和启动。接着,在ActivityonResume中,Activity的内容将开始渲染到Window上面,然后开始绘制直到我们可以看见。(四大组件启动源码流程可以自行搜索、笔者以前写的排版不好,已经删除、后续不再分析)

首先我们需要明白,Window是在handleLaunchActivity方法中,在创建Activity后调用attach方法时创建的PhoneWindow

activity中的onCreate方法调用setContentView就是将PhoneWindwoDecorView关联起来,然后handleResumeActivity阶段会通过远程调用,将View添加到Window中,这个阶段同时会创建ViewRootImpl,并且调用requestLayout方法。

对于View的刷新机制和Surface相关知识 可以参考 屏幕刷新机制小结 Android屏幕刷新机制 VSync、Choreographer

requestLayout内部其实也是调用的performTraversals 方法,内部调用流程如下,至于 performTraversals 如何调用的,可以看刚才给的链接。

performMeasure 进行测量onMeasure工作

performLayout 进行onLayout操作

performDraw 进行 onDraw 操作

六、View的测量过程

MeasureSpec 高 2 位代表SpecMode(测量模式)、低30位代表SpecSize(测量模式下的规格大小)。其中SpecMode共有三类,如下

  • UNSPECIFIED 父容器不对View限制,要多大有多大

  • EXACTLY 精确大小,为SpecSize值,一般对应于match_parent和具体数据两种模式

  • AT_MOST 父容器指定了一个可用大小即SpecSizeView的大小不能大于这个值

其中关键代码

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

我们通过Window尺寸和getRootMeasureSpec确定DecorViewMeasureSpec

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

然后通过performMeasure 方法调用 DecorViewonMeasure 方法 (DecorView是继承FrameLayout的,onMeasure也在FrameLayout中),onMeasure 关键代码如下

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

我们分析一下 measureChildWithMargins 方法

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
​
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
​
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这里的意思主要是 根根据父视图的 MeasureSpec以及View本身的LayoutParams来确定子元素的MeasureSpec,关键方法是 getChildMeasureSpec

 //padding参数为父视图已经占用的空间
 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
​
        int size = Math.max(0, specSize - padding);
​
        int resultSize = 0;
        int resultMode = 0;
​
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
​
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
​
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

主要作用就是根据父视图的MeasureSpec以及View本身的LayoutParams来确定子元素的MeasureSpec

大概规则如下

总结一下,就是当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,ViewMeasureSpec都是精确模式,大小都是LayoutParams中的大小;当View的宽高是match_parent时,如果父容器是精准模式,那么View也是精准模式并且大小是父容器的剩余空间,如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽高是wrap_content时,不管父容器是精准模式还是最大化模式,View总是最大化并且大小不超过父容器的剩余空间。

当你自定义View时,是不是需要重写onMeasure方法,如果不重写,你对View设置wrap_content或者是match_parent都是parentSize,撑满屏幕大小?通过上面介绍,你应该心里有底了。

这块还有个常见的问题,为什么onMeasure会调用两次?

这个可能根据不同控件、不同API有不同,例如api 16-19 可能会执行2次 onMeasure、2次onLayout、1次onDraw,而api 21-23执行 3次 onMeasure、2次onLayout、1次onDraw,api 24 -25 执行2次onMeasure、2次onLayout、1次onDraw

有的网友说是performTravels被调用了两次,其实个人认为确实是这样的。在performTravesls 代码的最后

        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        if (!cancelDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
​
            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();
            }
        }

第一次走到这的时候,cancelDraw 是为 false 的,因为 isViewVisibletrue。因为在Activity启动过程中,是先有requestLayout方法(这里主要是通过发送异步屏障消息),然后立马在调用了DecorViewsetVisibility方法。所以android 10中其实是一次就完事了,没有多次onMeasure

我们看下android 9api

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

此时newSurfacefalse,只有等Surface准备好了之后,onDraw才能拿到Canvas,往Surfacebuffer中写数据。

 if (!hadSurface) {
                    if (mSurface.isValid()) {
                        // If we are creating a new surface, then we need to
                        // completely redraw it.  Also, when we get to the
                        // point of drawing it we will hold off and schedule
                        // a new traversal instead.  This is so we can tell the
                        // window manager about all of the windows being displayed
                        // before actually drawing them, so it can display then
                        // all at once.
                        newSurface = true;
                        ...
}
    public boolean isValid() {
        synchronized (mLock) {
            if (mNativeObject == 0) return false;
            return nativeIsValid(mNativeObject);
        }
    }

**那你还有可能会问?**我问的不是启动时候onMeasure执行次数,我是问的例如FrameLayout中这个方法的执行次数?

首先我们要明白整个流程,我们是先经过测量,然后父布局才能将子View放置到合适的地方,然后再绘制。那测量这个过程,是自上向下的一个过程,子View测量完后将结果汇总返回给父布局,父布局再设置自身的宽高,也就是先算孩子再算自己。可以理解为第一次用预置宽高测量View Tree,因为存在需要宽度比预置的大的子布局,所以对结果不满意,又会重新测量,直到满意为止。

七、View的布局过程

performLayout实际会调用 FrameLayoutlayoutChildren方法就行布局,这里没有太多需要分析的点。

我们通过常说 invalidate 必须在主线程执行, postInvalidate 可以在子线程执行,并且这两个方法不一定会触发Viewmeasurelayout,多数情况下只会执行draw方法,这是为什么呢?

Viewmeasure中,如果要触发onMeasure方法,需要设置ViewPFLAG_FORCE_LAYOUT 标志位

public final void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
       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;
            }
}

通过上述代码,可以发现如果forceLayouttrue,就会执行 onMeasure 方法,在requestLayout中执行了 mPrivateFlags |= PFLAG_FORCE_LAYOUTinvalidate 并没有设置这个标志。

onLayout也是同理

 public void  onLayout(int l, int t, int r, int b)
 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ......
  }

可以看出,当View的位置改成,或者是添加PFLAG_LAYOUT_REQUIRED 标志位后,onLayout才会被执行,当调用invalidate 时,如果view没有改变,或者没有设置这个标识位,也不会调用onLayout

你可能会问 invalidate如何更新view的,其实触发流程最后还是会走到scheduleTraversals -> performTravels的。

postInvalidate为什么能在主线程执行,普遍做法,基本都是内部通过Handler切换到主线程,然后调用invalidate,很多开源库都是这样做的。

八、View的绘制过程

绘制过程主要做了什么?

      draw:
      canvas = mSuface.lockCanvas();
      drawBackground(canvas);
      .......
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            onDraw(canvas);
​
            // Step 4, draw the children
            dispatchDraw(canvas);
​
     surface.unlockCanvasAndPost(canvas);

主要是绘制View的背景、绘制View的自身内容、对draw事件进行分发,递归调用子View的draw事件。

通过lockCanvas获取一块buffer, 接下来java层会通过View绘制,最后通过unlockCanvasAndPost提交buffer。

这里lockCanvas获取的是backCanvas,视图刷新使用了双缓冲机制

每次CPU提交的数据被缓存到Back Buffer中,然后GPUBack Buffer中的数据做栅格化操作,完成之后将其交换(swap)到 Frame Buffer 中,最后屏幕从 Frame Buffer中取数据显示。GPU会负责定期交换Back BufferFrame Buffer中的数据,以保证屏幕上显示最新的内容。当CPU正在向Back Buffer写入数据时,GPU会将Back Buffer锁定,如果此时正好到了交换两个Buffer的时间点,那么这次swap就会被忽略,直接导致的就是Frame Buffer还是保存的之前一帧的数据,即之前的内容,即对应Android的丢帧。

刷新数据

首先应用从系统服务内申请buffer,系统返回buffer,应用拿到buffer后,绘制,然后提交buffer给系统服务,系统服务把这块buffer写到屏幕缓冲区域,屏幕以一定帧率去刷新,每次刷新的时候会去缓冲区域里把图像刷数据读出来,显示出来,如果缓冲区域里没有新的数据,屏幕就一直用老的数据,看起来屏幕就没有变一样。(缓冲不只是只有一个,一个缓冲写,一个缓冲读)。