前言:
想要搞清楚View的完整的绘制流程,我认为主要分为三大块需要掌握。
第一块,最终呈现给用户看的其实是Window,那么Window与View的关系是怎样的?主要是搞清楚Window,DecorView,ViewRootImpl,WindowManager的关系。
第二块,了解了各个组件之间的关系,那么我们就可以开始了解一次完整的绘制是执行了怎样的一个流程。绘制流程是如何执行到我们常说的measure,layout,draw的流程上的。
第三块,draw的流程走完了。产生了各种绘制UI的指令,那么这些指令是如何形成数据,发给SurfaceFlinger,最终发送到硬件层进行展示的。
所以围绕着这是三块,写了三篇文章来进行详细的讲解。第一块和第二块主要是java层的讲解,第三块主要是JNI层的讲解。
本篇主要是讲解上面三块内容的第二块内容。
一.View一层一层向上通知
我们把开始的起点设置为View.setVisibility();
首先View中会有一层判断,只有显示状态并且有变化,才会向上进行通知。
if ((changed & GONE) != 0) {
needGlobalAttributesUpdate(false);
requestLayout();
那么流程是像下面这样的,一层一层的向上传递requestLayout,直到最上层的ViewRootImpl接收到。
PS:为什么最上层是ViewRootImpl,可以看这一篇文章:View绘制流程1-View与Window的关系_失落夏天的博客-CSDN博客
无论View,还是ViewGroup,还是DecorView,最终执行的其实都是View中的requestLayout的方法:
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
最终执行到ViewRootImpl的requestLayout方法中。在这个方法中,主要做了两件事,第一是执行主线程检查:checkThread ,第二是触发渲染流程scheduleTraversals。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
而checkThread方法就是我们经常说的,安卓的UI只能在主线程刷新的原因,因为这里做了主线程检查,不是主线程的请求就会抛出异常。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
而渲染流程scheduleTraversals则是单纯的注册,请注意,这里的注册并不代表立马会执,这个我们下一节来讲。
二.如何开始绘制流程
scheduleTraversals方法中,会先插入一个屏障消息,执行到屏障消息之后,同步消息会被阻断,Handler中只会执行异步消息。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//插入屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
另外,ViewRootImpl中会向Choreographer中注册类型为Choreographer.CALLBACK_TRAVERSAL的Callbak。而正在的完整渲染任务是在mTraversalRunnable中(下一节专门讲mTraversalRunnable中的任务),它会等待Choreographer中的回调机制去执行。
我们在看一下Choreographer的postCallback方法,最终其实会执行到postCallbackDelayedInternal方法。最终会把回调的任务(mTraversalRunnable)加入到一个队列当中。
mCallbackQueues是一个数组,数组的每个元素是一个链表(PS:有点类似于hashmap的结构)。这里加入的是数组的第1位的链表。
上面我们讲到是Choreographer.CALLBACK_TRAVERSAL类型的,其实一共有5种类型,这5种类型都可以分别添加,这个我们后面再讲。
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//加入到队列当中。这里mCallbackQueues是一个数组,数组是队列的类型。
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
接下来会有一个判断,判断执行时间是否小于当前时间,如果小于则会立马执行scheduleFrameLocked方法。另外如果大于的话,最终只是通过handler转发了一下(注意这里使用的是异步消息,所以同步消息在这期间是不会执行的),最终也会执行到scheduleFrameLocked方法的,所以我们只看scheduleFrameLocked方法就好了。
scheduleFrameLocked方法中,会判断是否使用VSYNC信号量,这个大多数手机都是使用的,另外一个场景我们就不讲了。
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
...
//lopper检查,默认肯定是true
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
...g);
}
} else {
...
}
}
}
所以又会走到scheduleVsyncLocked的方法,而这个方法里面很简单,调用scheduleVsync发送通知。
private void scheduleVsyncLocked() {
...
mDisplayEventReceiver.scheduleVsync();
...
}
最终会执行到DisplayEventReceiver.nativeScheduleVsync(mReceiverPtr)的方法。这里涉及到native的方法了,native层的逻辑我们会在第三块中详细讲解,这里就不扩展了,我们只需要知道,通过这个native方法,通知到APP的native层,然后最终会通知到surfaceFlinger。surfaceFlinger中会有一个硬件定时机制(一秒钟60帧或者90帧就来源于此),每隔固定的时间发送信号返回给APP的native层。
然后APP的native层会通过反射的方式调用DisplayEventReceiver的dispatchVsync方法。dispatchVsync方法中,会调用onVsync方法,进行通知。这个onVsync方法,会通知到Choreographer.FrameDisplayEventReceiver.onVsync中。这个方法中,仍然会发送一个异步消息。
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
try {
...
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData = vsyncEventData;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
Message中是可以带Runnable任务的,而FrameDisplayEventReceiver实现了Runnable接口,所以FrameDisplayEventReceiver中也实现了run方法。等到handler执行这条异步message消息的时候,就会调用FrameDisplayEventReceiver的run方法。
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
run中主要的实现方法是doFrame,那我们就看一下doFrame方法。doFrame中的内容很多,我们还是只看核心,核心主要是下面这部分:
void doFrame(long frameTimeNanos, int frame,
DisplayEventReceiver.VsyncEventData vsyncEventData) {
...
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos,
frameIntervalNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);
...
}
大家应该看到了,分别去会去执行5种不同类型的队列中的回调。而我们之前注册的CALLBACK_TRAVERSAL会在第四个位置被回调。
腾讯有一个GT的性能卡顿检测方法,其原理就是注册CALLBACK_ANIMATION类型的回调,因为CALLBACK_TRAVERSAL和CALLBACK_ANIMATION一定会是在同一个回调中执行,所以只要判断CALLBACK_ANIMATION的回调间隔,就知道刷新的间隔了,知道了刷新的间隔,自然也知道了是否卡顿了。
我们最后看一下doCallBacks方法。doCallBacks的核心作用其实就是执行队列中的runnable任务。而且是一次性的全部执行。也就是说如果队列中三个任务,那么会在这一次流程当中全部执行完。
void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
CallbackRecord callbacks;
...
for (CallbackRecord c = callbacks; c != null; c = c.next) {
...
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
这里我们看到finally的时候有一个方法recycleCallbackLocked。我们点进去看一下,
private void recycleCallbackLocked(CallbackRecord callback) {
callback.action = null;
callback.token = null;
callback.next = mCallbackPool;
mCallbackPool = callback;
}
我们在看一下如何创建CallbackRecord的?
private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
CallbackRecord callback = mCallbackPool;
if (callback == null) {
callback = new CallbackRecord();
} else {
mCallbackPool = callback.next;
callback.next = null;
}
callback.dueTime = dueTime;
callback.action = action;
callback.token = token;
return callback;
}
典型的对象池的使用,避免重复创建CallbackRecord对象。
说到这,也许你和我一样有个疑问,既然doCallBacks会执行队列中所有的runnable,那么如果界面卡顿了,积攒了三四甚至更多的runnable任务,那么全部执行的话,岂不是是一种浪费?因为只有最后一次才是真正生效的。
带着这个疑问,我回头看了一下添加的代码,发现这么用一句。原来,渲染任务并不是可以随意添加的,这里有判断,添加了一次之后就会设置为true,后面的渲染任务就加不进来了。所以,一个渲染周期内,无论我执行多少次setVisibility这样的方法,最终真正触发的绘制流程mTraversalRunnable只会执行第一次。
void scheduleTraversals() {
//这里有一个标记位判断
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
一直到真正执行渲染任务的时候,才会设置为false,允许下一次的渲染任务添加。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
...
}
}
三.渲染一层一层向下刷新
3.1 doTraversal方法实现的功能
经过了上面流程的层层转发,最终终于执行到了mTraversalRunnable这个渲染任务了。这个runnable中会去调用doTraversal方法,这个方法中,主要做了三件事:
1.把标记位mTraversalScheduled改回false外,
2.取消屏障消息,允许同步消息的执行。因为此时渲染任务开始执行了,所以不存在还会被插队一说。
3.执行了真正的渲染流程方法:performTraversals()方法。
void doTraversal() {
if (mTraversalScheduled) {
//改回标记位,允许插入新的渲染任务
mTraversalScheduled = false;
//取消屏障消息,允许同步消息的执行
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
//执行真正的渲染任务
performTraversals();
...
}
}
3.2 performTraversals中去触发measure,layout,draw三大流程
在performTraversals方法中,回去执行完成的渲染的所有流程。
private void performTraversals(){
...
//1.触发首次measure
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
//如果windowManager.LayoutParams的horizontalWeight或verticalWeight大于0,则会再次触发一次measure
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
//2.只要ViewRootImpl触发requestLayout,mLayoutRequested=true,则didLayout=true
performLayout(lp, mWidth, mHeight);//触发layout
...
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
//3.draw是真正绘制到界面上
performDraw();
...
mIsInTraversal = false;
}
}
总结一下,主要包含三大块功能:
1.measure:主要负责测量每个View的所应该分配的区域,有可能会执行多次。由上层向下层一层一层的传递,而每一层的ViewGroup也都有可能会执行多次测量,安卓渲染效率低,或者说多层嵌套会导致绘制卡顿,也就是因为这个原因。
2.layout:上面测量完成了,那么layout就负责划定区域。这个流程只会执行一次。
3.draw:划定了区域,那么就可以开始最终的绘制了。这个流程会根据上面计算出来的区域,以及自身的属性,会利用canvas完成最终画布的绘制。举一个例子,TextView的绘制流程如下(代码过多,有删减)
@Override
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
// Draw the background for this view
super.onDraw(canvas);
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int compoundPaddingTop = getCompoundPaddingTop();
final int compoundPaddingRight = getCompoundPaddingRight();
final int compoundPaddingBottom = getCompoundPaddingBottom();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final int right = mRight;
final int left = mLeft;
final int bottom = mBottom;
final int top = mTop;
final boolean isLayoutRtl = isLayoutRtl();
final int offset = getHorizontalOffsetForDrawables();
final int leftOffset = isLayoutRtl ? 0 : offset;
final int rightOffset = isLayoutRtl ? offset : 0;
final Drawables dr = mDrawables;
if (dr != null) {
/*
* Compound, not extended, because the icon is not clipped
* if the text height is smaller.
*/
int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mShowing[Drawables.LEFT] != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
dr.mShowing[Drawables.LEFT].draw(canvas);
canvas.restore();
}
...
canvas.restore();
}
3.3 canvas发出渲染指令,最终显示屏幕到上
我们可以看到会有canvas.drawLine(绘制线条),canvas.clipRect(绘制矩形区域)这样的操作。这种操作最终是如何传递到surfaceFlinger进行合成,又是如何最终展示到屏幕上的呢?这就是我们下一章要将的内容了。