How Android’s Choreographer Orchestrates Vsync for Smooth UI Rendering
This article explains Android’s display pipeline—CPU, GPU, and Display—and dives deep into how the Choreographer registers callbacks, schedules traversals, processes Vsync signals, and coordinates rendering to achieve fluid UI updates.
Android Display Pipeline Overview
The Android display system consists of three cooperating components: the CPU computes per‑frame UI data, the GPU renders that data into a framebuffer, and the Display hardware refreshes the screen from the framebuffer.
Choreographer as the Frame Scheduler
Choreographer receives Vsync signals from the display and coordinates input, animation, and traversal callbacks so that CPU work and GPU rendering occur in the same Vsync window, preventing unnecessary redraws.
Registering a Traversal Callback
When an activity resumes, ActivityThread.handleResumeActivity eventually calls ViewRootImpl.setView, which invokes scheduleTraversals(). This method posts a traversal runnable to the thread‑local Choreographer.
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// Insert a sync barrier to block normal messages until Vsync arrives
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// Register the traversal callback with Choreographer
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}Choreographer Construction
Each Looper thread owns a single Choreographer instance. Its constructor creates a FrameDisplayEventReceiver that listens for Vsync events.
public final class Choreographer {
private final FrameDisplayEventReceiver mDisplayEventReceiver;
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ?
new FrameDisplayEventReceiver(looper, vsyncSource) : null;
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
}Callback Types
Choreographer defines five ordered callback categories. They are stored in per‑type linked lists ( CallbackQueue) and executed sequentially after each Vsync.
private static final String[] CALLBACK_TRACE_TITLES = {
"input", "animation", "insets_animation", "traversal", "commit"
};
public static final int CALLBACK_INPUT = 0;
public static final int CALLBACK_ANIMATION = 1;
public static final int CALLBACK_INSETS_ANIMATION = 2;
public static final int CALLBACK_TRAVERSAL = 3;
public static final int CALLBACK_COMMIT = 4;TraversalRunnable and doTraversal
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// Remove the sync barrier so normal messages can resume
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// Perform the classic measure‑layout‑draw phases
performTraversals();
}
}Vsync Dispatch Flow
When the display generates a Vsync, the native method nativeScheduleVsync() registers the request with SurfaceFlinger. SurfaceFlinger eventually calls DisplayEventDispatcher.handleEvent(), which forwards the event to NativeDisplayEventReceiver.dispatchVsync(). This creates a Java Message that is posted to the main thread’s handler.
static void nativeScheduleVsync(JNIEnv* env, jclass clazz, jlong receiverPtr) {
sp<NativeDisplayEventReceiver> receiver =
reinterpret_cast<NativeDisplayEventReceiver*>(receiverPtr);
receiver->scheduleVsync();
}The FrameDisplayEventReceiver (a subclass of DisplayEventReceiver) receives the Vsync in onVsync(), builds an asynchronous message, and sends it to its handler.
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
// Convert nanoseconds to milliseconds for the Looper timing API
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}When the Looper processes this message, run() invokes doFrame(), which executes callbacks in the fixed order: INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT.
void doFrame(long frameTimeNanos, int frame) {
doCallbacks(CALLBACK_INPUT, frameTimeNanos);
doCallbacks(CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(CALLBACK_COMMIT, frameTimeNanos);
}Callback Execution Details
Each callback list is traversed; every CallbackRecord runs either a Runnable or a FrameCallback.
private static final class CallbackRecord {
CallbackRecord next;
long dueTime;
Object action; // Runnable or FrameCallback
Object token;
void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback) action).doFrame(frameTimeNanos);
} else {
((Runnable) action).run();
}
}
}Scheduling the Next Vsync
When a callback is posted, postCallback() eventually calls scheduleFrameLocked(). If no frame is currently scheduled, it marks mFrameScheduled and asks the FrameDisplayEventReceiver to request the next Vsync.
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
}
}
}
}
private void scheduleVsyncLocked() {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
mDisplayEventReceiver.scheduleVsync();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
public void scheduleVsync() {
nativeScheduleVsync(mReceiverPtr);
}Sync Barrier and Asynchronous Messages
scheduleTraversals()inserts a sync barrier into the Looper queue. Because the Vsync‑driven message is marked asynchronous ( msg.setAsynchronous(true)), it bypasses the barrier and is processed immediately, ensuring that performTraversals() runs as soon as the Vsync arrives.
Key Takeaways
Choreographer is created per Looper and holds a FrameDisplayEventReceiver that registers for Vsync.
Activity resume ultimately calls scheduleTraversals(), which posts a CALLBACK_TRAVERSAL runnable and inserts a sync barrier.
The barrier blocks normal messages; the Vsync‑driven asynchronous message jumps the barrier, invoking doTraversal() and the full measure‑layout‑draw cycle.
Callbacks are categorized (input, animation, insets‑animation, traversal, commit) and executed in a deterministic order on each Vsync.
Native‑to‑Java plumbing (
nativeScheduleVsync → DisplayEventDispatcher → NativeDisplayEventReceiver → FrameDisplayEventReceiver) guarantees that UI work is tightly coupled to the display’s refresh rhythm, yielding smooth, jitter‑free rendering.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coolpad Technology Team
Committed to advancing technology and supporting innovators. The Coolpad Technology Team regularly shares forward‑looking insights, product updates, and tech news. Tech experts are welcome to join; everyone is invited to follow us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
