1、View绘制流程总览:从Activity启动到ViewRootImpl,理解整个绘制链路的起点
大家好,我是你们的Android讲师。
今天咱们正式开始啃这块硬骨头——View绘制流程。
说实话,我刚入行那会儿,看到屏幕上花花绿绿的界面,总觉得这玩意儿挺神奇的。后来自己动手写自定义View,动不动就卡顿、显示不全,才意识到:不懂底层绘制流程,写出来的UI就是碰运气。
这一章,我们先搭个全局框架。不急着抠细节,先把从Activity启动到ViewRootImpl这条链路走通。你想想看,一个Activity从创建到界面显示,中间到底经历了什么?
1.1 起点:Activity的启动
一切从startActivity()开始。
但咱们关心的不是AMS怎么调度,而是界面什么时候开始准备绘制。
关键节点在ActivityThread.handleLaunchActivity()里。它会调用Activity.onCreate(),你在onCreate()里写了setContentView(),这时候布局文件被解析成View树,但注意——还没开始测量和绘制。
核心认知:onCreate()只是把布局加载进内存,真正的绘制还没开始。
我记得有个同事,在onCreate()里直接拿View.getWidth(),结果全是0。他一脸懵地问我为什么。嗯,这就是典型的时机不对。
1.2 关键转折:Window与DecorView
Activity.onCreate()执行完后,ActivityThread会调用handleResumeActivity()。这里有个重要动作:创建PhoneWindow,并把DecorView添加到WindowManager。
说白了,DecorView就是整个Activity界面的根View。它包含了一个LinearLayout,里面放着标题栏和内容区域。你写的布局文件,最终被塞进android.R.id.content这个FrameLayout里。
// 伪代码示意
// ActivityThread.handleResumeActivity()
View decor = activity.getWindow().getDecorView();
ViewManager wm = activity.getWindowManager();
wm.addView(decor, layoutParams);
这里有个细节:WindowManager.addView()最终会走到WindowManagerGlobal.addView()。而WindowManagerGlobal会创建一个ViewRootImpl对象,并调用root.setView()。
我的经验:调试UI问题时,经常在WindowManagerGlobal里打断点。这里能看到所有Window的添加和移除,对排查窗口泄漏特别有用。
1.3 核心枢纽:ViewRootImpl登场
ViewRootImpl是什么?
它是View树和WindowManager之间的桥梁。没有它,你的View就是一堆孤立的对象,没法被绘制到屏幕上。
ViewRootImpl.setView()里做了三件大事:
- 建立连接:通过Binder与WMS(WindowManagerService)通信,请求添加窗口
- 请求布局:调用
requestLayout(),触发第一次绘制 - 接收事件:注册InputChannel,开始接收触摸事件
其中,requestLayout()是绘制流程的真正起点。
// ViewRootImpl.java (简化)
public void setView(View view, ...) {
// 1. 与WMS通信
res = mWindowSession.addToDisplay(mWindow, ...);
// 2. 触发第一次绘制
requestLayout();
// 3. 注册输入事件
mInputChannel = new InputChannel();
mWindowSession.addToDisplay(mInputChannel, ...);
}
注意:requestLayout()不是立即执行绘制,而是发送一个异步消息。真正干活是在下一个VSync信号到来时。
1.4 绘制三兄弟:measure、layout、draw
当ViewRootImpl收到VSync信号后,会调用performTraversals()。这个方法,就是整个绘制流程的总调度中心。
它里面依次调用了三个核心方法:
| 阶段 | 方法 | 作用 |
|---|---|---|
| 测量 | performMeasure() |
确定每个View的宽高 |
| 布局 | performLayout() |
确定每个View的位置 |
| 绘制 | performDraw() |
把View画到Canvas上 |
这三个阶段,顺序不能乱。你想想看,不知道大小怎么定位置?不知道位置怎么画?
我个人习惯把performTraversals()比作导演,measure、layout、draw就是三个演员。导演喊一声“Action”,演员按剧本依次上场。
1.5 从performTraversals到ViewRootImpl的完整链路
咱们把整个链路串起来,画个流程图:
Activity.startActivity()
→ ActivityThread.handleLaunchActivity()
→ Activity.onCreate() // 加载布局,但未绘制
→ ActivityThread.handleResumeActivity()
→ WindowManager.addView(decor)
→ WindowManagerGlobal.addView()
→ 创建ViewRootImpl
→ ViewRootImpl.setView()
→ requestLayout()
→ scheduleTraversals()
→ doTraversal()
→ performTraversals()
→ performMeasure()
→ performLayout()
→ performDraw()
看到没?从startActivity()到performDraw(),中间隔了整整七八层调用。每一层都有它的职责,少一环都不行。
避坑指南:我曾经在onResume()里直接获取View的宽高,结果全是0。后来才知道,onResume()执行时performTraversals()还没跑完。正确的做法是用View.post()或者ViewTreeObserver。
1.6 为什么需要理解这条链路?
你可能会问:我平时写布局,不都是直接写XML吗?知道这些底层细节有什么用?
用处大了去了:
- 性能优化:知道绘制是在
performTraversals()里完成的,你就明白为什么频繁requestLayout()会导致卡顿 - 自定义View:理解measure/layout/draw的调用顺序,你写自定义View时就不会乱调方法
- 问题排查:UI显示异常时,你能快速定位是哪个阶段出了问题
我记得有一次,项目里有个页面启动特别慢。我顺着performTraversals()往下查,发现是某个自定义View在onMeasure()里做了大量计算。优化后,启动速度提升了30%。
小技巧:用Choreographer可以监控每一帧的绘制耗时。在performTraversals()前后打日志,能精确知道measure、layout、draw各花了多少时间。
1.7 本章小结
这一章,咱们把View绘制的宏观链路走了一遍:
- Activity启动后,通过
WindowManager把DecorView添加进来 ViewRootImpl作为桥梁,连接View树和WMSperformTraversals()是总调度,依次执行measure、layout、draw- 整个流程是异步的,依赖VSync信号触发
下一章,咱们深入performMeasure(),看看MeasureSpec到底是怎么计算出来的。到时候你会发现,很多布局问题,其实都是MeasureSpec没搞明白。
嗯,今天就到这儿。有问题欢迎留言,咱们下章见。