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里,我们通常做两件事:setContentViewfindViewById

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图来总结一下整个流程:

View绘制与Activity生命周期关系图 onCreate onStart onResume onWindowFocusChanged ViewRootImpl未创建 ViewRootImpl已创建 第一次绘制排队中 第一次绘制完成 View的宽高在onWindowFocusChanged中才真正可用 ⚠️ onWindowFocusChanged会被多次调用,注意加标志位

五、总结与建议

好了,我们来总结一下今天的内容:

  • onCreate:View的“胚胎期”,宽高为0,不要做任何测量操作。
  • onStart:View的“婴儿期”,ViewRootImpl已创建,但绘制还没开始。
  • onResume:View的“少年期”,第一次绘制还在排队中。
  • onWindowFocusChanged:View的“成年期”,第一次绘制完成,宽高可用。

我个人建议:如果你需要在View绘制完成后执行某些操作,优先使用View.post()或者ViewTreeObserver。onWindowFocusChanged虽然可靠,但容易被多次调用,需要额外处理。

最后,送大家一句话:理解生命周期和绘制流程的时间差,是Android进阶的必修课。希望今天的分享对你有帮助。

额外提示:如果你在做动画或者自定义View,建议在onSizeChanged里获取宽高,这个回调只在尺寸变化时触发,比onWindowFocusChanged更精确。


公众号:蓝海资料掘金营,微信deep3321