美文网首页Android知识进阶
Android View绘制流程总结

Android View绘制流程总结

作者: CyanStone | 来源:发表于2019-01-12 18:41 被阅读224次

    最近在学习View流程的绘制,看了几篇不错的博客,自己也跟了下源码,现不打算上篇大论的贴源码了,需要详细的分析过程的,可以参考Android应用层View绘制流程与源码分析,这篇文章写的很详细,现在只做下各个流程的总结,便于不了解的同学迅速了解整个流程,了解整个流程的也可以做迅速的回顾。

    绘制起点

    Android应用层View绘制流程与源码分析这篇文章里,对绘制起点是这么描述的:

    View的绘制,是在我们调用了Activity.setContentView() -> PhoneWindow.setContentView中的mContentParent.addView(mContentParent是FrameLayout)中调用了invalidate()后触发的,invalidate()方法会调用ViewRootImpl.performTraversals(),Activity的整个View树的绘制从这里开始的;

    但其实这个描述是不对的,因为我们都知道,View的测量、布局、绘制流程是在ActivityThread调用handleResumeActivity之后,把decorView加入到window,把window add到windowmanager之后才开始的,具体分析,可以看看Activity/Window/View的关系以及View的绘制时机和我再Activity启动后View何时开始绘制(onCreate中还是onResume之后?) 文章里的分析。

    //ActivityThread
    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { 
         ...
          //执行onResume
          r = performResumeActivity(token, clearHide, reason);
          ....
            if (!a.mWindowAdded) {
                            a.mWindowAdded = true;
                            //触发requestLayout和invalidate方法,开始绘制
                            wm.addView(decor, l);
                        }
    }
    
    • 整个View树的绘图流程是在ViewRootImpl的performTraversals()开始的,整个方法分别调用了mView.measure(),mView.layout(),mView.draw(),完成了测量、布局和绘制流程(mView指的就是DecorView,也就是FrameLayout);
    measure测量
    • measure的过程会递归整个View树,在ViewGroup中会遍历子View调用measureChildren\measureChild\measureChildWithMargins方法对子View进行测量,这三个方法把根据子View的LayoutParams和ViewGroup自身的MesureSpec计算得到要传入子View的MeasureSpec,然后调用childMeasure进行绘制;
    • measure的主要的作用就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight变量赋值
    • measure方法不可以重写,但子View可以覆写onMeasure(),measure方法中回调了onMeasure;
    • MeasureSpec参数,是由父View传递进来的,代表了父View的规格。它是一个32位的整数,有两部分组成,高2位代表模式,低30位代表了父View的具体的size。有三种模式:
      • MeasureSpec.EXACTLY表示确定大小;
      • MeasureSpec.AT_MOST表示最大大小;
      • MeasureSpec.UNSPECIFIED不确定;
    • 当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight成员才会有具体的数值,所以如果我们自定义的View或者使用现成的View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值;
    layout布局
    • layout方法接收四个参数,分别代表相对于Parent的上下左右;
    • layout方法首先会调用setFrame方法把传入的值赋值给mLeft\mRight\mTop\mBottom,然后判断下是否发生了改变,如果改变,会调用onLayout方法;
    • View的onLayout方法可以被重写,但是ViewGroup的onLayout方法是抽象方法,要自定义一个ViewGroup,则必须实现onLayout方法(ViewGroup继承自View,View的onLayout方法参数是上下左右位置,viewGroup的onLayout抽象方法参数多了一个changed);
    • 自定义ViewGroup的步骤:
      • 先调用onMeasure(按需实现,一般想要支持Wrap_Content属性的时候需要测量)进行测量;
      • 调用onLayout动态获取子View和子View测量的大小,按需进行布局;
    • layout过程中,会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中的位置,但不是必须的;
    • 所以,重点来了:

    getWidth()和getHeight()必须是在layout执行之后才有效,且与getMeasuredWidth()和getMeasuredHeight()不同,所以获取View的实际大小,一定要在layout之后调用getWidth()方法和getHeight()方法!

    • 在具体ViewGroup的onLayout方法中,是一定会遍历调用子View的layout方法的,所以也是递归的进行整个View树的布局。比如:LinearLayout的onLayout中会循环遍历子孩子调用setChildFrame(),该方法会调用child.layout方法:
        void layoutVertical(int left, int top, int right, int bottom) {
            for (int i = 0; i < count; i++) {
                    ...
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                }
            }
        }
        
        private void setChildFrame(View child, int left, int top, int width, int height) {
            child.layout(left, top, left + width, top + height);
        }    
    
    draw绘制
    • 在ViewRootImpl中,经过measure和layout之后,直接调用了mView.draw(canvas),这个mView就是DecorView;
    • draw方法的内部,进行了如下几步:
      • 绘制背景backgroud,所以要减少不必要的背景图,避免过度绘制;
      • 调用onDraw()进行绘制,View中的这个方法是空方法,需要各子View去实现,这个方法就是我们在自定义View的时候最关注的方法了;
      • 调用dispatchDraw()方法去绘制子View,这个方法在View中是空方法,但在ViewGroup中有实现,它遍历去调用drawChild,在drawChild中又去调用了child.draw(),从而递归的去绘制View;
      • onDrawScrollBars()去绘制滚动条;
    invalidate和postInvalidate
    • invalidate只能在主线程中调用;
    • invalidate方法有几个重载方法,不仅仅是可以整个重绘View,也可以重绘View的一部分区域;
    • invalidate的内部调用了invalidateInternal,在invalidateInternal中调用了ViewParent.invalidateChild方法,把子View要刷新的区域传递给了父View;在父ViewGroup的validateChild方法中,循环层层先上传递(调用invalidateChildInParent方法),直到传递到ViewRootImpl,在ViewRootImpl的invalidateChildInParent方法中,调用scheduleTraversals()方法,scheduleTraversals会通过Handler的Runnable发送一个异步消息,调运doTraversal方法,然后最终调用performTraversals()执行重绘;
    • postInvalidate跟invalidate的作用是一致的,只是它会先向主线程的消息队列里发送一个MSG_INVALIDATE消息,主线程Looper到这个消息,就会调用invalidate,所以,postInvalidate支持子线程调用刷新UI;
    • invalidate以后,不会调用measure和layout,只会进行重绘需要重绘的View;
    • 这个有个tips需要大家注意下: setVisibility时,Gone掉和InVisible是有区别的,inVisible只会进行View的重绘,而Gone会调用requesetLayout和invalidate,导致整个View视图树的重新测量布局和绘制,所以,减少不必要的Gone的操作,可以适当的对应用进行优化
    requestLayout
      public void requestLayout() {
            ......
            if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
            ......
        }
    
    • 当调用了requestLayout之后,会层层向上调用,直到调用到ViewRootImpl的requestLayout方法:
    @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
                scheduleTraversals();
            }
        }
    
    • 最终触发了ViewRootImpl的scheduleTraversals()方法,最终会调用到ViewRootImpl的performTraversals()方法,不过,由于整个过程中触发的标记位与invalidate不一样,在performTraversals()中,会重新的measure和layout,也可能会调用draw,也可能不会调用draw;
    参考链接

    Android应用层View绘制流程与源码分析 (强烈推荐)

    Android View 深度分析requestLayout、invalidate与postInvalidate

    相关文章

      网友评论

        本文标题:Android View绘制流程总结

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