Android 屏幕刷新机制

本文部分图文参考自:juejin.cn/post/686375...

在开始讲解屏幕刷机制前,先回顾一下 View 的绘制流程,读过前面的文章 从点击桌面图标到应用界面展示 应该都有印象,View 绘制流程开启的地方就是 ViewRootImplscheduleTraversals

java 复制代码
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
						// 添加同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 加入一个 callback 到 ****Choreographer****
						mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

可以看到 scheduleTraversals() 主要做了两件事:

  1. 往当前线程的 Loop 加入同步屏障,关于同步屏障详见 [Android 消息机制](../Android 消息机制)
  2. 封装了一个 mTraversalRunnable 加入到 mChoreographer
java 复制代码
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
						// 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
						
						// 可以看到这里出现了熟悉的 performTraversals,这里面封装了 onMeasure、onLayout、onDraw
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

可见,TraversalRunnable 就是一个 Runnable,里面做了两件事情:

  1. 移除同步屏障
  2. 开始真正的绘制流程。

那 TraversalRunnable 是什么时候被执行呢,这就引出本文的主角: Choreographer

Choreographer

Choreographer 翻译为编舞者,负责从显示系统接收脉冲信号,编排渲染下一帧的绘制工作,负责获取 Vsync 同步信号并控制 UI 线程完成图像绘制的类。

怎么理解这一句话呢? 一次完整的绘制,是需要 CPU、GPU 和显示设备的配合,但是三者是一个并行运作的状态,那怎么相互协调呢?所以引进了 VSync 信号机制:每 16ms,硬件(或者软件)会发出一个 VSync 信号,CPU 接收到这个信号,开始了一次绘制流程。再下一次 VSync 信号到来之时,Display 就可以直接显示第一帧,CPU 也开始绘制第二帧。就这样循环下去。

也就是说 CPU 和 GPU 必须要在这一次的 VSync 信号发生和下一次 VSync 信号到来之前要准备好这一帧的数据,否则就发生了掉帧的现象了。

可以看下图,大致可以了解 VSync 信号机制:

Choreographer怎么跟Vsync信号机制相挂钩呢?

java 复制代码
// ViewRootImpl
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

// Choreographer
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}
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);
		
		// 这里的 delayMills 为 0,所以会走到 scheduleFrameLocked()
        if (dueTime <= now) { 
            scheduleFrameLocked(now);
        }
        ...
    }
}
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        ...
        // 判断当前 thread 是否运行loop,这个后续会讲到
        if (isRunningOnLooperThreadLocked()) {
            scheduleVsyncLocked();
        }
   }
}
private void scheduleVsyncLocked() {
    // 注册订阅了 vsync 信号
    mDisplayEventReceiver.scheduleVsync();
}
// DisplayEventReceiver
public void scheduleVsync() {
    ...
    // 注册订阅了vsync信号
    nativeScheduleVsync(mReceiverPtr);
}
private static native void nativeScheduleVsync(long receiverPtr);

可以看到主要是两个动作:

  1. mCallbackQueues 加入了Runnable
  2. 从 jni 层注册了 Vsync 信号

那什么时候会收到 Vsync 信号回来呢?

DisplayEventReceiverdisjpatchVsync() 方法可以看到,该方法是 jni 层调用的

java 复制代码
// DisplayEventReceiver,由 jni 调用
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
    onVsync(timestampNanos, builtInDisplayId, frame);
}

// Choreographer 内部类 DisplayEventReceiver,重写了 onVsync 方法
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
	mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    // 设置成异步消息
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame);
}

这里可以看到,其实 mHandle r就是当前主线程的 handler,当接收到 onVsync 信号的时候,将自己封装到 Message中,等待 Looper 处理,最后 Looper 处理消息的时候就会调用 run 方法,这里是 Handler 的机制,不做解释,详见 Android 消息机制

run 方法中,调用了 doFrame() 方法:

java 复制代码
// Choreographer
void doFrame(long frameTimeNanos, int frame) {
	...
	doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    // 从 mCallbackQueues 取出
    callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
    for (CallbackRecord c = callbacks; c != null; c = c.next) {
         c.run(frameTimeNanos);
    }
}
// CallbackRecord
public void run(long frameTimeNanos) {
    if (token == FRAME_CALLBACK_TOKEN) {
				// 通过 postFrameCallback 或 postFrameCallbackDelayed,会执行这里
        ((FrameCallback)action).doFrame(frameTimeNanos);
    } else {
        // 这里也即是调用了 TraservalRunnable 的 run 方法,也即是三个绘制流程
        ((Runnable)action).run();
    }
}

可见当 vsync 信号来临的时候,主要做了两件事情

  1. 从 mCallbackQueues 取出 callback
  2. 执行 callback,这里最后会执行到 TraservalRunnable 的三大绘制流程

这里是不是说,Choreographer 是不是只要注册一次以后,都可以收到 Vsync 信号呢?

其实不是的,Vsync 信号需要每次都去注册,而且只能接收到一次。这样能保证在不需要重新绘制的情况下,Choreographer 也就不需要接收信号,就不会去注册 Vsync 信号。

  • 一次 requestLayout 只会注册一次并且收到一次 VSYNC
  • 动画会在 doFrame 中请求下一次 VSYNC

Choreographer.CallbackQueue

  1. 这是一个单链表,入队的时候,根据执行时间从小到大排列
java 复制代码
public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            // 如果表头是空的,就把传入的任务当成表头
						if (entry == null) {
                mHead = callback;
                return;
            }
						// 如果传入的任务时间小于表头任务,就把任务插在前面,新任务成为表头
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
						// 否则遍历链表,把新任务按照时间从小到大插入链表
            while (entry.next != null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }
  1. 当 doFrame() 的时候,执行 extractDueCallbacksLocked 把所有当前时间需要被执行的任务都取出来,构成一个新的链表

    java 复制代码
    public CallbackRecord extractDueCallbacksLocked(long now) {
                CallbackRecord callbacks = mHead;
                // 如果表头为空,或者时间没有到需要执行的时间,直接返回空链表
    						if (callbacks == null || callbacks.dueTime > now) {
                    return null;
                }
    					
                CallbackRecord last = callbacks;
                CallbackRecord next = last.next;
                // 遍历链表,找到所有当前时间可被执行的任务,由于链表已经是从小到大排序
    						// 所以只需要两个指针,一个指向表头,一个指向时间大于当前时间的结点就可以了
    						while (next != null) {
                    if (next.dueTime > now) {
                        // 找到了新链表的结束位置
    										last.next = null;
                        break;
                    }
                    last = next;
                    next = next.next;
                }
    						// 新表头指向截断的位置,截断位置之前的所有结点构成的链表被返回
                mHead = next;
    						// 返回新链表表头位置
                return callbacks;
            }

总结

  1. ViewRootImpl → ViewRootImpl.scheduleTraversals() 添加同步屏障,添加一个任务到 Choreographer 内的队列,这个任务里面调用 performTraversals
  2. Choreographer 的队列是一个单链表
  3. Choreographer → Choreographer 添加任务到队列,通过 FrameDisplayEventReceiver 向 JNI 注册 VSYNC 信号,当 VSYNC 到来时,JNI 调用 onVsync(),
  4. Choreographer. FrameDisplayEventReceiver onVsync() 通过 Handler 发送异步消息到主线程(这个线程是初始化 Choreographer 的线程,可以是主线程也可以是其他线程,每个线程都可以有一个 Choreographer 对象)
  5. Choreographer. FrameDisplayEventReceiver 主线程处理 doFrame()
  6. Choreographer → 取出所有这个时间内可以被执行的任务(是一个链表),这个任务里面调用 performTraversals,然后移除同步屏障
  7. 至此一次绘制流程结束

Choreographer 初始化的地方

在 ViewRootImpl 的构造方法里面

java 复制代码
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        //...
        mChoreographer = useSfChoreographer
                ? Choreographer.getSfInstance() : Choreographer.getInstance();
        // ...
    }
private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

public static Choreographer getInstance() {
        return sThreadInstance.get(); // ThreadLocal.get 方法会回调 initialValue() 来创建实例
    }

Choreographer 是线程内单例的

Choreographer 的 5 种 callback 类型

java 复制代码
void doFrame(long frameTimeNanos, int frame,
            DisplayEventReceiver.VsyncEventData vsyncEventData) {
						// ...
						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);
}

可以看到五种 callback 类型的优先级是

  1. CALLBACK_INPUT:用于处理输入事件,例如触摸事件、按键事件等。当输入事件发生时,Choreographer会在每个VSYNC信号周期内通过CALLBACK_INPUT回调来处理这些事件,例如分发触摸事件给相应的视图、处理滑动手势等。通常情况下,开发者无需直接操作CALLBACK_INPUT回调,因为Android的输入框架会自动处理这些任务
  2. CALLBACK_ANIMATION:处理动画,外部通过 postFrameCallback 都是这个类型,动画也是用这个类型
  3. CALLBACK_INSETS_ANIMATION:用于处理窗口插入物(如导航栏、状态栏等)的动画。在Android 11(API级别30)及更高版本中,Choreographer提供了CALLBACK_INSETS_ANIMATION回调来同步窗口插入物的动画。开发者可以使用这个回调来实现自定义的窗口插入物动画,以便在每个VSYNC信号周期内更新动画的状态。通常情况下,开发者无需直接操作CALLBACK_INSETS_ANIMATION回调,因为Android的窗口插入物框架会自动处理这些任务
  4. CALLBACK_TRAVERSAL,处理绘制,measure、layout、draw
  5. CALLBACK_COMMIT

CALLBACK_INPUT 、CALLBACK_ANIMATION 会修改 view 的属性,所以要先于 CALLBACK_TRAVERSAL 执行

Choreographer 监控帧率的原理

前面提到过

  1. 每一帧会调用 android.view.Choreographer#doFrame() 方法来处理逻辑

  2. 从 Choreographer 的队列数组 mCallbackQueues 里面取出 Choreographer.*CALLBACK_TRAVE*RSAL 对应的队列(是一个单向链表)

  3. 然后找到所有当前时间能执行的任务 CallbackRecord,执行 run 方法

    java 复制代码
    private static final class CallbackRecord {
            public CallbackRecord next;
            public long dueTime;
            public Object action; // Runnable or FrameCallback
            public Object token;
    ​
            @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
            public void run(long frameTimeNanos) {
                if (token == FRAME_CALLBACK_TOKEN) {
                    ((FrameCallback)action).doFrame(frameTimeNanos);
                } else {
                    ((Runnable)action).run();
                }
            }
        }

    requestLayout() 或者 invalidate() 触发的流程,token 是空的

    scss 复制代码
    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // 这里 token 是空的
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }

    所以,CallbackRecord 的 run 方法会执行到 action.run(), 也就是后面会走到 android.view.ViewRootImpl#performTraversals()

    但是如果是通过 android.view.Choreographer#postFrameCallback 触发的,token 传的是 FRAME_CALLBACK_TOKEN

    typescript 复制代码
    public void postFrameCallback(FrameCallback callback) {
            postFrameCallbackDelayed(callback, 0);
        }
    ​
       
        public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
            if (callback == null) {
                throw new IllegalArgumentException("callback must not be null");
            }
    ​
            // 这里 token 是 FRAME_CALLBACK_TOKEN,注意这了的 callbackType 是 CALLBACK_ANIMATION
            postCallbackDelayedInternal(CALLBACK_ANIMATION,
                    callback, FRAME_CALLBACK_TOKEN, delayMillis);
        }

    所以,CallbackRecord 的 run 方法会执行到 ((FrameCallback)action).doFrame(frameTimeNanos); 也就是执行 postFrameCallback 传入的 callback 的 doFrame 方法;因此也可以用来监听帧率

示例代码

java 复制代码
// Application.java
 public void onCreate() {
     super.onCreate();
     //在Application中使用postFrameCallback
     Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
 }
​
​
  public class FPSFrameCallback implements Choreographer.FrameCallback {
​
    private static final String TAG = "FPS_TEST";
    private long mLastFrameTimeNanos = 0;
    private long mFrameIntervalNanos;
​
    public FPSFrameCallback(long lastFrameTimeNanos) {
        mLastFrameTimeNanos = lastFrameTimeNanos;
        mFrameIntervalNanos = (long)(1000000000 / 60.0);
    }
​
    // 每一帧会回调
    @Override
    public void doFrame(long frameTimeNanos) {
​
        //初始化时间
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = frameTimeNanos;
        }
        // 计算上一帧到当前帧的时间差
        final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
        // 如果大于正常刷新率,则表示掉帧
        if (jitterNanos >= mFrameIntervalNanos) {
            // 计算掉帧数 = 两次刷新的时间差 / 正常没帧的间隔
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if(skippedFrames>30){
              //丢帧30以上打印日志
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
        }
        mLastFrameTimeNanos=frameTimeNanos;
        //注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
}
相关推荐
Yang-Never1 天前
Kotlin协程 -> Job.join() 完整流程图与核心源码分析
android·开发语言·kotlin·android studio
资深前端之路1 天前
react 面试题 react 有什么特点?
前端·react.js·面试·前端框架
拉不动的猪1 天前
回顾vue中的Props与Attrs
前端·javascript·面试
一笑的小酒馆1 天前
Android性能优化之截屏时黑屏卡顿问题
android
boonya1 天前
Redis核心原理与面试问题解析
数据库·redis·面试
懒人村杂货铺1 天前
Android BLE 扫描完整实战
android
在未来等你1 天前
Kafka面试精讲 Day 8:日志清理与数据保留策略
大数据·分布式·面试·kafka·消息队列
沐怡旸1 天前
【算法--链表】114.二叉树展开为链表--通俗讲解
算法·面试
白水清风1 天前
关于Js和Ts中类(class)的知识
前端·javascript·面试