持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
一、前言
之前的Android消息机制中,我们知道了MessageQueue中的Message是按照message.when顺序以链表的形式排列的,主线程一直在处理MessageQueue里的Message。
主线程在同一时间只能处理一个Message.
那对于Android这样一个前台与用户展示与交互比较繁重的系统,不管是系统开机还是启动一个app。
这里面都有大量的绘制任务.此时主线程肯定已经收到各种繁重任务的message,我们希望绘制任务的message提前执行或者有一些优先级别较高的message,此时该如何告诉Looper呢
在深入探讨这个问题之前我们需要先提前了解一下Android的刷新机制
二、显示系统
一个通用的显示系统一般分为CPU,GPU,Display三个部分
- CPU负责计算数据,把计算好的数据交给GPU
- GPU会对图形数据进行渲染,渲染好后放到buffer里
- Display负责把buffer里的数据呈现到屏幕上
三、Android的Choreographer的实现原理
3.1 invalidate()
Activity中的布局首次绘制,以及每次调用view的invalidate()时
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
....
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
3.2 invalidateChild()
子view的invalidate(),最终走到了父布局的invalidateChild()方法
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
...
ViewParent parent = this;
if (attachInfo != null) {
....
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...
parent = parent.invalidateChildInParent(location, dirty);
...
} while (parent != null);
}
}
3.3 invalidateChildInParent()
通过do while循环查找父布局,最终指向了顶层父布局ViewRootImpl的invalidateChildInParent方法
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
...
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
3.4 scheduleTraversals()
View的invalidate()方法中,通过一系列的条件判断和循环查找父布局的工作,最终走到了ViewRootImpl的scheduleTraversals()方法里.
其实,invalidate(),requestLayout(),requestFocus(),以及设置背景,设置动画等与view相关的刷新操作,最终都会调用view的scheduleTraversals()方法
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里面用到了我们熟悉的handler.getlooper
mHandler.getLooper().getQueue().postSyncBarrier();
同时还传入了一个Runnable
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
3.5 doTraversal()
void doTraversal() {
if (mTraversalScheduled) {
...
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
3.6 performTraversals()
private void performTraversals() {
...
{
//View的Measure
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
}
final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
...
if (didLayout) {
//View的layout方法
performLayout(lp, mWidth, mHeight);
...
//view的draw方法
performDraw();
} else {
....
}
可以看到view的测量measure,布局layout,绘制draw三大流程都是在performTraversals方法中进行的.
四、Choreographer
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
此时我们来看一下mChoreographer这个对象
4.1 postCallback()
public void postCallback(int callbackType, Runnable action, Object token) {
//此时delayMillis为0
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
//当前时间
final long now = SystemClock.uptimeMillis();
//delayMillis=0
final long dueTime = now + delayMillis;
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);
}
}
}
可以看到postCallbackDelayedInternal()方法会根据当前时间戳将这个Runnable(action)保存到mCallbackQueues队列里.
4.2 scheduleFrameLocked()
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769497)
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//这个变量一直是true
...
if (isRunningOnLooperThreadLocked()) {
//是否是当前线程
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} ....
}
}
}
4.4 scheduleVsyncLocked()
private void scheduleVsyncLocked() {
...
mDisplayEventReceiver.scheduleVsync();
....
}
4.5 scheduleVsync()
DisplayEventReceiver.java
private static native void nativeScheduleVsync(long receiverPtr);
public void scheduleVsync() {
...
nativeScheduleVsync(mReceiverPtr);
...
}
此时已经走到了native层的nativeScheduleVsync方法
小结
到这里,我们知道当一个View发起刷新操作时,会层层通知ViewRootImpl的 scheduleTraversals方法,然后这个方法会将view的测量,布局,绘制三大方法封装成performTraversals()封装到Runnable里.
传给Choreographer,并且将当前的时间戳放进了mCallbackQueues队列里.并且调用了一个native方法.
所以暂时我们还不知道这个runnable何时不执行
五、mCallbackQueues
既然这个方法被封装成Runnable并且放进了mCallbackQueues,那它肯定会被执行,我们先看下这个队列的取操作
private final class CallbackQueue {
private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {...
}
//队列取出
public CallbackRecord extractDueCallbacksLocked(long now) {
...
}
//入队列操作
public void addCallbackLocked(long dueTime, Object action, Object token) {
...
}
//移除操作
public void removeCallbacksLocked(Object action, Object token) {
...
}
}
5.1 extractDueCallbacksLocked()
看下这个extractDueCallbacksLocked()方法是何时被取出的.
void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
...
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
...
}
5.2 doCallbacks()
再来看下doCallbacks是哪里调用的
void doFrame(long frameTimeNanos, int frame,
DisplayEventReceiver.VsyncEventData vsyncEventData) {
...
...
//对应于之前postCallBack的入参
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);
....
}
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
5.3 doFrame()
当doFrame()被调用并且对应的条件是Choreographer.CALLBACK_TRAVERSAL时,就可以执行我们的view绘制方法了,我们来看下到底有几处方法调用了doFrame()
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
private VsyncEventData mLastVsyncEventData = new VsyncEventData();
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, 0);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
// the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
// for the internal display implicitly.
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
try {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW,
"Choreographer#onVsync " + vsyncEventData.id);
}
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
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;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
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);
}
}
@Override
public void run() {
mHavePendingVsync = false;
//在run方法里会从mCallbackQueue中取出消息并按照时间戳顺序调用mTraversalRunnable的run函数.
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
}
这一篇文章我们先不涉及Native的Vsnnc信号是如何回调给Java层.到这里我们知道View的绘制方法是封装在Runnable接收到信号时回调执行的.
六、同步消息屏障
我们知道Android是基于消息机制的,每一个操作都是Message,如果在触发绘制的时候,此时消息队列中还有很多消息还没执行,此时如何保证Vsync信号和绘制的同步,提高这个绘制消息的优先级呢?
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//核心代码
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
再回过头看一下当初发送绘制消息时,ViewRootImpl是如何调用的.
很明显,我们在使用handler时好像没接触过postSyncBarrier();
6.1 postSyncBarrier()
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
可以看到,这个方法只是创建了一个Message,并且像之前的msg enqueueMessage加入消息链表一样,但是这个msg没有设置target.
//普通的handler发送消息 会将msg.target与当前发送消息的handler进行一个绑定
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
这样一个没有和handler进行绑定的Msg到底有什么用呢?
6.2 MessageQueue.next()
Message next() {
...
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//此时遇到msg.target为null的消息
if (msg != null && msg.target == null) {
//do while 循环遍历消息链表
//msg指向离表头最近的一个异步消息,此时跳出循环
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
//返回msg
return msg;
}
} else {
...
}
....
...
}
}
可以看到,当我们设置了同步屏障之后,next函数将会忽略所有的msg.isAsynchronous()同步消息,返回异步消息.
在这个优先级机制下,异步消息的优先级是高于同步消息的.
而我们也可以在之前的postCallBack中看到设置了给绘制任务设置了异步消息的设置
public void postCallback(int callbackType, Runnable action, Object token) {
//此时delayMillis为0
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
//当前时间
final long now = SystemClock.uptimeMillis();
//delayMillis=0
final long dueTime = now + delayMillis;
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);
}
}
}
可以看到,在Android源码中,很多与View绘制相关的类中都调用了这种设置异步消息的方法.
6.3 removeSyncBarrier
我们知道postSyncBarrier设置同步屏障只是为了告诉Looper此时消息队列里有异步消息处理,需要尽快找到异步消息并返回执行.
那当我们找到异步消息并开始执行之后,此时就要拆掉此次的同步屏障,恢复之前的消息执行次序.
可以看到在doTraversal方法中
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//此时拆除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...执行绘制任务
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
七、总结
我们都知道MessageQueue中的消息是以链表的形式按照msg.when的大小依次排列的.而对于有些优先级较高比如一些view绘制刷新动作,android内部采用了同步异步消息以及消息屏障的机制,提高了消息的优先级/
当view需要执行绘制任务,会把这个异步消息封装成异步的message时,先给Looper发送一个同步屏障,当next()方法在取到一个同步屏障的时候,就会遍历整个队列,寻找添加了异步消息标志的消息.一直到找到这个异步消息,此时开始执行这个优先级最高的异步消息,并且拆除同步屏障.
7.1 彩蛋
看到这里,可能有朋友要问了,既然这个消息优先级这么高,为啥不立马把消息放到队列里执行啊?链表是按照时间排序的啊,早点进去就能早点执行,干嘛还费这么大劲给一个后插入的消息,还得提高优先级,早进去不就行了?
7.2 屏幕刷新机制
开头我们只是简单的介绍了一下android的view刷新机制,试想如果同一时刻各种不同view一直在刷新,那系统该如何控制view的绘制频率呢?
所以Android底层会定时的发送屏幕刷新信号,也就是我们熟悉的16.6ms,这是因为手机屏幕的刷新频率是60hz,一秒内刷新60次,也就是每隔16.6ms刷新一次,此时就会回调我们之前走到的view绘制方法调用.
所以当我们告诉系统需要刷新的时候,系统也不是立马展开绘制任务,而是给主线程设置一个同步屏障,告诉Looper此时有优先级较高的消息处理,并且监听屏幕刷新信号,当Vsync信号监听到时,会发送一个异步消息给主线程Handler,此时执行我们需要刷新的绘制任务,并且移除同步屏障.
而正因为这种定时刷新机制也提高了绘制效率
7.3 丢帧
既然定时定频率的刷新,怎么还会丢帧呢?
造成丢帧一般有两种原因
-
View的绘制方法超过了16.6ms,下一帧的屏幕刷新信号已经来了,可是此时绘制方法还没有执行结束
-
主线程一直在处理其他耗时的操作,导致无法读到同步屏障,无法读到这个异步消息.
第一个原因就是我们的布局嵌套或者绘制时间过长,需要我们去优化布局
第二个原因就是我们说的避免在主线程中执行耗时操作.
所以在发送同步信号,以及接受到屏幕刷新信号之前主线程是不是啥都没干,一直在等异步任务进来?--- 答案是的.因为对于App来说,这种绘制的任务优先级最高,所以在收到同步屏障消息之后,为了尽可能保证及时进行View的绘制工作,主线程的确在等待此时优先级很高的绘制任务到来.
至于Android底层的屏幕刷新信号到底是如何回调的,以及更多FrameWork层源码解析,后续都会慢慢更新~