Android组件View绘制流程原理分析
Android组件View绘制流程原理分析
android视图构成
如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件的,里面有TitleActionBar和我们setContentView传入进去的layout布局文件
- Window类时一个抽象类,提供绘制窗口的API
- PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View
- DecorView继承FrameLayout,里面id=content的就是我们传入的布局视图
依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置
setContentView流程
setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,重点概括为:
-
创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
-
依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。
-
将Activity的布局文件添加至id为content的FrameLayout内。
-
当setContentView设置显示OK以后会回调Activity的onContentChanged方法。Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。
android的View绘制
view绘制主要包括三个方面:
- measure 测量组件本身的大小
- layout 确定组件在视图中的位置
- draw 根据位置和大小,将组件画出来
视图绘制的起点在ViewRootImpl类的performTraversals()方法,该方法完成的工作主要是: 根据之前的状态,判定是否重新计算测试视图大小(measure)、是佛重新放置视图位置(layout)和是否重新重绘视图(draw) ,部分源码如下:
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
measure计算视图大小
几乎所有的组件都是继承View类的,而关于view的测量工作,日常开发用得多的方法就是measure和onMeasure两个方法,measure不可重写,当我们自定义时主要重写onMeasure方法即可,在方法内部我们必须完成组件的mMeasuredWidth和mMeasuredHeight实际尺寸测量,而这个尺寸是需要父视图和子视图共同决定的
measure流程从根视图measure遍历整个view树结构,如下:
还要注意视图尺寸MeasureSpec是一个组合尺寸,它是一个32位bit值,高两位是尺寸模式specMode,低30位是尺寸大小值,我们可以利用提供的原声库方法很方便的进行尺寸组合和拆解:
specMode有三种: MeasureSpec.EXACTLY表示确定大小, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不确定
int measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); //合成
int specMode = MeasureSpec.getMode(measureSpec); //拆解
int specSize = MeasureSpec.getSize(measureSpec);
而在视图测量meause中,父组件传给子组件的一般都是一个组合尺寸,我们可以拿出具体尺寸然后根据其他条件产生一个新的尺寸值,将这个值用setMeasuredDimension设置mMeasuredWidth和mMeasuredHeight具体尺寸,完成测量;
measure原理总结
- MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
-
View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
-
最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
-
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
-
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
-
View的布局大小由父View和子View共同决定。
-
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
layout视图位置确定
layout的流程主要也是遍历整个view树结构,调用view.layout(int l, int t, int r, int b)确定好view的具体坐标位置,流程图如下
当我们自定义一个组件时,通常时重写onLayout方法,里面实现好自己的逻辑,最后在调用layout方法完成视图位置确定,如果自定义组件时一个ViewGroup的话,还需要我们去遍历每一个child确定尺寸
layout原理总结
-
整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:
-
View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
-
measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
-
凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。
-
使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
draw绘制
完成measure和Layout后,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程
先来看下View树的递归draw流程图,如下:
draw原理总结
可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:
-
如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
-
View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
-
View的绘制是借助onDraw方法传入的Canvas类来进行的。
-
区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
-
在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
-
默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
View的刷新机制
当我们调用view.invalidate或者setText()时,其背后view是如何绘制的,是立即绘制,还是有一个声明样的执行流程
这里就要引出另一个东西FPS,Android FPS为60,在这个帧率上我们不会查看到屏幕有撕裂画面出现,这个60如何而来?
由硬件触发每个16ms产生一次VSYNC信号,从而触发view绘制任务(添加了绘制任务的前提下才会触发),这样一秒就绘制60次
view结构树
如本文最顶上view树图,其实最上层还有一个ViewRootImp对象,它才是整个view树的最顶层;当我们调用view.invalidate()要求重绘时,他会依次调用:
当view执行invalidate时,会调用它的parent.invalidateChild,层层调用调用,直到顶层ViewRootImp执行invalidateChildInParent绘制,这也好理解,因为紧靠自身view无法测量、布局和绘制自己,需要整颗view到自己的引用树
最终顶层的invalidateChildInParent方法会执行到scheduleTraversals遍历函数:
void scheduleTraversals() {
1. 防止重复调用
if (!mTraversalScheduled) {
mTraversalScheduled = true;
2. 发送同步屏障,保证优先处理异步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
3. 提交mTraversalRunnable绘制任务到Choreographer回调中去,这个mTraversalRunnable
任务会执行doTraversal方法
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
以下这个函数就是mTraversalRunnable任务
void doTraversal() {
if (mTraversalScheduled) {
1. mTraversalScheduled 置为 false
mTraversalScheduled = false;
2. 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
3. 开始布局,测量,绘制流程,这步就会执行上面讲到的那些layout、measure和draw了
performTraversals();
......
}
但是现在这个任务mTraversalRunnable是如何提交到Choreographer任务中,什么时候又执行呢?
我们进入Choreographer编舞者这个类进去看看,首先看看任务添加时机:
添加绘制任务
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
......
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
传入的参数依次是 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null,0
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
1. 将 mTraversalRunnable 塞入队列
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) { 立即执行
2. 由于 delayMillis 是 0,所以会执行到这里
scheduleFrameLocked(now);
} else { 延迟执行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
在第一个步骤,将我们的绘制任务添加到了mCallbackQueues任务列表中去,然后执行schudleFrameLocked函数,在这个函数中会注册一次监听(nativeScheduleVsync)底层VYSNC信号,这个VYSNC信号是每个16ms由硬件触发,触发后就会执行onVsync方法,那这个onVsync是干什么的呢?
看看就知道了:
vsync 信号监听回调
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
......
long now = System.nanoTime();
timestampNanos 是 vsync 回调的时间,不能比 now 大
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
......
mTimestampNanos = timestampNanos;
mFrame = frame;
这里传入的是 this,会回调本身的 run() 方法
Message msg = Message.obtain(mHandler, this);
这是一个异步消息,保证优先执行
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
doFrame(mTimestampNanos, mFrame);
}
vsync信号来临后,会发送一个handler异步消息,由于传入时间小于当前时间,而且messageQueue中还有一个同步栏,所以会立即执行这个消息,这个消息是一个run的消息,也就是会执行doFrame方法
相信你也才到了,doFrame中肯定会执行我们之间添加的重绘任务,看看吧!
frameTimeNanos传入的是VYSNC的产生信号时间
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
......
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
计算超时时间
frameTimeNanos 是 vsync 信号回调的时间,startNanos 是当前时间戳
相减得到主线程的耗时时间
final long jitterNanos = startNanos - frameTimeNanos;
mFrameIntervalNanos 是一帧的时间,16ms
if (jitterNanos >= mFrameIntervalNanos) {
做除法,得到16ms的倍数,也就是有多少帧
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
掉帧超过 30 帧,打印 log
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
......
}
try {
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
doCallBacks() 开始执行回调
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
我们的回调就在CALLBACK_TRAVERSAL这个type中
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
}
......
}
还要注意的是,在doCallbacks方法中,执行任务后,会将任务清除掉,也就是说你要再次执行重绘任务的话,在下一个vsync信号来临之前要再次添加任务,使用Choreographer.postCallback将新的任务添加到回调中,也就是你需要在使用一次invalidate方法
来个图片小结:
view提供的API控制视图的方法
invalidate和postInvalidate方法源码分析
请求重新绘制视图,调用draw
- invalidate在主线程调用
- postInvalidate是在非主线程调用
View的requestLayout方法
requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。
本文参考于:这里
view计算与渲染
View绘制会用到CPU和GPU,CPU主要是完成上面的measure/layout测量每个view,最后使用onDraw绘制到canvas上去,而这个canvas来源于phoneWindow的Surface,所以最终绘制的view视图都会转移到surface里面去;
我们知道,我们视图最终都会转递给surfaceflinger去,而我们的window里面的surface就是由surfaceflinger创建的,并且对应一个Graphic Buffer,这块内存主要就是传递图像数据的;
插入一个 displaylist 概念,displaylist缓存了view视图的绘制指令,包含它的位置、大小、透明度等绘制信息,对应view的drawCanvas绘制,每个view视图对应一个displayList,并且父view包含子view的displaylist,多以最终提交到GPU的Render thread时,只需要绘制根节点的displaylist即可;
所以CPU需要计算、测量view树中每个view的大小、位置以及draw到canvas,构建绘制指令序列到displayList,最终提交到GPU的Render Thread渲染线程,完成渲染;
Android有软件和硬件渲染:
- 软件渲染,CPU会将所有的displayList绘制到一个bitmap中,提交到GPU作为纹理,贴图上去
- 硬件渲染,就是上述讲到的过程
硬件渲染的优劣
当第二帧页面渲染时,如果视图view树没有变化或是简单变化(透明度、大小),就无需重新执行onDraw方法,直接复用上次提交displayList,简单修改其属性后渲染即可;
若是view树有更新,其对应的displayList也会发生更新并且通过Graphic Buffer提交给GPU,但是硬件渲染有个好处是只绘制有更新的脏区,软件绘制则会全部都会更新
android 4.0以后默认会开启硬件加速!也有缺点,更耗电和内存。
对不可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。
但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视
每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。
在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示比较复杂,需要先经过CPU换算成纹理,然后交给GPU进行渲染,返回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则存在一个更加复杂的操作流程。
为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。