一、获取控件宽高的时序问题
在 Android 开发中,在 onResume() 中直接获取控件宽高可能无效,主要与 UI 生命周期、垂直同步(VSYNC)机制 和 消息队列(post 机制) 相关。以下是详细分析:
1. 为何在 onResume() 中直接获取宽高可能无效?
- 视图未完成布局:
onResume()调用时,Activity 已可见,但 UI 的测量(measure)、布局(layout)可能尚未完成。此时直接调用getWidth()或getHeight()可能返回 0。 - UI 绘制流程:视图的测量和布局由
ViewRootImpl触发,并通过Choreographer在下一个 VSYNC 信号到来时执行。若onResume()执行时尚未到达下一个 VSYNC 周期,布局任务未执行,宽高自然无效。
2. 垂直同步(VSYNC)的影响
- 同步 UI 更新:VSYNC 每隔 16.6ms(60Hz 屏幕)触发一次,确保 UI 的测量、布局、绘制操作在固定周期执行。
- **
onResume()与 VSYNC 的时序**:若onResume()调用时,下一个 VSYNC 尚未触发,布局任务可能未执行。此时直接获取宽高会失败。 - Choreographer 的作用:通过
Choreographer.postCallback()将布局任务注册到 VSYNC 回调,保证任务在下一个周期执行。
3. post 队列的作用
-
延迟执行机制:
View.post(Runnable)将任务添加到 UI 线程的消息队列尾部。若在onResume()中使用post,任务可能在布局完成后执行。 -
不可靠性:若布局任务尚未加入队列(等待 VSYNC),
post的任务可能在布局前执行,仍无法获取宽高。此时需更可靠的方法:- ViewTreeObserver:通过
OnGlobalLayoutListener监听布局完成事件。 - 手动延迟:结合
postDelayed(不推荐,可能引入时序问题)。
- ViewTreeObserver:通过
4. 可靠获取宽高的方法
-
ViewTreeObserver:
java 复制 view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = view.getWidth(); int height = view.getHeight(); // 处理宽高 } }); -
手动 post 到消息队列:
java 复制 view.post(() -> { int width = view.getWidth(); int height = view.getHeight(); // 处理宽高(可能需二次检查) }); -
onWindowFocusChanged:在窗口获得焦点时获取宽高(需注意前后台切换)。
java 复制 @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getWidth(); int height = view.getHeight(); } }
5. 总结
- 直接获取无效的原因:布局未完成,VSYNC 尚未触发测量和布局。
- post 队列的作用:延迟任务执行,但无法保证在布局之后。
- 垂直同步的影响:布局任务依赖 VSYNC 周期,间接影响
post的执行时机。 - 最佳实践:使用
ViewTreeObserver监听布局完成,或通过onGlobalLayout()回调获取宽高。
通过理解 UI 生命周期、VSYNC 机制和消息队列的协作,可以更精准地处理控件宽高获取的时机问题。
二、ViewTreeObserver 的原理分析
ViewTreeObserver 是 Android 中用于监听视图树(View Hierarchy)状态变化的工具类。它通过观察视图树的全局事件(如布局完成、绘制前、触摸模式变化等),提供了一种在视图树生命周期关键节点执行代码的机制。以下是其核心原理:
1. 核心作用
- 监听视图树全局事件:例如布局完成(
OnGlobalLayoutListener)、绘制前(OnPreDrawListener)、焦点变化(OnGlobalFocusChangeListener)等。 - 确保代码在正确的时机执行:例如在布局完成后获取控件宽高,或在绘制前动态调整视图属性。
2. 实现原理
2.1 数据结构
-
观察者模式:
ViewTreeObserver内部维护多个监听器列表,每个列表对应一类事件(如mOnGlobalLayoutListeners存储布局完成的监听器)。java 复制 // 简化的伪代码结构 public class ViewTreeObserver { private CopyOnWriteArrayList<OnGlobalLayoutListener> mOnGlobalLayoutListeners; private CopyOnWriteArrayList<OnPreDrawListener> mOnPreDrawListeners; // 其他监听器列表... }
2.2 注册监听器
-
添加监听器:通过
addOnGlobalLayoutListener()等方法将监听器添加到对应列表。java 复制 view.getViewTreeObserver().addOnGlobalLayoutListener(listener); -
绑定到视图树:当监听器被注册时,
ViewTreeObserver会与当前视图树关联(通过ViewRootImpl)。
2.3 事件触发
-
由系统驱动:视图树的关键事件(如布局、绘制)由
ViewRootImpl触发,并通过Choreographer同步到 VSYNC 信号。 -
分发事件:例如,在布局完成后,
ViewRootImpl会调用dispatchOnGlobalLayout()方法,遍历所有注册的OnGlobalLayoutListener并通知它们。java 复制 // ViewRootImpl 内部逻辑(伪代码) void performTraversals() { // 1. 执行测量(measure)、布局(layout) measureHierarchy(...); layout(...); // 2. 通知布局完成 if (mView != null) { mView.dispatchOnGlobalLayout(); } }
2.4 自动注销监听器
-
视图树分离时失效:当视图从窗口移除时(如 Activity 销毁),
ViewTreeObserver会标记为失效(mAlive = false),后续添加监听器会抛出异常。 -
开发者手动移除:监听器需在回调中手动移除(避免内存泄漏或重复触发):
java 复制 view.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // 移除监听器 view.getViewTreeObserver().removeOnGlobalLayoutListener(this); // 处理逻辑(如获取宽高) } });
3. 关键机制
3.1 与 ViewRootImpl 的协作
- ViewRootImpl 控制视图树生命周期:负责触发视图的测量、布局、绘制流程。
- 事件分发入口:
ViewRootImpl在关键节点(如performTraversals())调用ViewTreeObserver的分发方法。
3.2 线程与消息队列
- 仅限主线程:所有监听器的注册和回调均在主线程执行。
- 与 Choreographer 同步:布局和绘制的监听器通过 VSYNC 信号同步,确保事件在渲染周期内触发。
3.3 性能优化
- CopyOnWriteArrayList:监听器列表使用线程安全的
CopyOnWriteArrayList,避免并发修改异常。 - 惰性初始化:仅当有监听器注册时,才会创建对应的列表。
4. 典型应用场景
4.1 获取控件宽高
-
在布局完成后触发:通过
OnGlobalLayoutListener确保代码在布局完成后执行。java 复制 view.getViewTreeObserver().addOnGlobalLayoutListener(...);
4.2 动态干预绘制
-
在绘制前修改视图属性:通过
OnPreDrawListener在绘制前调整属性(如缩放控件),并返回true以继续绘制。java 复制 view.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { // 修改视图属性 view.setScaleX(0.5f); // 返回 true 表示继续绘制 return true; } });
5. 与其他机制的对比
| 机制 | 特点 |
|---|---|
View.post(Runnable) | 将任务添加到主线程队列,但无法保证在布局完成后执行。 |
ViewTreeObserver | 直接绑定到视图树生命周期,确保在布局、绘制等关键节点执行。 |
OnWindowFocusChanged | 在窗口焦点变化时触发,可能因后台切换导致多次调用。 |
6. 总结
- 核心原理:通过观察者模式监听视图树生命周期事件,由
ViewRootImpl驱动事件分发。 - 关键优势:确保代码在视图树状态稳定时执行(如布局完成、绘制前)。
- 注意事项:需手动移除监听器,避免内存泄漏;仅在视图附着到窗口时有效。
通过 ViewTreeObserver,开发者可以在视图树的关键生命周期节点插入逻辑,实现精准的 UI 控制。