Android View绘制流程总结
最近在学习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绘制流程与源码分析 (强烈推荐)