从 onResume 到像素上屏:用日志逐行走读 Android 显示全流程
网上讲 Android 渲染原理的文章很多,但大多数停在"概念图 + 源码跳转"层面------你看完知道有 SurfaceFlinger,知道有 vsync,却始终回答不了一个问题:我写完
onResume()之后,屏幕上的像素到底是怎么一步步出现的?中间每个环节发生在哪个进程、哪一行代码、哪个时刻?本文换一种学法:先讲清前置知识,然后直接在 AOSP 三个进程的关键函数里埋日志(26 个点位),冷启动一个真实 App,把 logcat 的时间轴拿出来,一行一行对着源码讲。
环境说明:Android 14 车机平台(分屏多窗口),测试 App 为
com.hmi.bev.carapp/.MainActivity,屏幕 2560x1440。文中所有日志均为真实抓取,所有代码行号均可在对应文件中对照。
一、前置知识
不讲清这些概念,后面的日志一行都读不懂。每一节都尽量用一句话 + 一张图说透。
1.1 参与显示的五个角色
Android 显示不是"App 画到屏幕上"这么简单,至少五个角色分处四个进程:
| 角色 | 所在进程 | 职责 | 本文日志中的 pid |
|---|---|---|---|
| App(UI线程/RenderThread) | App 进程 | 构建 UI、录制绘制命令、驱动 GPU 渲染、提交 buffer | 5689 |
| WMS(WindowManagerService) | system_server | 窗口的"户籍管理":登记窗口、分配尺寸/Z序、编排窗口动画 | 1215 |
| AMS(ActivityManagerService) | system_server | Activity 生命周期调度,负责喊 App 去 onResume | 1215 |
| SurfaceFlinger(SF) | 独立进程 | 把所有窗口的 layer 合成成一帧,交给显示硬件 | 308 |
| HWC(Hardware Composer) | vendor 进程 | 显示硬件抽象层:高效合成(overlay)、送显(page flip)、上报硬件 vsync | --- |
| GPU | 硬件 | 光栅化、合成执行者 | --- |
一个关键认知:App 永远画不到屏幕上,App 只能画到自己的一块内存(buffer)里。 屏幕上某一时刻的内容,是 SF 把所有窗口的 buffer 按 Z 序叠出来的。这就是"合成"的含义------就像 Photoshop 的图层。
1.2 屏幕是怎么"亮"的:扫描、VBI、VSync
显示器(无论 LCD/OLED)刷新是逐行扫描 :从左上角第一个像素开始,一行一行写到右下角,写完一帧回到起点。写完一帧、准备写下一帧之间的空隙叫 VBI(Vertical Blanking Interval,垂直消隐期) 。
由此引出三个基础问题:
① 画面撕裂 :如果扫描到一半时 App 把 FrameBuffer 改了,上半屏是旧帧、下半屏是新帧------撕裂。解法是双缓冲:App 画到"后缓冲区",扫描读"前缓冲区",只在 VBI 里交换两块缓冲区(page flip)。
② Jank(掉帧) :双缓冲解决了撕裂,但 App 不知道何时该开始画。如果画得太晚,VBI 到了后缓冲区还没好,只能继续显示旧帧------连续两帧显示同一画面就是 jank。解法是 VSync 同步:硬件在每个 VBI 发出一个信号(VSync),通知上游"该开始画下一帧了"。
③ 流水线阻塞 :双缓冲下,如果 GPU 画得慢,CPU 想画下一帧时唯一的空闲 buffer 都没有,只能干等。解法是三缓冲(多缓冲) :多一块 buffer 让 CPU/GPU/显示器三者流水线并行。
Android 的答案就是把这三层方案打包:VSync 信号 + BufferQueue(默认 3 块 GraphicBuffer) 。
1.3 VSync 不是广播,是"请求-响应";而且分两路
这是初学者最容易误解的两点:
第一,vsync 是 App 主动请求的。 Choreographer 不会被动收到每秒钟 60 个信号------只有当 App 有绘制需求(你调了 requestLayout()/invalidate())时,它才向 SF 的 EventThread 请求"下一个 vsync 叫我"。没人画图的进程就静静睡着,省电。
第二,vsync 分 App 相位和 SF 相位两路。 如果 App 和 SF 在同一个信号时刻惊醒,SF 合成时 App 的帧八成还没画完。所以 SF 内部会把硬件 vsync 建模后错开分发:
arduino
vsync-app ──────► App 开始画第 N 帧
(App 画...约一个周期)
vsync-sf ──────► SF 醒来,latch 第 N 帧、合成、送显
本文日志里 FF17 onVsync(App 收到)和 FF42 commit(SF 醒来)会以约 16.7ms 的节奏交错出现,就是这个双相位机制在呼吸。
1.4 GPU 渲染管线:为什么"画"要分两步
CPU 擅长逻辑控制,GPU 擅长海量并行计算。把 200 万个像素算出颜色是典型的"无逻辑、纯计算",所以交给 GPU。一帧从数据到像素经历四个阶段:
arduino
①Application CPU:准备好"图元"(三角形/矩形/文字)和它们的顶点、颜色、纹理
②Geometry GPU:顶点着色器变换坐标 → 装配出三角形
③Rasterization GPU:算出三角形覆盖了哪些像素
④Pixel GPU:片段着色器给每个像素上色 → 测试混合 → 写入 buffer
重要推论:CPU 发出的 drawRect() 只是"命令",像素是 GPU 稍后算出来的。 CPU 提交命令后可以继续干别的(异步),GPU 算完了用 fence(栅栏) 通知下游"这块 buffer 可以用了"。后面会看到 SF latch buffer 前要先等 fence,就是在这里衔接的。
1.5 Skia / OpenGL ES / EGL:三个名词各站在哪
- OpenGL ES:GPU 的通用驱动接口。你的代码不直接碰 GPU,通过它发命令。
- Skia :2D 绘图库,Android UI 的画笔。它底层可以走 CPU(软件绘制,画到 Bitmap),也可以走 OpenGL ES/Vulkan(硬件加速)。你在
onDraw()里写的canvas.drawRect()最终进的是 Skia。 - EGL :OpenGL ES 与"窗口系统"之间的胶水。它负责三件事:
eglCreateWindowSurface(把 GL 的输出绑定到一块可显示的 buffer 上)、管理 GL 上下文(eglCreateContext)、eglSwapBuffers(一帧画完,提交)。后面 RenderThread 初始化时会遇到它。
1.6 HWC:最后一棒
SF 合成完(或决定不合)后要交给显示硬件。理论上 SF 可以用 GPU 把所有 layer 叠成一张图再给显示器,但现代显示控制器(DPU)自带多个 overlay plane,能硬件层
面叠加多张图------零 GPU 开销、零内存拷贝。HWC 就是这个能力的 HAL 封装:
vbnet
SF: "这几层你能直接叠吗?"(prepare)
HWC: "状态栏和导航栏我叠(DEVICE),中间那层有半透明混合,你自己用GPU合(CLIENT)"
SF: 用 RenderEngine 把 CLIENT 层合成一张 framebuffer
SF: "拿去"(presentDisplay)
HWC: 驱动 DPU 在下个 VBI page flip → 面板扫出新帧 👁
1.7 核心对象关系图
arduino
【App 进程】 【system_server】 【SurfaceFlinger】
Activity WindowState ←─ 窗口的"户口档案"
└ PhoneWindow └ SurfaceControl ────┐
└ DecorView │ binder 句柄
└ ViewRootImpl ── 一个窗口的显示总管家 ──────────────────────────┐ │
├ Choreographer(收vsync、排帧) │ │
│ └ DisplayEventReceiver ──(vsync Connection)──────────► EventThread(app)
├ Surface(画布的Java壳) │ │
├ BLASTBufferQueue(App侧buffer池+提交器) │ │
│ └ GraphicBuffer × 3 ←─ App进程里的显示内存 ▼ ▼
└ ThreadedRenderer(硬件渲染器) Layer #135
└ RenderThread(独立渲染线程) (SF里的图层对象,
持有buffer、参与合成)
- SurfaceControl:指向 SF 里某个 Layer 的"遥控器"(binder 句柄)。App 和 WMS 都持有它,但真正干活的对象在 SF 进程。
- Layer:SF 里的图层。一个窗口的像素容器。
- BLASTBufferQueue :Android 12+ 的提交通道。App 的 RenderThread 画完一帧,把 buffer 封进
SurfaceControl.Transaction直接提交给 SF(旧架构是 App/SF 之间一条双向 binder 的 BufferQueue,BLAST 干掉了中间商)。 - ViewRootImpl:一个窗口 = 一个 ViewRootImpl,它串起 Choreographer/Surface/Renderer 和与 WMS 的会话。
1.8 硬件绘制 vs 软件绘制
-
软件绘制 (默认已淘汰):主线程里 Skia 用 CPU 直接画到 Bitmap(就是一块 buffer),
lockCanvas → draw → unlockCanvasAndPost。同步、慢。 -
硬件绘制(默认):分两步走------
- UI 线程 :遍历 View 树,把
draw()录制成一棵 RenderNode 操作树(display list)。注意是录制,不产生像素! - RenderThread:独立线程,把操作树交给 Skia,Skia 通过 OpenGL ES 发命令给 GPU,GPU 异步渲染到 buffer。
好处:主线程不被 GPU 拖累;display list 可以缓存复用(动画只改属性不用重录)。
- UI 线程 :遍历 View 树,把
本文示例 App 走硬件绘制 + BLAST 提交,是现代 Android 的主干道。
二、埋点:让流程可见
学习最快的方式是让代码自己开口。我在三个进程的关键函数里埋了 26 条日志,编号即流程顺序:(请注意编号是以APP开头为FF01-20,system_server开头的为FF3,SF开头的为FF4,并不是从01-44顺序)
| 进程 | TAG | 点位 | 所在文件 |
|---|---|---|---|
| App | FFApp | FF01~FF20 | ActivityThread / WindowManagerGlobal / ViewRootImpl / Choreographer / ThreadedRenderer |
| WMS | FFWms | FF30~FF34 | WindowManagerService |
| SF | FFSF | FF40~FF44 | SurfaceFlinger.cpp |
| 编号 | 函数 | 回答的问题 |
|---|---|---|
| FF01/02 | handleResumeActivity |
onResume 前后 |
| FF03/04 | wm.addView / new ViewRootImpl |
窗口管家何时诞生 |
| FF05 | ViewRootImpl.setView |
窗口绑定 DecorView |
| FF06/07 | addToDisplayAsUser 调用/返回 |
去WMS登记窗口的binder往返 |
| FF08 | scheduleTraversals |
遍历任务挂入队列 |
| FF09 | doTraversal |
vsync 放行、开始 measure/layout/draw |
| FF10/11 | relayoutWindow 调用/返回 |
向WMS要画布的binder往返 |
| FF12 | BLAST surface ready | 画布就绪 |
| FF13/14 | draw / 硬件分支 |
绘制入口 |
| FF15 | reportDrawFinished |
首帧画完上报WMS |
| FF16/17 | scheduleVsync / onVsync |
vsync 请求/到达(每帧循环) |
| FF18~20 | ThreadedRenderer.draw |
录制 display list、移交 RenderThread |
| FF30 | WMS addWindow |
窗口登记进 system_server |
| FF31~33 | WMS relayoutWindow/createSurfaceControl |
画布在WMS侧的诞生 |
| FF34 | WMS finishDrawingWindow |
WMS 知道"你画完了" |
| FF40/41 | SF createLayer |
Layer 在SF侧的诞生 |
| FF42/43/44 | SF commit/latchBuffer/composite |
取走buffer、合成、送显 |
编译方式:make framework services surfaceflinger;抓取方式:
perl
adb logcat -c
adb shell am start -n com.hmi.bev.carapp/.MainActivity
adb logcat -d -v time | grep -E "FFApp|FFWms|FFSF"
下面正式开讲。所有日志来自一次真实冷启动,pid:5689=carapp,1215=system_server,308=SF,1848=splitapp(分屏对侧,也在启动),1817=Launcher(退场中) 。
三、主流程走读
3.1 生命周期的终点,显示的起点
ActivityThread::handleResumeActivity 一路会调用到 ViewRootImpl::setView 方法,方法内会跨进程调用 WMS::addWindow 来添加窗口。 ◦ WMS 会为该窗口创建一个 Session(会话) ,用于 后续向 SF 请求创建 Surface。 ◦ 后续 App 执行 ViewRootImpl::performTraversals 测绘时,会走到 ViewRootImpl::relayoutWindow,进而调用到 WMS::relayoutWindow 方法。 ◦ WMS::relayoutWindow 方法会创建 SurfaceControl 对象,其内部利用 SurfaceComposerClient 向 SF 请求创建 生产者-消费者模型。 ◦ SF 收到消息后,创建 Layer、Producer、Consumer、BufferQueue 对象。 ◦ WMS::relayoutWindow 方法后续创建一个 Surface.cpp 对象,内部持有 Producer 的引用。将这个 Surface.cpp 对象绑定到 Surface.java 的 mNativeObject 引用上。 ◦ 至此,Surface、BufferQueue、Layer 三者便联系了起来,App 与 SF 之间可以进行数据传输了
css
07:56:43.869 5689 5689 D FFApp: FF01 handleResumeActivity enter ActivityRecord{...}
07:56:43.871 5689 5689 D FFApp: FF02 performResumeActivity done, activity=ComponentInfo{...MainActivity}
java
// ActivityThread.java:5029
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
android.util.Slog.d("FFApp", "FF01 handleResumeActivity enter " + r);
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// TODO Push resumeArgs into the activity for consideration
// skip below steps for double-resume and r.mFinish = true case.
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
if (mActivitiesToBeDestroyed.containsKey(r.token)) {
// Although the activity is resumed, it is going to be destroyed. So the following
// UI operations are unnecessary and also prevents exception because its token may
// be gone that window manager cannot recognize it. All necessary cleanup actions
// performed below will be done while handling destruction.
return;
}
final Activity a = r.activity;
android.util.Slog.d("FFApp", "FF02 performResumeActivity done, activity=" + a.getComponentName());
if (localLOGV) {
Slog.v(TAG, "Resume " + r + " started activity: " + a.mStartedActivity
+ ", hideForNow: " + r.hideForNow + ", finished: " + a.mFinished);
}
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
android.util.Slog.d("FFApp", "FF03 calling wm.addView(decor, l)");
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
① 里回调了你写的 onResume()------FF01/FF02 之间夹着 App 自己打的 MainActivity onResume。
此刻要建立的认知:onResume 执行完,屏幕上还没有这个 Activity 的任何像素。 手里只有一个 PhoneWindow 和它包裹的 DecorView(setContentView 早就装好的 View 树)------一堆 Java 对象而已。屏幕上显示的是 AMS 更早准备的 Splash Screen(启动画面窗口,日志后面会看到它的 layer #117)。接下来的所有日志,就是把"一堆 Java 对象"变成"像素"的过程。
3.2 创建 ViewRootImpl:窗口管家上任
makefile
07:56:43.871 5689 5689 D FFApp: FF03 calling wm.addView(decor, l)
07:56:43.896 5689 5689 D FFApp: FF04 ViewRootImpl created for com.hmi.bev.carapp
scss
// WindowManagerGlobal.java:397
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display, //核心动作就是 new ViewRootImpl
windowlessSession, new WindowlessWindowLayout());
}
android.util.Slog.d("FFApp", "FF04 ViewRootImpl created for " + view.getContext().getPackageName());
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
// BadTokenException or InvalidDisplayException, clean up.
if (viewIndex >= 0) {
removeViewLocked(viewIndex, true);
}
throw e;
}
wm.addView() 一路进到 WindowManagerGlobal.addView(),核心动作就是 new ViewRootImpl。看它的构造函数:
arduino
public ViewRootImpl(Context context, Display display) {
...
mChoreographer = Choreographer.getInstance(); // ★
}
Choreographer.getInstance() 会创建 FrameDisplayEventReceiver,其 native 构造 binder 到 SF:
ini
// frameworks/native/libs/gui/DisplayEventReceiver.cpp
mEventConnection = sf->createDisplayEventConnection(vsyncSource);
这一步在 App 与 SF 的 EventThread(app) 之间建立了一条 Connection (底层 socketpair)。从此 vsync 信号能从 SF 唤醒这个进程------后面所有 FF17 日志的物理前提在此刻奠定。
3.3 setView:一次精心的排序
makefile
07:56:43.897 5689 5689 D FFApp: FF05 setView enter
07:56:43.912 5689 5689 D FFApp: FF08 scheduleTraversals (post TRAVERSAL callback) ← 先!
07:56:43.912 5689 5689 D FFApp: FF06 addToDisplayAsUser call (binder->WMS)
07:56:43.916 1215 2117 D FFWms: FF30 addWindow title=...MainActivity type=1
07:56:43.927 308 1464 I FFSF: FF40 createLayer 8ba7de3 ...MainActivity
07:56:43.928 308 1464 I FFSF: FF41 createLayer done '8ba7de3 ...#130' id=130
07:56:43.940 5689 5689 D FFApp: FF07 addToDisplayAsUser returned res=11
scss
framework\base\core\java\android\view\ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
android.util.Slog.d("FFApp", "FF05 setView enter " + mWindowAttributes.getTitle());
mView = view;
mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
....
// ViewRootImpl.java:1457(源码注释:Schedule the first layout -before- adding to the
// window manager, to make sure we do the relayout before receiving any other events)
requestLayout(); // ① → FF08
....
android.util.Slog.d("FFApp", "FF06 addToDisplayAsUser call (binder->WMS)"); // ② :1486 → FF06
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
android.util.Slog.d("FFApp", "FF07 addToDisplayAsUser returned res=" + res);
注意日志顺序:FF08(挂遍历任务)打在 FF06(跨进程注册窗口)之前。这不是日志乱序,是源码的刻意设计
scss
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals(); // FF08
}
}
scss
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
android.util.Slog.d("FFApp", "FF08 scheduleTraversals (post TRAVERSAL callback)");
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();// 插入同步屏障
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);// 挂遍历回调,等待到FF17,就调到下面的FF09开工
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
先把遍历任务挂进 Choreographer 队列,再去 binder 注册窗口。等注册返回、下个 vsync 一到,遍历立即开跑,一次不用多等。
① FF08 scheduleTraversals()(ViewRootImpl.java:2623)做两件事:
同步屏障让主线程暂停处理普通消息,优先等"异步"的 vsync 消息------遍历任务从此在等一个信号。等待到FF17,就调到下面的FF09开工
② FF06这条 binder->WMS进了 system_server从而打印出了FF30
应用端是如何 SurfaceFlinger 建立链接,进行通信。 它们直接使用 SurfaceSession 通信,SurfaceSession 的创建时机发生在应用第一个窗口显示(addWindow流程)流程,APP 端触发 addWindow 流程是通过匿名 Binder -> Session 这个类与 WindowManagerService 通信的。 在 WindowManagerService::addWindow 方法会先创建 WindowState 对象,此时也会将 Session 作为参数传递。 然后又会执行 WindowState::attach 方法,这里会触发 SurfaceSession 的创建逻辑
java
// WindowManagerService.java:1408
public int addWindow(Session session, IWindow client, LayoutParams attrs, ...) {
android.util.Slog.d("FFWms", "FF30 addWindow title=" + attrs.getTitle() + " type=" + attrs.type); // ← FF30
...
final WindowState win = new WindowState(this, session, ...); // 建户口档案
...
...
win.attach();
mWindowMap.put(client.asBinder(), win);
win.initAppOpsState();
WindowState 只是一条登记记录,此刻还没有画布。 但日志里 SF 同期打出了 FF40/41------layer #130 诞生了:
scss
// SurfaceFlinger.cpp:5358
status_t SurfaceFlinger::createLayer(LayerCreationArgs& args, gui::CreateSurfaceResult& outResult) {
ALOGI("FFSF FF40 createLayer %s", args.name.c_str()); // ← FF40
...
result = createBufferStateLayer(args, &outResult.handle, &layer); // new Layer
result = addClientLayer(args, ...); // 挂进layer树
ALOGI("FFSF FF41 createLayer done '%s' id=%d", String8(outResult.layerName).c_str(),
static_cast<int>(outResult.layerId)); // ← FF41
名字带窗口 hash 前缀 8ba7de3 的 #130 是 WMS 为窗口建的容器层 (WMS 摆位置、做动画用的"外壳"),不是画布 。真正的画布 layer 要等 3.5 节的 #135。这里先记住一个事实:WMS 自己也是 SF 的客户,它建的 layer(容器层、动画 leash、Task 层)比 App 的画布 layer 多得多 ------后面日志里成串的 animation-leash、SplitDecorManager 都是它们。
FF07 的 res=11(二进制 1011)= ADD_OKAY | ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS | ...,注册成功。
3.4 vsync 循环:FF16 与 FF17 的二重奏
在继续主线前,先看这对贯穿全场的日志:
python
(此后每 ~16.7ms 循环,本文只截一次)
... FF16 scheduleVsync (request next vsync)
... FF17 onVsync received ts=64251308413
... FF16 scheduleVsync ...
07:56:43.987 5689 5689 D FFApp: FF09 doTraversal -> performTraversals
java
// Choreographer.java:987
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void scheduleVsyncLocked() {
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
android.util.Slog.d("FFApp", "FF16 scheduleVsync (request next vsync)");
mDisplayEventReceiver.scheduleVsync(); // binder → SF EventThread::requestNextVsyn
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
// Choreographer.java:1284
public void onVsync(long timestampNanos, ...) {
Slog.d("FFApp", "FF17 onVsync received ts=" + timestampNanos);
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true); // 异步消息:能穿过同步屏障
mHandler.sendMessageAtTime(msg, ...);
}
scss
// \frameworks\base\core\java\android\view\ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
android.util.Slog.d("FFApp", "FF09 doTraversal -> performTraversals"); // ← FF09
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals(); //首帧 performTraversals
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
完整信号回路(对应前置知识 1.3):
scss
硬件vsync(HWC上报) → SF调度器按app相位分发 → EventThread(app)检查活跃Connection
→ 往bitube(socketpair)写一个字节 → App主线程Looper的epoll被唤醒
→ onVsync(FF17) → doFrame() → 执行到期的CALLBACK_TRAVERSAL → FF09
ts 相邻差值 ≈ 16.7ms,就是 60Hz 刷新周期。每一帧的 measure/layout/draw 都被这对日志包着:FF16 是"预约",FF17 是"到号",FF09 是"开工"。
防止流程混乱,这里再贴下调用
硬件 vsync 到达 └─ Choreographer.FrameDisplayEventReceiver.onVsync() Choreographer.java:1284 (FF17) └─ Choreographer.doFrame() → doCallbacks(CALLBACK_TRAVERSAL) └─ TraversalRunnable.run() ViewRootImpl.java:9546 └─ doTraversal() ViewRootImpl.java:2643 (FF09) └─ performTraversals() ViewRootImpl.java:3093 --> FF10 -FF12 ├─ performMeasure() ... ├─ performLayout() ... └─ performDraw() ViewRootImpl.java:4825 └─ draw(fullRedrawNeeded, ...) ViewRootImpl.java:4990 ├─ FF13 打印(surface.isValid() 之后) :4996 ├─ FF14 打印(isHardwareEnabled() 分支) :5091 └─ mAttachInfo.mThreadedRenderer.draw() → FF18
3.5 relayout:App 拿到画布
FF09-doTraversal调用 首帧 performTraversals 走到一半发现没有 Surface,发起首帧最贵的一次调用链:
scss
base\core\java\android\view\ViewRootImpl.java
private void performTraversals() {
...
...
if (mFirst || viewVisibilityChanged) {
mViewFrameInfo.flags |= FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED;
}
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); //调用relayoutWindow,后续流程打印FF10 - FF12
cancelDraw = (relayoutResult & RELAYOUT_RES_CANCEL_AND_REDRAW)
== RELAYOUT_RES_CANCEL_AND_REDRAW;
cancelReason = "relayout";
...
...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
if (!performDraw(mActiveSurfaceSyncGroup) && mActiveSurfaceSyncGroup != null) {
mActiveSurfaceSyncGroup.markSyncReady();
}
}
ini
07:56:44.005 5689 5689 D FFApp: FF10 relayoutWindow enter vis=0
07:56:44.045 1215 2117 D FFWms: FF31 relayoutWindow win=8ba7de3 ...MainActivity vis=0 req=2560x1440
07:56:44.046 1215 2117 D FFWms: FF32 createSurfaceControl win=8ba7de3 ...
07:56:44.047 308 340 I FFSF: FF40 createLayer com.fawvw.hmi.bev.carapp/.MainActivity
07:56:44.047 308 340 I FFSF: FF41 createLayer done '...MainActivity#135' id=135 ★画布诞生
07:56:44.048 1215 2117 D FFWms: FF33 SurfaceControl created OK for 8ba7de3 ...
07:56:44.099 5689 5689 D FFApp: FF11 relayout returned, surfaceControlValid=true frame=Rect(0, 0 - 2560, 1440)
07:56:44.103 5689 5689 D FFApp: FF12 app surface ready (BLAST=true, size=2560x1440)
App 侧(ViewRootImpl.java:8418):
csharp
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, ...) {
Slog.d("FFApp", "FF10 relayoutWindow enter vis=" + viewVisibility);
...
relayoutResult = mWindowSession.relayout(mWindow, params,
requestedWidth, requestedHeight, viewVisibility, ...,
mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, // ★出参
mTempInsets, mTempControls, mRelayoutBundle);
Slog.d("FFApp", "FF11 relayout returned, surfaceControlValid=" + mSurfaceControl.isValid()
+ " frame=" + mTmpFrames.frame);
注意 mSurfaceControl 是出参 :App 递进去一个空壳,WMS 填好再跨进程拷回来。binder 传的不是内存不是 buffer,是 SF 里 layer #135 的"遥控器"句柄。
ViewRootImpl 创建的时候就有 mSurface 和 mSurfaceControl 2个成员变量,但是只是个 java 层的对象实例,目前还没有对应 native 的实现,真正赋值的方式是在 relayoutWindow 流程中,后面解释
WMS 侧链路(WindowManagerService.java):
csharp
// :2152
public int relayoutWindow(Session session, IWindow client, ...) {
android.util.Slog.d("FFWms", "FF31 relayoutWindow win=" + win.getName()
+ " vis=" + viewVisibility + " req=" + requestedWidth + "x" + requestedHeight); // 布局计算:算出2560x1440
...
result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); // :2373
// :2611
private int createSurfaceControl(SurfaceControl outSurfaceControl, int result,
WindowState win, WindowStateAnimator winAnimator) {
android.util.Slog.d("FFWms", "FF32 createSurfaceControl win=" + win.getName());
if (!win.mHasSurface) {
result |= RELAYOUT_RES_SURFACE_CHANGED;
}
WindowSurfaceController surfaceController;
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "createSurfaceControl");
surfaceController = winAnimator.createSurfaceLocked(); // new SurfaceControl
// → SurfaceControl.nativeCreate → SurfaceComposerClient::createSurface
// → binder → SurfaceFlinger::createLayer ★ FF40/41 这里调用打印FF40/41
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
if (surfaceController != null) {
surfaceController.getSurfaceControl(outSurfaceControl); // 填出参
android.util.Slog.d("FFWms", "FF33 SurfaceControl created OK for " + win.getName());
ProtoLog.i(WM_SHOW_TRANSACTIONS, "OUT SURFACE %s: copied", outSurfaceControl);
} else {
// For some reason there isn't a surface. Clear the
// caller's object so they see the same state.
ProtoLog.w(WM_ERROR, "Failed to create surface control for %s", win);
outSurfaceControl.release();
}
return result;
}
relayoutWindow 流程是在 addWindow 流程之后, addWindow 流程会在 system_server 进程为要显示的窗口创建对应的 WindowState 。所以这里在执行 relayoutWindow 的时候可以通过windowForClientLocked 方法获取到对应的 WindowState ,而 WindowStateAnimator 是在 WindowState 构造方法中创建的。 拿到了 WindowState 和 WindowStateAnimator 就可以创建SurfaceControl了,注意参数,也就是打印FF33这里,这个 outSurfaceControl 就是在应用端传递过来的,参数前面加上了"out"是 binder 作为出参的方式,也就是说 system_server 进程会对这个参数进行赋值,这样应用端 ViewRootImpl 下的 SurfaceControl 就会持有一个 native 层的 SurfaceControl 指针
SF 里 layer #135 诞生(无窗口 hash 前缀,与 #130 区分)------它才是 App 像素的最终容器。此刻三方各持一份指向它的钥匙:
scss
carapp(5689) system_server(1215) SurfaceFlinger(308)
mSurfaceControl ────────── WindowState.mSurfaceControl ─→ Layer #135(真身)
scss
// SurfaceFlinger.cpp:5358
status_t SurfaceFlinger::createLayer(LayerCreationArgs& args, gui::CreateSurfaceResult& outResult) {
ALOGI("FFSF FF40 createLayer %s", args.name.c_str()); // ← FF40
...
result = createBufferStateLayer(args, &outResult.handle, &layer); // new Layer
result = addClientLayer(args, ...); // 挂进layer树
ALOGI("FFSF FF41 createLayer done '%s' id=%d", String8(outResult.layerName).c_str(),
static_cast<int>(outResult.layerId)); // ← FF41
再回到 App(ViewRootImpl.java:8553):
csharp
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, ...) {
Slog.d("FFApp", "FF10 relayoutWindow enter vis=" + viewVisibility);
...
relayoutResult = mWindowSession.relayout(mWindow, params,
requestedWidth, requestedHeight, viewVisibility, ...,
mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, // ★出参,作为参数传递到 system_sertvice 进程
mTempInsets, mTempControls, mRelayoutBundle);
Slog.d("FFApp", "FF11 relayout returned, surfaceControlValid=" + mSurfaceControl.isValid()
+ " frame=" + mTmpFrames.frame);
...
...
...
if (mSurfaceControl.isValid()) {
if (!useBLAST()) {
mSurface.copyFrom(mSurfaceControl); // 旧架构
} else {
updateBlastSurfaceIfNeeded(); // ★ 建 BLASTBufferQueue,现在都走这给mSurface赋值
}
Slog.d("FFApp", "FF12 app surface ready (BLAST=true ...)"); // ← FF12
mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);
}
updateBlastSurfaceIfNeeded() 创建 BLASTBufferQueue (App 本地的 buffer 池 + 事务提交器)。RenderThread 以后画完一帧,由它把 buffer 封进 SurfaceControl.Transaction 直接提交 SF------现代显示的提交主干道。
上面所说mSurfaceControl 作为参数(out参数)传递到 system_server ,执行 relayoutWindow 流程,在这个过程中会创建 native 层的 SurfaceControl 也会对应用端传过去的 mSurfaceControl 进行赋值 经过 system_server 处理后的 mSurfaceControl 有值了,再给 mSurface 赋值
调用链
arduino
ViewRootImpl::init
Surface::init
SurfaceControl::init
ViewRootImpl::relayoutWindow -- SurfaceControl作为出参
Session::relayout
WindowManagerService::relayoutWindow
WindowManagerService::createSurfaceControl -- 创建显示的Surface
WindowStateAnimator::createSurfaceLocked
WindowSurfaceController::init
SurfaceControl.Build::init
SurfaceControl::init
nativeCreate -- native层创建Surface
WindowSurfaceController::getSurfaceControl
SurfaceControl::copyFrom
SurfaceControl::assignNativeObject -- 将Surface的指针和句柄传递给应用端
Surface::updateBlastSurfaceIfNeeded -- 赋值给应用端的Surface
BLASTBufferQueue::init
BLASTBufferQueue::createSurface
Surface::transferFrom
Surface::setNativeObjectLocked
来看看这个updateBlastSurfaceIfNeeded函数,逻辑还是比较清晰的。 之前分析过在 system_server 执行 relayoutWindow 流程创建 SurfaceControl 的时候 native 层也会创建一个 Surface 。这里又看到通过 BLASTBufferQueue::createSurface 创建了一个 Surface ,这里可能会怀疑为什么又要创建一个 Surface ,这里可以先确定一个结论: 这里"创建"的 Surface 其实和之前创建 SurfaceControl 时创建的 Surface 是一个 native 层的 Surface
scss
// ViewRootImpl.java
void updateBlastSurfaceIfNeeded() {
if (!mSurfaceControl.isValid()) {
return;
}
if (mBlastBufferQueue != null && mBlastBufferQueue.isSameSurfaceControl(mSurfaceControl)) {
mBlastBufferQueue.update(mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y,
mWindowAttributes.format);
return;
}
// If the SurfaceControl has been updated, destroy and recreate the BBQ to reset the BQ and
// BBQ states.
if (mBlastBufferQueue != null) {
mBlastBufferQueue.destroy();
}
mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);
Surface blastSurface = mBlastBufferQueue.createSurface();
// Only call transferFrom if the surface has changed to prevent inc the generation ID and
// causing EGL resources to be recreated.
mSurface.transferFrom(blastSurface);
}
先看看 java 层是如何通过 Surface::transferFrom 给另一个 Surface 赋值的,可以看到 Java 层的 Surface 中定义了一个变量 mNativeObject ,这个变量和 SurfaceControl 是一样都代表着 native 层的指针。 总之,经过这段调用,应用端的 Surface 也持有了对应的 native 层的 Surface 指针
java
/**
* This is intended to be used by {@link SurfaceView#updateWindow} only.
* @param other access is not thread safe
* @hide
* @deprecated
*/
@Deprecated
@UnsupportedAppUsage
public void transferFrom(Surface other) {
if (other == null) {
throw new IllegalArgumentException("other must not be null");
}
if (other != this) {
final long newPtr;
synchronized (other.mLock) {
newPtr = other.mNativeObject; // 设置 surface 指针
other.setNativeObjectLocked(0);
}
synchronized (mLock) {
if (mNativeObject != 0) {
nativeRelease(mNativeObject); // 释放之前的
}
setNativeObjectLocked(newPtr); // 设置新的
}
}
}
private void setNativeObjectLocked(long ptr) {
if (mNativeObject != ptr) {
if (mNativeObject == 0 && ptr != 0) {
mCloseGuard.open("Surface.release");
} else if (mNativeObject != 0 && ptr == 0) {
mCloseGuard.close();
}
mNativeObject = ptr; // 设置 native 指针
mGenerationId += 1;
if (mHwuiContext != null) {
mHwuiContext.updateSurface();
}
}
}
BLAST Buffer Queue机制完全填补了Buffer Queue机制的缺点:
-
BufferQueue的创建放在了业务进程,surfaceflinger中只接受Buffer,同时所有图层属性的提交都通过Transaction进行,大大减轻了surfaceflinger的负担;
-
支持Transaction的合并操作,多个Transaction可以合并为一个提交给surfaceflinger进程,从而可以保证Buffer的提交和几何属性的提交都可在同一帧完成;
-
支持多进程间的协同能力(BLAST SyncEngine),可以对Transaction跨进程传递,携带不同进程的Buffer一起提交给surfaceflinger,实现进程间的帧同步。

3.6 绘制三部曲:录制,而不是画
makefile
07:56:44.112 5689 5689 D FFApp: FF08 scheduleTraversals (post TRAVERSAL callback) ← 第二次!
07:56:44.112 5689 5689 D FFApp: FF16 scheduleVsync
(等一个 vsync......FF17)
07:56:44.158 5689 5689 D FFApp: FF13 draw enter, surface valid
07:56:44.158 5689 5689 D FFApp: FF14 hardware draw path begin
07:56:44.160 5689 5689 D FFApp: FF18 ThreadedRenderer.draw enter
07:56:44.164 5689 5689 D FFApp: FF19 display list built (RenderNode tree ready)
07:56:44.186 5689 5689 D FFApp: FF20 syncAndDrawFrame done syncResult=0 (frame submitted to RenderThread)
为什么 FF08 出现第二次? 第一轮 performTraversals 拿回 Surface 后没直接画------窗口配置变了,需要再来一轮完整遍历,于是又挂任务、又等 vsync。这揭示了一条铁律:每一轮遍历都严格对齐 vsync,绝不自作主张插帧。 从"拿到画布"到"开始画"隔一个周期,是机制使然不是卡顿。
FF13/FF14 的分岔(ViewRootImpl.java:4990):
这里是上面的 performTraversals() ViewRootImpl.java:3093 --> FF10 -FF12 ├─ performMeasure() ... ├─ performLayout() ... └─ performDraw() ViewRootImpl.java:4825 └─ draw(fullRedrawNeeded, ...) ViewRootImpl.java:4990 → FF13 ├─ FF13 打印(surface.isValid() 之后) :4996 ├─ FF14 打印(isHardwareEnabled() 分支) :5091 └─ mAttachInfo.mThreadedRenderer.draw() → FF18
java
// ViewRootImpl.java:4990
private boolean draw(boolean fullRedrawNeeded, ...) {
Surface surface = mSurface;
if (!surface.isValid()) return false;
Slog.d("FFApp", "FF13 draw enter, surface valid");
...
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { // ①有脏区才画
if (isHardwareEnabled()) { // ②硬件加速
Slog.d("FFApp", "FF14 hardware draw path begin");
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); // 调用 FF18
} else {
drawSoftware(...); // 软件分支
}
}
彩蛋:本例后续 44.353 有一条 FF13 但没有 FF14------那轮脏区为空,
draw()直接返回。App 不是每个 vsync 都画,有变化才画。
不要问为什么从FF14 直接打印了FF18,因为FF15是由RenderThread的帧完成回调触发(在下面),FF16,FF17是VSYNC信号申请,收到
FF18~20(ThreadedRenderer.java:795):
arduino
// ThreadedRenderer.java:795
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
Slog.d("FFApp", "FF18 ThreadedRenderer.draw enter");
updateRootDisplayList(view, callbacks); // ①
Slog.d("FFApp", "FF19 display list built (RenderNode tree ready)");
int syncResult = syncAndDrawFrame(frameInfo); // ②
Slog.d("FFApp", "FF20 syncAndDrawFrame done ...");
① 内部从 DecorView 开始递归:
ini
RecordingCanvas canvas = mRootNode.beginRecording(...);
canvas.drawRenderNode(view.updateDisplayListIfDirty()); // 递归每个View
mRootNode.endRecording();
每个 View 持有一个 RenderNode。updateDisplayListIfDirty() 里执行你写的 draw(canvas)------此刻不产生任何像素 ,drawRect/drawText 只被"录制"成 RenderNode 上的操作;子 View 的 RenderNode 作为 DrawRenderNodeOp 挂到父节点。递归完成,DecorView 的 RenderNode 就是记录整个 UI 的操作树(display list) 。为什么录制不直画?主线程不碰 GPU、display list 可缓存复用、动画帧可重放。
② syncAndDrawFrame → native DrawFrameTask::postAndWait():
scss
mRenderThread->queue().post([this]() { run(); }); // 任务投给RenderThread
mSignal.wait(mLock); // UI线程只等"同步",不等渲染
UI 线程等的是 RenderNode 树所有权移交、Surface 绑定这些同步动作 ,做完就返回(FF20)。从此刻起,本帧与 UI 线程无关。
RenderThread 侧异步执行(首帧还夹着 EGL 初始化、buffer 分配等一次性成本,日志里 App 进程同期出现的 OpenGLRenderer: Unable to match the desired swap behavior 就是 eglCreateWindowSurface 的痕迹):
scss
// CanvasContext::draw()
Frame frame = mRenderPipeline->getFrame(); // ① 向BLASTBufferQueue dequeue一块GraphicBuffer
mRenderPipeline->draw(mRenderNodes, ...); // ② 重放display list → Skia → GL命令 → GPU
mRenderPipeline->swapBuffers(frame); // ③ buffer+GPU fence 封进Transaction提交SF
3.7 首帧完成上报
ini
07:56:44.258 5689 5689 D FFApp: FF15 reportDrawFinished seqId=0 (first frame drawn & reported to WMS)
07:56:44.263 1215 1590 D FFWms: FF34 finishDrawingWindow win=8ba7de3 ...MainActivity
less
// ViewRootImpl.java:4718 ------ 由RenderThread的帧完成回调触发,不是主线程
private void reportDrawFinished(@Nullable Transaction t, int seqId) {
Slog.d("FFApp", "FF15 reportDrawFinished seqId=" + seqId);
mWindowSession.finishDrawing(mWindow, t, seqId); // binder → WMS
less
void finishDrawingWindow(Session session, IWindow client,
@Nullable SurfaceControl.Transaction postDrawTransaction, int seqId) {
if (postDrawTransaction != null) {
postDrawTransaction.sanitize(Binder.getCallingPid(), Binder.getCallingUid());
}
final long origId = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
WindowState win = windowForClientLocked(session, client, false);
android.util.Slog.d("FFWms", "FF34 finishDrawingWindow win=" //FF34
+ (win != null ? win.getName() : "null"));
ProtoLog.d(WM_DEBUG_ADD_REMOVE, "finishDrawingWindow: %s mDrawState=%s",
win, (win != null ? win.mWinAnimator.drawStateToString() : "null"));
if (win != null && win.finishDrawing(postDrawTransaction, seqId)) {
if (win.hasWallpaper()) {
win.getDisplayContent().pendingLayoutChanges |=
WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
}
win.setDisplayLayoutNeeded();
mWindowPlacerLocked.requestTraversal();
}
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
WMS 侧(WindowManagerService.java:2654):win.finishDrawing() 把窗口状态推进到 READY_TO_SHOW ,随后 requestTraversal() 触发 WMS 的布局遍历------决定何时真正 show 这个窗口、何时移除 Splash、何时播过渡动画。
seqId 值得单独一讲:它对应 WMS 的 BLASTSyncGroup 。当 WMS 要编排多窗口同步动画(本例是分屏切换)时,给窗口发 seqId,App 提交的事务在 WMS 侧攒着不 apply,等组里所有窗口都画完首帧再统一放行 。本例 seqId=0 = 没有同步组,buffer 到了直接上屏。对照组实验中同样场景出现过 seqId=1:首帧画好后被扣了 341ms 才 latch------不是渲染慢,是在等分屏对侧的 App 画完一起进场。"首帧延迟"的大头往往不在渲染,而在 transition 编排。
3.8 SF 合成上屏:最后一棒
arduino
07:56:44.262 308 308 I FFSF: FF42 commit (vsync-sf wakeup, latch+rebuild)
07:56:44.263 308 308 I FFSF: FF43 latched buffer, layer='com.hmi.bev.carapp/.MainActivity#135'
07:56:44.264 308 308 I FFSF: FF44 composite (HWC/GPU compose + present)
App 的 BLAST transaction 到达 SF 后并不立即处理------SF 标记 layer 有新数据,等自己的 vsync(vsync-sf 相位,前置知识 1.3 的第二路)。主循环每周期两拍:
第一拍 commit(SurfaceFlinger.cpp:2338):
scss
bool SurfaceFlinger::commit(...) {
ALOGI("FFSF FF42 commit ..."); // ← FF42
bool mustComposite = latchBuffers() || shouldCommit;
latchBuffers()(:4074)遍历 layer 树,对有新帧的 layer 调 latchBuffer()------latch = acquire 那块刚渲染好的 buffer(先等 GPU fence 释放),提升为 layer 的当前帧:
less
// SurfaceFlinger.cpp:4134
if (layer->latchBuffer(visibleRegions, latchTime)) {
ALOGI("FFSF FF43 latched buffer, layer='%s'", layer->getDebugName()); // ← FF43
FF43 打出 #135:你的首帧正式成为 SF 的合成输入。 同刻 latch 的还有 Splash #117 等兄弟 layer。
第二拍 composite(SurfaceFlinger.cpp:2493):
scss
CompositeResultsPerDisplay SurfaceFlinger::composite(...) {
ALOGI("FFSF FF44 composite ..."); // ← FF44
mCompositionEngine->present(refreshArgs); // ①
① 内部三步(前置知识 1.6 的落地):
- rebuildLayerStacks:按 Z 序遍历,算每层可见区域
- 问 HWC:逐 layer "你能 overlay 直接叠吗?"→ 回报 DEVICE(硬件)或 CLIENT(GPU 合成)
- presentDisplay :CLIENT 层用 SF 自带 RenderEngine 合成进 framebuffer;最终 layer→plane 绑定提交 HWC → 驱动在下一个 VBI page flip → 面板逐行扫出新帧
下一个扫描周期,你的首帧物理上成为屏幕上的光。 从 FF01 到这里,395ms(首帧有大量一次性初始化;稳态帧只有十几毫秒)。
3.9 尾声:首帧"已显示"≠"可见"
日志继续往后还有一课:
arduino
44.263 FF43 latch #135 ← 首帧已合成(被Splash盖着)
44.313 FF43 latch #135 ← 第二帧(App 连续绘制)
45.115 FFSF: createLayer '...animation-leash of window_anim' ← Splash退场动画开始
45.200 FF43 latch #135 ← 第三帧
~45.3 Splash 移除 → 用户完整看到 App
Splash(AMS 为冷启动预绘的窗口)要等 App 首帧 READY 后才开始退场动画。所以"首帧已上屏"和"用户看见内容"之间还隔着一段动画------这是 UX 设计,不是性能问题。看日志要区分"layer 有内容"和"layer 可见"两件事。
四、全景总结
一张图收束全文(编号即日志点位,可当流程检查单用):
scss
【App·主线程】 【system_server】 【SurfaceFlinger】
FF01 handleResumeActivity
FF02 (onResume完)
FF03 wm.addView
FF04 new ViewRootImpl ──(构造时注册vsync Connection)──→ EventThread(app)
FF05 setView
FF08 scheduleTraversals(任务挂队)
FF06 addToDisplay ──binder──→ FF30 addWindow(new WindowState)
└─容器层──────────→ FF40/41 createLayer #130
FF07 返回
⏳ FF16请求 → 硬件vsync → FF17到达 → FF09 doTraversal
(performMeasure / performLayout)
FF10 relayoutWindow ──binder──→ FF31 relayoutWindow
FF32 createSurfaceControl ─binder─→ FF40/41 createLayer #135 ★
FF33 OK
FF11 SurfaceControl到手
FF12 BLASTBufferQueue就绪
⏳ 再等一个vsync(FF16→FF17)
FF13/FF14 draw
FF18 ThreadedRenderer.draw
FF19 录制display list(不产生像素!)
FF20 移交RenderThread(主线程下班)
【App·RenderThread】dequeue buffer → Skia→GL→GPU渲染 → Transaction提交
FF15 reportDrawFinished ──binder──→ FF34 finishDrawingWindow(READY_TO_SHOW)
⏳ 等vsync-sf相位
FF42 commit
FF43 latch #135 ★
FF44 composite→HWC→page flip
↓ 下一扫描周期
像素上屏 👁
每个角色的一句话定位:
- ViewRootImpl ------ 窗口的显示总管家
- Choreographer ------ 节拍器,vsync 的请求与消费
- WMS ------ 户籍科 + 导演:登记窗口、发画布、编排动画(BLASTSyncGroup 是它的片场闸门)
- Layer/SurfaceFlinger ------ 像素的容器与合成师
- HWC ------ 最后一级投影仪,兼发 vsync 的钟表匠
三条最反直觉、也最值钱的结论:
- onResume 时窗口还什么都没有------Surface 要到首次 performTraversals 的 relayout 才诞生;
- draw() 不画像素------UI 线程只录 display list,像素由 RenderThread+GPU 异步产出,靠 fence 做交接;
- 首帧画好 ≠ 立刻上屏------中间还隔着一个 vsync-sf 相位、(可能有)BLASTSyncGroup 的统一放行、(通常有)启动动画的退场。

五、动手建议
想继续深挖,顺着这套"埋点→读日志"的方法可以钻的方向:
- fence 机制 :在
Layer::latchBuffer里打 fence 状态,观察 GPU 完成时刻与 SF 消费时刻 - HWC 决策 :
adb shell dumpsys SurfaceFlinger看composition type字段,验证哪些 layer 走了 overlay - 掉帧定位 :
adb shell dumpsys SurfaceFlinger --latency <layer>拿 vsync/desired/present 三个时间戳 - transition 编排 :给 WMS 的
BLASTSyncEngine埋日志,量化"等待队友"的耗时 - 稳态帧:给动画中的 App 抓 FF16/FF17/FF09 循环,对照 Choreographer 的 FrameInfo 理解每帧节奏
源码比任何博客都诚实。给系统加上自己的日志,让它亲口告诉你发生了什么------这是学显示体系(乃至任何复杂系统)最快的路。