关于view的绘制流程,现在网上一查,就会直接告诉你,view的绘制流程是先onMeasure,然后onLayout,在最后onDraw,没错,绘制流程确实也是这样。不过我们今天要讨论的话题主要是知道这个流程是怎么来的,然后顺便浅尝他们的内部的实现流程和逻辑。这样大家就会对view的绘制流程有一个比较清晰的认识。
这里我想从view的一些常用方法来进行研究,就从invalidate这个方法开始吧。相信这个方法很多人都用过,在自定义view的时候,或者在刷新view的时候,大家会经常用到这个函数。所以通俗了讲,这个函数就是用来刷新view的。那么就开始吧。
注:以下代码来源API 14
为什么要用这么老的代码分析是有原因的,因为越先考虑到的方案一定是越粗暴越简单的,所以我们可以通过了解API 14的源码,很快也很清晰的认识到整个view的绘制流程,当讨论完API 14的源码之后,我将会带你走一遍API 28的源码,不过这个时候,看API 28的源码对你也就相对简单了。
开门见山,直接开看invalidate的源码:
public void invalidate() {
invalidate(true);
}
显然这种源码是无法满足我们的求知欲的,所以让我们跟进去。
void invalidate(boolean invalidateCache) {
...
final ViewParent p = mParent;
...
if(xxx){
...
p.invalidateChild(this, null);
return;
}
if(xxx){
...
p.invalidateChild(this, r);
}
...
}
我们可以看到,实际上我们是调用了ViewParent的invalidateChild方法,而ViewParent实际上只是一个接口,那么是由谁实现的这个接口呢,其实就是ViewRootImpl,那就来吧。
// ViewRootImpl.java
public void invalidateChild(View child, Rect dirty) {
...
scheduleTraversals();
...
}
继续跟踪:
// ViewRootImpl.java
public void scheduleTraversals() {
...
sendEmptyMessage(DO_TRAVERSAL);
...
}
哈,可以直接发送message,没错,ViewRootImpl其实继承handler,也不难猜,毕竟只能在主线程更新UI,所以用到handler也不奇怪,那我们就接着看吧。
// ViewRootImpl.java
@Override
public void handleMessage(Message msg) {
switch(msg.what){
...
case DO_TRAVERSAL:
...
performTraversals();
...
break;
...
}
}
接着我们进入performTraversals(),不过这个方法奇长无比,我们要找的绘制流程,其实全都在这里,所以让我们来慢慢欣赏吧。
不如继续简化版:
// ViewRootImpl.java
private void performTraversals() {
final View host = mView;
...
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
mView.draw(canvas);
...
}
简化的是不是太简单了,其实这个方法里面有很多大量且复杂的变量赋值设置转换等等的代码。
但是这样有利于我们分析代码主干逻辑,不是吗。
不过这里首先讲一下方法里面的mView是啥,如果是调用的invalidate,那这里的view自然是调用invalidate方法的view,如果是setContentView,那么这里的view就是DecorView,我在setContentView的时候,到底发生了什么这篇文章有讲到这个DecorView。
分析到这里,我想大家就很熟悉了,view绘制的三大流程,measure、layout、draw,其实分析setContentView也能很容易分析到这里来。
接下来我们重点看看这三个方法是一个怎样的过程。
首先我们来看看测量,代码是
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
那么我们先来看看这里的这两个参数具体是什么东西。
听名字是跟宽高有关的东西,确实是这样,这两个参数代表了这个view的宽和高。
我们来看看view的measure方法是怎么处理这两个参数的
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
onMeasure,这不就是我们平时自定义view要重写的方法吗,所以这也是为什么我们可以通过这个方法来确认view大小的原因。
我们是否有过自定义view,没有重写这个方法,回想一下,会出现什么情况,我们的view会铺满整个父容器对吗。
如果我们没有重写onMeasure方法,那么调用的方法是什么呢,当然是view类自己实现的onMeasure方法啦,不如我们来看看view自己实现的onMeasure做了什么事。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
看似简单,却方法嵌套,并不好分析,应该先分析外层还是内层呢,当然是外层啦,让我们来看看setMeasuredDimension方法。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET;
}
很简单,就是单纯的赋值,将测量出来的宽高赋值给view类的宽高两个属性。自此我们可以看出,view的真实宽高其实就保存在mMeasuredWidth和mMeasuredHeight这两个变量之中。
接着我们再来看看getDefaultSize方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这里出现了MeasureSpec,大概讲一下。MeasureSpec是类,所以我们可以将其类比成Integer类,不过这个类不仅仅保存了值,还保存了这个值的模式,无论是宽还是高,都有一种模式,所以MeasureSpec保存了这个数据的模式和值,值就是具体的值,比如宽20高20,这就是具体的值。
一个MeasureSpec类型的变量,我们可以通过MeasureSpec.getMode(measureSpec);
得到这个变量的模式,通过MeasureSpec.getSize(measureSpec);
得到这个变量的值。
那么模式又分为哪些呢,分为三种
这三种具体代表什么意思,通俗来讲EXACTLY类似MATCH_PARENT,这个很熟悉吧,即填充父容器,宽或者高填充父容器。
AT_MOST类似WRAP_CONTENT,自己有多大就占多大地方。
我们可以看到前两种模式都能够计算出一个具体的值,有具体的宽度和高度,但是UNSPECIFIED比较奇特,没有明显的值,为什么会出现这种东西,大小都不能确定的view,这有可能吗,当然有可能,ListView不就是一个典型的例子吗,没有特定的高度,UNSPECIFIED就是用于这种view的。
好了,MeasureSpec讲完了,我们回过头来继续看getDefaultSize方法。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
还记得这个方法是干嘛的吗,获得具体的宽和高。将计算出来的值赋予view的mMeasuredWidth和mMeasuredHeight这两个变量。
首先我们来看参数,第一个参数是view自己计算出来的,第二个参数这是在ViewRootImpl#performTraversals()方法中计算出来的,所以记住,第二个参数是在其他地方计算出来的,这意味着精准。
所以我们在getDefaultSize方法中可以看到,AT_MOST和EXACTLY模式下,系统都选择了使用在其他地方计算出来的值作为宽高。
也是由于AT_MOST和EXACTLY模式下,View的宽高没作任何处理,所以我们自定义view的时候需要在这里作一下处理,比如处理一下AT_MOST模式下的情况,毕竟AT_MOST模式意味着WRAP_CONTENT。不过也可以理解,毕竟系统并不知道你自定义的view到底长什么样,对宽高要有怎样的要求。
接下来我们来看看getDefaultSize方法的第一个参数是怎么来的,这并不是在其他地方计算出来的,而是view内部就计算出来了,所以归类为UNSPECIFIED模式。第二个参数分别从getSuggestedMinimumWidth()和getSuggestedMinimumHeight()得到,其实这两个方法差不多,我们拿一个方法举例子。
protected int getSuggestedMinimumWidth() {
int suggestedMinWidth = mMinWidth;
if (mBGDrawable != null) {
final int bgMinWidth = mBGDrawable.getMinimumWidth();
if (suggestedMinWidth < bgMinWidth) {
suggestedMinWidth = bgMinWidth;
}
}
return suggestedMinWidth;
}
首先告诉大家mMinWidth变量是什么东西,这个东西默认为0,其实我们可以直接在布局文件里给这个变量赋值,像这样:
如果没有定义,这个值就是0。
至于mBGDrawable这个变量,这是指view的背景,也可以在布局文件里面设置,如果没有,那就没有了。
自此,我们基本上就算分析完了整个测量的过程。
是以下代码将我们拉入布局这里的
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
所以我们进入view的layout方法,一探究竟。
public void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
...
}
changed变量记录着该view是否有过位置变化,其实其实单单看view的布局方法未免有些单调,因为onLayout方法如下
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
这里是空的,原因很简单。回想一下,我们什么时候才会重写onLayout,当我们在自定义ViewGroup的时候,才会重写onLayout,而这个方法的目的是确定ViewGroup的子View的位置,所以View类里面的这个方法必然是空的。
不过要知道,我们应用的界面,肯定不会直接使用ViewGroup,而是使用他们的子类,比如LinearLayout,这些子类对onLayout方法的重写肯定是很到位的。
所以我们还是直接进入绘制这个流程吧。
这里使用到的是View的draw方法,是draw,不是onDraw哦,这个方法十分亲民,因为方法内部注释了绘制view的整个流程,如下:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 绘制遍历执行几个绘制步骤,这些步骤必须按适当的顺序执行:
* 1. Draw the background
* 绘制背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 如有必要,保存画布的图层以备渐变
* 3. Draw view's content
* 绘制视图的内容
* 4. Draw children
* 绘制子view
* 5. If necessary, draw the fading edges and restore layers
* 如有必要,绘制渐变边缘并恢复图层
* 6. Draw decorations (scrollbars for instance)
* 绘制装饰品(例如滚动条)
*/
而我们在自定义view的时候,通常不是会重写onDraw方法吗,这个方法在上述第三步被调用,也就是视图的主要内容就是onDraw绘制的。
(感觉注释把绘制的流程写的好清楚= =)
接下来我们来看API 28的源码,由于很多逻辑都和API 14相同,所以我们快速过一遍即可。
也从invalidate开始分析好了,其实前面都差不多,都可以很方便的跟踪到ViewRootImpl#scheduleTraversals()方法来,不过API 14 和API 28在这里的处理有区别,28主要源码如下:
// ViewRootImpl.java
void scheduleTraversals() {
...
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
...
performTraversals();
...
}
然后就又来到包含了测量布局和绘制的performTraversals方法来了。
// ViewRootImpl.java
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
其实这里省略了的代码量其实非常大,如果没有目的的看这个方法,相信要看很久很久。经过8年(11~18)的优化和演变,这个方法在绘制view上的主要逻辑还是没有变,可想当初在设计view的时候,下了多大的功夫,才能这么稳定。闲话不说,先看看测量有关的源码。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
代码很少,这里就不做省略了,看源码,没想到Android源码也用上了Systrace工具。
我们可以看到依然调用了view的measure方法。而measure方法内部也调用了View的onMeasure,几乎和API 14的源码相同,这里就不多做展示了。接下来我们来看布局performLayout。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
省略之后好像也没有什么变化是吧,layout方法里面自然也调用了view的onLayout方法,相比14的变化,其实很少,大多都是把14原有的代码进行了封装,变得更好被使用,然后新加了很多情景条件而已。
接下来看绘制。
API 28里面都绘制倒是变得复杂不少,应该是这些年来,Android的UI变化很大的原因导致的吧。我们使用跟踪法来看。
private void performDraw() {
...
boolean canUseAsync = draw(fullRedrawNeeded);
...
}
然后我们继续看draw方法。
private boolean draw(boolean fullRedrawNeeded) {
...
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)
...
}
这个方法绝对没有上述那么简单,而且很复杂,里面牵扯了大量和渲染器有关的代码,而且还考虑到了view被滑动时的UI绘制,比API 14不知道复杂到哪里去了= =
我们接着看。
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
这个方法也不是这么简单,看参数就知道,这个方法不仅涉及绘制,而且主要是对view的偏移量的计算和控制。
关于view的draw就不说了,跟API 14一样,区别不大,而且在注释里面也很贴心的记录了绘制的具体过程,虽然上面我已经贴过了,不过这里再贴一次。
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 绘制遍历执行几个绘制步骤,这些步骤必须按适当的顺序执行:
* 1. Draw the background
* 绘制背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 如有必要,保存画布的图层以备渐变
* 3. Draw view's content
* 绘制视图的内容
* 4. Draw children
* 绘制子view
* 5. If necessary, draw the fading edges and restore layers
* 如有必要,绘制渐变边缘并恢复图层
* 6. Draw decorations (scrollbars for instance)
* 绘制装饰品(例如滚动条)
*/
同样在第三步的时候,调用了view的onDraw方法。
好了,关于API 28的源码探索就到这里吧。
本文讨论的主要是view的三大流程的来源,以及这三大流程里面大概做了什么,并没有涉及详细的代码分析,主要是为了给大家一个大概的结构,而非钻研其实现。并且还分析了API 14和API 28,我们发现其实这两个版本的代码其实在主干上其实并没有多少差别。只是后者更加完善,并且实现方式也更加巧妙了。所以view的流程分析大概就到这里吧。