Android|Android View绘制原理(绘制流程调度、测算等)

本文主要关注View的测量、布局、绘制三个步骤,讨论这三个步骤的执行流程。本文暂不涉及View和Window之间的交互以及Window的管理。在论述完这三个步骤之后,文末以自定义TagGroup为例,讲述如何自定义ViewGroup。
Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
目录 View 树的绘图流程 View树的绘图流程是由核心类:ViewRootImpl 来处理的,ViewRootImpl作为整个控件树的根部,它是控件树正常运作的动力所在,控件的测量、布局、绘制以及输入事件的派发处理都由ViewRootImpl触发。
核心成员变量
这里我主要讲几个Handler:
ViewRootHandler
这是ViewRootImpl调度的核心,其处理的消息事件主要有:
MSG_INVALIDATE、MSG_INVALIDATE_RECT、MSG_RESIZED、MSG_DISPATCH_INPUT_EVENT、MSG_CHECK_FOCUS、MSG_DISPATCH_DRAG_EVENT、MSG_CLOSE_SYSTEM_DIALOGS、MSG_UPDATE_CONFIGURATION等
主要有以下几类:View绘制相关、输入焦点等用户交互相关、系统通知相关。
有经验的同学肯定遇到过这样的场景:动态创建一个View之后,想要直接获取measureWidth 和 measureHeight往往取不到,这个时候我们会通过view.postDelayed()方法去获取。那么,问题来了,为什么这样就能取到呢?
答案就在ViewRootImpl中的ViewRootHandler,view.post--> attachInfo.mHandler.post --> ViewRootImpl ViewRootHandler. 这个Handler保证了当你post的runable被执行到时,view早就测量好了。
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); }// Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; }

Choreographer.FrameHandler
Choreographer这个类来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操作,这里不得不提一下Choreographer.FrameHandler目的就在于ViewRootImpl中涉及到到的View绘制流程,是通过Choreographer.FrameHandler来进行调度的。具体的调度过程如下:
1、 ViewRootImpl.scheduleTraversals
这个方法会往Choreographer注册类型为Choreographer.CALLBACK_TRAVERSAL的Callback。
// ViewRootImpl.scheduleTraversals 注册callback mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

2、 Choreographer.FrameHandler
Choreographer.FrameHandler源码如下,主要处理三个信号:
MSG_DO_FRAME:开始渲染下一帧的操作
MSG_DO_SCHEDULE_VSYNC:请求Vsync信号
MSG_DO_SCHEDULE_CALLBACK:请求执行callback
对于这三个信号,Choreographer是有一个调度过程的,最终callback的回调执行都是落实到doFrame()方法上面的。
private final class FrameHandler extends Handler { public FrameHandler(Looper looper) { super(looper); }@Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DO_FRAME: doFrame(System.nanoTime(), 0); break; case MSG_DO_SCHEDULE_VSYNC: doScheduleVsync(); break; case MSG_DO_SCHEDULE_CALLBACK: doScheduleCallback(msg.arg1); break; } } }

doFrame执行回调有一个顺序的,顺序依次如下:
Choreographer.CALLBACK_INPUT
Choreographer.CALLBACK_ANIMATION
Choreographer.CALLBACK_TRAVERSAL
Choreographer.CALLBACK_COMMIT
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

关于Choreographer,读者可以参考下这篇文章,讲的非常详细:Android Choreographer 源码分析
如何动态去检测APP卡顿
这里简单说一个小窍门,通过Choreographer.getInstance().postFrameCallback() 注册回调,并计算前后两帧的时间差,我们可以测算出APP的掉帧数,从而动态检测APP 卡顿。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {long lastFrameTimeNanos = 0; long currentFrameTimeNanos = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTimeNanos == 0) { lastFrameTimeNanos = frameTimeNanos; } currentFrameTimeNanos = frameTimeNanos; long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS); long droppedCount = 0; if (diffMs > 100) { droppedCount = (int) (diffMs / 16.6); String anrLog = collectAnrLog(applicationContext); DjLog.e("Block occur, droppedCount: " + droppedCount + ", anrLog: " + anrLog); } lastFrameTimeNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } });

View树流程控制:performTraversals
整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:
Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
image 更详细的图示如下:

View树绘制过程.png performTraversals 方法非常庞大,整个源码在800行左右,看起来会让人吐血。这个方法主要的过程有四个:
预测量阶段
这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()/Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。
布局窗口阶段
根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。
最终测量阶段
预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多(参考第4章),WMS不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。
布局控件树阶段
完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。
绘制阶段
这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。
那问题来了,这个方法什么时候会被触发,或者说Android系统什么时候会对整个View树进行一次全量的操作呢?从源码中,我们可以看到以下几个核心的方法会触发:
  1. requestLayout: 注意在View中也有同样的一个requestLayout方法,view中的requestLayout方法调用的就是ViewRootImpl中的requestLayout,最终触发View树的绘制流程,即 measure-layout-draw;
  2. invalidate:同样的View中也有一个invalidate方法,View中该方法的调用最终调用的也是ViewRootImpl中的方法。有经验的同学肯定知道,invalidate只会触发draw,不会触发measure和 layout。具体的ViewRootImpl会通过变量mLayoutRequested控制是否要进行measure和layout,invalidate操作时这个变量为false
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }void invalidate() {...if (!mWillDrawSoon) { scheduleTraversals(); } }

View 绘制流程函数调用链 Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
image 有几点注意:
? invalidate/postInvalidate 只会触发 draw;
? requestLayout,会触发 measure、layout 和 draw 的过程;
? 它们都是走的 scheduleTraversals -> performTraversals,用不同的标记位来进行区分;
? resume 会触发 invalidate;
? dispatchDraw 是用来绘制 child 的,发生在自己的 onDraw 之后,child 的 draw 之前
Measure 和 Layout 的具体过程
Measure 和 Layout 的具体过程 Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
image 关于Measure过程,不得不详细提一下MeasureSpec。MeasureSpec是一个复合整型变量(32bit),用于指导控件对自身进行测量,它有两个分量:前两位表示SPEC_MODE,后30位表示SPEC_SIZE。SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,SPEC_SIZE则是父视图给定的指导大小。
SPEC_MODE有三种模式,具体的计算如下:
MeasureSpec.UNSPECIFIED: 表示控件在进行测量时,可以无视SPEC_SIZE的值。控件可以是它所期望的任意尺寸。
MeasureSpec.EXACTLY: 表示子控件必须为SPEC_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一确定值,或者是MATCH_PARENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。
MeasureSpec.AT_MOST: 表示子控件可以是它所期望的尺寸,但是不得大于SPEC_SIZE。当控件的LayoutParams.width/height为WRAP_CONTENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。
自定义一个TagGroup 讲了这么多,下面我们来实操一下。
需求:自定义一个TagGroup,用来显示一系列标签元素。要求标签样式完全可以自定义,标签间距可在xml中指定,要有最多显示多少行的控制,显示不全时要展示“更多 ...”
样式协定
在attrs.xml中协定样式:

协定接口,用来提供具体的标签元素:
public interface TagViewHolder { View getView(); }

自定义Measure过程
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); int width = 0; int height = 0; int row = 0; // The row counter. int rowWidth = 0; // Calc the current row width. int rowMaxHeight = 0; // Calc the max tag height, in current row.if (moreTagHolder != null) { moreTagMeasureWidth = moreTagHolder.getView().getMeasuredWidth(); moreTagMeasureHeight = moreTagHolder.getView().getMeasuredHeight(); }final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); if (child.getVisibility() != GONE) { // judge the max_row if (row + 1 >= maxRow && rowWidth + childWidth> widthSize) { break; } rowWidth += childWidth; if (rowWidth > widthSize) { // Next line. rowWidth = childWidth; // The next row width. height += rowMaxHeight + verticalSpacing; rowMaxHeight = childHeight; // The next row max height. row++; } else { // This line. rowMaxHeight = Math.max(rowMaxHeight, childHeight); } rowWidth += horizontalSpacing; } }// Account for the last row height. height += rowMaxHeight; // Account for the padding too. height += getPaddingTop() + getPaddingBottom(); // If the tags grouped in one row, set the width to wrap the tags. if (row == 0) { width = rowWidth; width += getPaddingLeft() + getPaddingRight(); } else {// If the tags grouped exceed one line, set the width to match the parent. width = widthSize; }setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height); }

自定义layout过程
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int parentLeft = getPaddingLeft(); final int parentRight = r - l - getPaddingRight(); final int parentTop = getPaddingTop(); final int parentBottom = b - t - getPaddingBottom(); int childLeft = parentLeft; int childTop = parentTop; int row = 0; int rowMaxHeight = 0; boolean showMoreTag = false; final int count = getChildCount(); int unTagCount = count; if (moreTagHolder != null) { unTagCount--; } for (int i = 0; i < unTagCount; i++) { final View child = getChildAt(i); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); if (child.getVisibility() != GONE) { if (row + 1 >= maxRow && childLeft + width + (horizontalSpacing + moreTagMeasureWidth)> parentRight) { // 预留一个空位放置moreTag showMoreTag = true; break; } if (childLeft + width > parentRight) { // Next line childLeft = parentLeft; childTop += rowMaxHeight + verticalSpacing; rowMaxHeight = height; row++; } else { rowMaxHeight = Math.max(rowMaxHeight, height); }// this is point child.layout(childLeft, childTop, childLeft + width, childTop + height); childLeft += width + horizontalSpacing; } }if (showMoreTag) { final View child = getChildAt(count - 1); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); child.layout(childLeft, childTop, childLeft + width, childTop + height); } }

使用
在xml中直接引用

定义自己的TagViewHolder
public class DjTagViewHolder implements DjTagGroup.TagViewHolder {public String content; public View rootView; public TextView tagView; public DjTagViewHolder(View itemView, String content) { this.rootView = itemView; tagView = itemView.findViewById(R.id.tag); tagView.setText(content); tagView.setOnClickListener(v -> Toast.makeText(context, "点击了:" + content, Toast.LENGTH_SHORT).show()); }@Override public View getView() { return rootView; } }

往DjTagGroup直接设置tags
private void initDjTags() { String[] tags = TagGenarator.generate(10, 6); List viewHolders = new ArrayList<>(); for (String tag: tags) { DjTagViewHolder viewHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false), tag); viewHolders.add(viewHolder); } DjTagViewHolder moreHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false), "更多 ..."); djTagGroup.setTags(viewHolders, moreHolder); }

实际的效果

Android|Android View绘制原理(绘制流程调度、测算等)
文章图片
源码地址: Github: 自定义View辑录DjCustomView
【Android|Android View绘制原理(绘制流程调度、测算等)】参考文章
Hencoder: 自定义View相关

    推荐阅读