前言

今天继续屏幕刷新机制的知识讲解,上文说到vsync的处理,每一帧UI的绘制前期处理都在Choreographer中实现,那么今天就来看看这个神奇的舞蹈编舞师是怎么将UI变化反应到屏幕上的。

代码未动,图先行

UI变化

上期说到app并不是每一个vsync信号都能接收到的,只有当应用有绘制需求的时候,才会通过scheduledVsync 方法申请VSYNC信号。

那我们就从有绘制需求开始看,当我们修改了UI后,都会执行invalidate方法进行绘制,这里我们举例setText方法,再回顾下修改UI时候的流程:

可以看到,最后会调用到父布局ViewRootImplscheduleTraversals方法。

    public ViewRootImpl(Context context, Display display) {
//...
mChoreographer = Choreographer.getInstance();
} void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}

为了方便查看,我只留了相关代码。可以看到,在ViewRootImpl构造方法中,实例化了Choreographer对象,并且在发现UI变化的时候调用的scheduleTraversals方法中,调用了postSyncBarrier方法插入了同步屏障,然后调用了postCallback方法,并且传入了一个mTraversalRunnable(后面有用处,先留意一下),暂时还不知道这个方法是干嘛的。继续看看。

Choreographer实例化

//Choreographer.java

    public static Choreographer getInstance() {
return sThreadInstance.get();
} private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
//...
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
//...
return choreographer;
}
}; private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper); //初始化FrameDisplayEventReceiver
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE; //一帧间隔时间
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
} }

ThreadLocal,是不是有点熟悉?之前说Handler的时候说过,Handler是怎么获取当前线程的Looper的?就是通过这个ThreadLocal,同样,这里也是用到ThreadLocal来保证每个线程对应一个Choreographer

存储方法还是一样,以ThreadLocal为key,Choreographer为value存储到ThreadLocalMap中,不熟悉的朋友可以再翻到《Handler另类难点三问》看看。

所以这里创建的mHandler就是ViewRootImpl所处的线程的handler。接着看postCallback做了什么。

postCallback

    private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
} synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
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);
}
}
} private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
} @Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
} void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

ViewRootImpl中调用了postCallback方法之后,可以看到通过addCallbackLocked方法,添加了一条CallbackRecord数据,其中action就是对应之前ViewRootImplmTraversalRunnable

然后判断设定的时间是否在当前时间之后,也就是有没有延迟,如果有延迟就发送延迟消息消息MSG_DO_SCHEDULE_CALLBACK到Handler所在线程,并最终执行到scheduleFrameLocked方法。如果没有延迟,则直接执行scheduleFrameLocked

scheduleFrameLocked(准备申请VSYNC信号)

    private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//是否运行在主线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
//通过Handler切换线程
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else { //计算下一帧的时间
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
} case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break; void doScheduleVsync() {
synchronized (mLock) {
if (mFrameScheduled) {
scheduleVsyncLocked();
}
}
}

该方法中,首先判断了是否开启了VSYNC(上节说过Android4.1之后默认开启VSYNC),如果开启了,判断在不在主线程,如果是主线程就运行scheduleVsyncLocked,如果不在就切换线程,也会调用到scheduleVsyncLocked方法,而这个方法就是我们之前说过的申请VSYNC信号的方法了。

如果没有开启VSYNC,则直接调用doFrame方法。

另外可以看到,刚才我们用到Handler发送消息的时候,都调用了msg.setAsynchronous(true)方法,这个方法就是设置消息为异步消息。因为我们刚才一开始的时候设置了同步屏障,所以异步消息就会先执行,这里的设置异步也就是为了让消息第一时间执行而不受其他Handler消息影响。

小结1

通过上面一系列方法,我们能得到一个初步的逻辑过程了:

  • ViewRootImpl初始化的时候,会实例化Choreographer对象,也就是获取当前线程(一般就是主线程)对应的Choreographer对象。
  • Choreographer初始化的时候,会新建一个当前线程对应的Handler对象,初始化FrameDisplayEventReceiver,计算一帧的时间等一系列初始化工作。
  • 当UI改变的时候,会调用到ViewRootImplscheduleTraversals方法,这个方法中加入了同步屏障消息,并且调用了Choreographer的postCallback方法去申请VSYNC信号。

在这个过程中,Handler发送了延迟消息,切换了线程,并且给消息都设置了异步,保证最先执行。

继续看scheduleVsyncLocked方法。

scheduleVsyncLocked

    private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
} public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}

代码很简单,就是通过FrameDisplayEventReceiver,请求native层面的垂直同步信号VSYNC。

这个FrameDisplayEventReceiver是在Choreographer构造方法中实例化的,继承自DisplayEventReceiver,主要就是处理VSYNC信号的申请和接收。

刚才说到调用nativeScheduleVsync方法申请VSYNC信号,然后当收到VSYNC信号的时候就会回调onVsync方法了。

onVsync(接收VSYNC信号)

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable { @Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) { //...
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
} @Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

这里同样通过Handler发送了一条消息,执行了本身的Runnable回调方法,也就是doFrame()

doFrame(绘制帧数据)

    void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
//... //当前帧的vsync信号来的时间,假如为12分200ms
long intendedFrameTimeNanos = frameTimeNanos;
//当前时间,也就是开始绘制的时间,假如为12分150ms
startNanos = System.nanoTime();
//计算时间差,如果大于一个帧时间,则是跳帧了。比如是50ms,大于16ms
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//计算掉了几帧,50/16=3帧
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//计算一帧内时间差,50%16=2ms
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//修正时间,vsync信号应该来得时间,为12分148ms,保证和绘制时间对应上
frameTimeNanos = startNanos - lastFrameOffset;
} if (frameTimeNanos < mLastFrameTimeNanos) {
//信号时间已过,不能再绘制了,等待下一个vsync信号,保证后续时间同步上
scheduleVsyncLocked();
return;
} mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
} try { //执行相关的callback任务
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
} }

这里主要的工作就是:

  • 设置当前帧的开始绘制时间,上节说过开始绘制要在vsync信号来的时候开始,保证两者时间对应。所以如果时间没对上,就是发送了跳帧,那么就要修正这个时间,保证后续的时间对应上。
  • 执行所有的Callback任务。

doCallbacks(执行任务)

    void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) { final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true; if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
+ mFrameIntervalNanos; frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
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);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
} private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token; @UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

其实就是按类型,从mCallbackQueues队列中取任务,并执行对应的CallbackRecord的run方法。

而这个run方法中,判断了token,并执行了action的对应方法。再回头看看我们当初ViewRootImpl传入的方法:

mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

token为空,所以会执行到action也就是mTraversalRunnable的run方法。

所以兜兜转转,又回到了ViewRootImpl本身,通过Choreographer申请了VSYNC信号,然后接收了VSYNC信号,最终回到自己这里,开始view的绘制。

最后看看mTraversalRunnable的run方法。

mTraversalRunnable

    final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
} void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
} performTraversals(); if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

这就很熟悉了吧,调用了performTraversals方法,也就是开始了测量,布局,绘制的步骤。同时,关闭了同步屏障。

总结

最后再看看总结图:

参考

https://juejin.cn/post/6863756420380196877

https://blog.csdn.net/stven_king/article/details/78775166

拜拜

有一起学习的小伙伴可以关注下️ 我的公众号——码上积木,每天剖析一个知识点,我们一起积累知识。公众号回复111可获得面试题《思考与解答》以往期刊。

Choreographer全解析的更多相关文章

  1. 【凯子哥带你学Framework】Activity界面显示全解析

    前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分 ...

  2. 【凯子哥带你学Framework】Activity界面显示全解析(下)

    咱们接着上篇继续讲,上篇没看的请戳:[凯子哥带你学Framework]Activity界面显示全解析(上) 如何验证上一个问题 首先,说明一下运行条件: //主题 name="AppThem ...

  3. 【转载】【凯子哥带你学Framework】Activity界面显示全解析(下)

    如何验证上一个问题 首先,说明一下运行条件 //主题 name="AppTheme" parent="@android:style/Theme.Holo.Light.No ...

  4. Google Maps地图投影全解析(3):WKT形式表示

    update20090601:EPSG对该投影的编号设定为EPSG:3857,对应的WKT也发生了变化,下文不再修改,相对来说格式都是那样,可以到http://www.epsg-registry.or ...

  5. C#系统缓存全解析(转载)

    C#系统缓存全解析 对各种缓存的应用场景和方法做了很详尽的解读,这里推荐一下 转载地址:http://blog.csdn.net/wyxhd2008/article/details/8076105

  6. iOS Storyboard全解析

    来源:http://iaiai.iteye.com/blog/1493956 Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果, ...

  7. 【转载】Fragment 全解析(1):那些年踩过的坑

    http://www.jianshu.com/p/d9143a92ad94 Fragment系列文章:1.Fragment全解析系列(一):那些年踩过的坑2.Fragment全解析系列(二):正确的使 ...

  8. (转)ASP.NET缓存全解析6:数据库缓存依赖

    ASP.NET缓存全解析文章索引 ASP.NET缓存全解析1:缓存的概述 ASP.NET缓存全解析2:页面输出缓存 ASP.NET缓存全解析3:页面局部缓存 ASP.NET缓存全解析4:应用程序数据缓 ...

  9. jQuery&nbsp;Ajax&nbsp;实例&nbsp;全解析

    jQuery Ajax 实例 全解析 jQuery确实是一个挺好的轻量级的JS框架,能帮助我们快速的开发JS应用,并在一定程度上改变了我们写JavaScript代码的习惯. 废话少说,直接进入正题,我 ...

随机推荐

  1. 第7.12节 可共享的Python类变量

    第7.12节 可共享的Python类变量 一.    引言 在上节已经引入介绍了类变量和实例变量,类体中定义的变量为类变量,默认属于类本身,实例变量是实例方法中定义的self对象的变量,对于每个实例都 ...

  2. BJOI2015 隐身术

    落谷. Description 给你两个串 \(A.B\).询问 \(B\) 中有多少个非空子串和 \(A\) 的编辑距离不超过 \(K\). Solution 发现 \(K \le 5\),考虑可以 ...

  3. 题解-洛谷P4139 上帝与集合的正确用法

    上帝与集合的正确用法 \(T\) 组数据,每次给定 \(p\),求 \[\left(2^{\left(2^{\left(2^{\cdots}\right)}\right)}\right)\bmod p ...

  4. 自己编写k8s

    ## 基于Docker和Kubernetes的企业级DevOps实践训练营 ### 课程准备 1. 离线镜像包 百度:https://pan.baidu.com/s/1N1AYGCYftYGn6L0Q ...

  5. js实现弹幕

    弹幕是一个很常见的功能,下面是本人封装的一个小小的实现方案,存在不足之处可以提出来或自由改进. 直接上代码:复制可运行 <!DOCTYPE html> <html> <h ...

  6. sqli-labs less11-12(post型union注入)

    less-11 post型union注入 过程: 输入admin admin 判断username password的闭合方式 对username查字段个数 ' union select 1,2# ' ...

  7. chrome 开发者工具使用一例

    今天搜到了一篇我想看的文章,某网站上又是弹出注册小窗遮挡,又是一堆漂浮广告,还把字体搞成灰色. 右键审查元素,找到几个div,删掉:原来那个字体的灰色,是个什么script做的遮罩,也删掉. 然后整个 ...

  8. Spark-2-性能监控方式

    1 Spark Web UI Spark提供了一些基本的Web监控页面,对于日常监控十分有用. 通过http://master:4040(默认端口是4040,可以通过spark.ui.port修改)我 ...

  9. ASP.NET Core 中间件的使用(二):依赖注入的使用

    写在前面 上一篇大家已经粗略接触了解到.NET Core中间件的使用:ASP .Net Core 中间件的使用(一):搭建静态文件服务器/访问指定文件, .NET Core框架中很多核心对象都是通过依 ...

  10. numpy的好处

    python是很慢的,因为python在执行代码的时候会执行很多复杂的check功能,比如 b=1; a=b/0.5 这个运算看起来很简单,但是在计算机的内部.b要先从一个整数integer转化成一个 ...