26、View的绘制与Activity生命周期:onCreate、onStart、onResume中绘制行为的不同、onWindowFocusChanged的作用
大家好,我是你们的老朋友。今天我们来聊一个非常经典,但又特别容易踩坑的话题——View的绘制和Activity生命周期之间的关系。
说实话,这个问题我面试过很多人,能答到点子上的不多。很多人背了生命周期,也背了绘制流程,但就是没把这两者串起来。今天我就带大家彻底搞明白:onCreate、onStart、onResume里到底能不能拿到View的宽高?onWindowFocusChanged又扮演了什么角色?
一、生命周期与绘制的“时间差”
先问大家一个问题:Activity的onCreate里,View的宽高是多少?
答案是:0。或者更准确地说,是未测量的状态。
为什么会这样?
我们得从Android的UI渲染机制说起。View的绘制流程分为三步:measure(测量)→ layout(布局)→ draw(绘制)。这三步并不是在生命周期方法里执行的,而是在Choreographer的调度下,通过ViewRootImpl来触发的。
说白了,onCreate、onStart、onResume只是Activity自身的状态回调,和View的绘制是两条并行的线。它们之间有一个时间差。
核心结论:onCreate执行时,ViewRootImpl还没创建,View连测量都没开始。onResume执行时,View可能还在排队等待绘制。
我记得有一次,一个同事在onCreate里调用view.getWidth(),结果拿到的永远是0。他百思不得其解,后来我告诉他:你这是在“催熟”一个还没长大的孩子。他听完就笑了。
二、onCreate、onStart、onResume中的绘制行为
我们一个一个来看。
1. onCreate:View的“胚胎期”
在onCreate里,我们通常做两件事:setContentView和findViewById。
setContentView干了什么?它只是把布局文件解析成View树,然后挂到Window上。但注意,这时候ViewRootImpl还没创建。ViewRootImpl是在onResume之后,通过WindowManager.addView才真正关联上的。
所以,在onCreate里,View的宽高是0,任何测量相关的操作都是无效的。
// 错误示范
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View view = findViewById(R.id.my_view);
int width = view.getWidth(); // 永远是0
int height = view.getHeight(); // 永远是0
}
2. onStart:View的“婴儿期”
onStart执行时,Activity已经可见了,但还没有和用户交互。这时候ViewRootImpl已经创建,但第一次绘制可能还没完成。
我个人习惯在onStart里做一些轻量级的初始化,比如注册广播、绑定服务。但千万别在这里依赖View的宽高,因为measure可能还没执行。
小技巧:如果你非要在onStart里获取View的宽高,可以调用view.post(),把操作放到消息队列的末尾。这样等View绘制完成后,你的Runnable才会执行。
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth(); // 这时候就能拿到了
int height = view.getHeight();
}
});
}
3. onResume:View的“少年期”
onResume执行时,Activity已经准备好和用户交互了。但注意,onResume返回后,View的第一次绘制才真正开始。
为什么?因为onResume是Activity生命周期的最后一个回调,之后AMS才会通知WindowManager去执行relayoutWindow,进而触发ViewRootImpl的performTraversals。
所以,在onResume里,View的宽高依然可能是0。除非你调用view.post(),或者使用ViewTreeObserver。
避坑指南:我曾经在onResume里直接调用view.measure(),想手动触发测量。结果发现布局错乱,因为手动measure会破坏View的测量缓存,导致后续的自动测量出现问题。所以,不要手动调用measure,除非你非常清楚自己在做什么。
三、onWindowFocusChanged:真正的“绘制完成”信号
那么,有没有一个回调能告诉我们:View已经绘制完成了?
有,就是onWindowFocusChanged。
这个方法在Window获取或失去焦点时被调用。第一次调用时,View的第一次绘制已经完成,宽高都是正确的。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getWidth(); // 这时候一定能拿到正确的值
int height = view.getHeight();
}
}
但注意,onWindowFocusChanged会被多次调用。比如用户按Home键、切到其他App、再切回来,都会触发。所以,如果你只想在第一次绘制完成时执行某些操作,记得加一个标志位。
核心结论:onWindowFocusChanged是获取View宽高的“最安全”时机,但要注意它会被多次调用。
四、知识体系图
下面我用一张SVG图来总结一下整个流程:
五、总结与建议
好了,我们来总结一下今天的内容:
- onCreate:View的“胚胎期”,宽高为0,不要做任何测量操作。
- onStart:View的“婴儿期”,ViewRootImpl已创建,但绘制还没开始。
- onResume:View的“少年期”,第一次绘制还在排队中。
- onWindowFocusChanged:View的“成年期”,第一次绘制完成,宽高可用。
我个人建议:如果你需要在View绘制完成后执行某些操作,优先使用View.post()或者ViewTreeObserver。onWindowFocusChanged虽然可靠,但容易被多次调用,需要额外处理。
最后,送大家一句话:理解生命周期和绘制流程的时间差,是Android进阶的必修课。希望今天的分享对你有帮助。
额外提示:如果你在做动画或者自定义View,建议在onSizeChanged里获取宽高,这个回调只在尺寸变化时触发,比onWindowFocusChanged更精确。