前言

今天继续屏幕刷新机制的知识讲解,上文说到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.28节 《Python类、类型、协议》章节总结

    本章详细介绍了Python协议.多态与"鸭子类型".类.类实例变量.类变量.实例方法.类方法.静态方法.类继承.抽象类.property函数和@property装饰器定义属性访问方 ...

  2. PyQt(Python+Qt)学习随笔:QTreeWidgetItem项中列的复选状态访问方法

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 树型部件QTreeWidget中的QTreeWidgetItem项,项中每列数据都可以单独设置复选状 ...

  3. Mybatis学习03

    title: Mybatis学习03 date: 2020-01-19 13:03:20 tags: Mybatis学习的第二天,内容有日志和分页. <!--more--> 1.日志 1. ...

  4. 极光实验室 第一次考核wp

    第一道题: 上来就让我买flag,用御剑扫目录,发现了这道题有源码index.php.bak!直接下载. <meta charset='UTF-8'> <title>极光实验室 ...

  5. web网络漏洞扫描器编写

    这两天看了很多web漏洞扫描器编写的文章,比如W12scan以及其前身W8scan,还有猪猪侠的自动化攻击背景下的过去.现在与未来,以及网上很多优秀的扫描器和博客,除了之前写了一部分的静湖ABC段扫描 ...

  6. 团队作业part1--团队展示&选题

    一.团队展示 1.队名 DiligentVegetableChicken 2.队员信息 纪昂学(组长):3118005053 廖业成:3118005060 蔡越:3118005086 周梓波:3118 ...

  7. Scrum 冲刺第一天

    一.团队信息 1.团队名称 挑战极限队 2.团队成员 张博愉(3118005074) 张润柏(3118005075) 郑堉涵(3118005077) 周伟建(3118005079) 林梓琦(31180 ...

  8. 【笔记】「pj复习」深搜——拿部分分

    说在最前面 众所周知, NOIP pj 的第三题大部分都是 dp ,但是有可能在考场上想不到动态转移方程,所以我们就可以拿深搜骗分. 方法 深搜拿部分分 剪枝 记忆化 看数据范围 有时候发现,写完深搜 ...

  9. 落谷 P1410 子序列

    题目链接. Discription 给定长度为 \(n\) 的序列 \(A\)(\(n\) 为偶数),判断是否能将其划分为两个长度为 \(\dfrac{N}{2}\) 的严格递增子序列. Soluti ...

  10. Struts2 S2-061(CVE-2020-17530)漏洞复现

    0x00 漏洞简介 Apache Struts2框架是一个用于开发Java EE网络应用程序的Web框架.Apache Struts于2020年12月08日披露 S2-061 Struts 远程代码执 ...