获取控件宽高的时序问题

266 阅读5分钟

一、获取控件宽高的时序问题

在 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(不推荐,可能引入时序问题)。

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 控制。