Android视图状态以及重绘

一、视图状态(View States)

1. 五种核心状态
状态 作用 修改方法 特点
enabled 视图是否响应交互 setEnabled(boolean) 禁用状态下不响应onTouch事件
focused 视图是否获得焦点 requestFocus() 需同时满足focusable和focusableInTouchMode
window_focused 视图所在窗口是否在前台 系统自动维护 应用无法直接修改
selected 视图是否被选中 setSelected(boolean) 同一界面允许多个视图同时选中
pressed 视图是否被按下 setPressed(boolean) 通常由系统自动设置点击状态
2. 状态变更响应流程

关键源码解析

  1. 状态变更入口

    java 复制代码
    // View.java
    protected void drawableStateChanged() {
        Drawable d = mBackground;
        if (d != null && d.isStateful()) {
            d.setState(getDrawableState()); // 传递新状态给Drawable
        }
    }
  2. 状态匹配原理

    java 复制代码
    // StateListDrawable.java
    protected boolean onStateChange(int[] stateSet) {
        int idx = findStateIndex(stateSet); // 匹配selector中对应的item
        return selectDrawable(idx); // 切换Drawable
    }
  3. 触发重绘

    java 复制代码
    // StateListDrawable.java
    public boolean selectDrawable(int idx) {
        // ...更新Drawable
        invalidateSelf(); // 关键重绘触发
    }

二、重绘机制(View Invalidation)

1. 两种重绘方式对比
方法 触发流程 应用场景 性能影响
invalidate() 仅重走draw()流程 内容变化但尺寸不变(如文字/颜色) 低开销,局部刷新
requestLayout() 完整measure-layout-draw 视图尺寸/结构变化(如添加子View) 高开销,全局重新布局
2. invalidate() 核心流程

源码关键路径

java 复制代码
// ViewRootImpl.java
void scheduleTraversals() {
    sendEmptyMessage(DO_TRAVERSAL); // 发送异步消息
}

public void handleMessage(Message msg) {
    if (msg.what == DO_TRAVERSAL) {
        performTraversals(); // 最终入口
    }
}

private void performTraversals() {
    // 根据标记位决定流程
    if (!mLayoutRequested) {
        // 仅执行draw流程
        performDraw();
    }
}
3. 性能优化要点
  1. 减少重绘范围

    java 复制代码
    // 只刷新局部区域
    public void invalidate(Rect dirty) {
        // 计算脏区域并传递
    }
  2. 避免过度重绘

    • 使用View.setWillNotDraw(true)跳过无内容视图

    • 合并状态变更(避免连续多次invalidate)


三、常见问题

Q1:按下按钮时背景图切换的完整流程?

A

  1. 状态变更View.setPressed(true)更新状态数组

  2. 通知DrawabledrawableStateChanged()调用StateListDrawable.setState()

  3. 匹配资源StateListDrawable.onStateChange()查找对应状态图片

  4. 触发重绘selectDrawable()invalidateSelf()View.invalidate()

  5. 绘制执行 :递归至ViewRootImpl.scheduleTraversals() → 下一帧触发draw()流程

Q2:invalidate() 和 requestLayout() 的本质区别?

A

  • invalidate()

    • 仅设置DIRTY标记 → 触发draw()流程

    • 不重新测量/布局 → 适用于内容变化但尺寸不变场景

  • requestLayout()

    • 设置FORCE_LAYOUT标记 → 触发完整measure-layout-draw

    • 向父视图递归 → 可能引发全局重新布局

Q3:为什么StateListDrawable能自动切换图片?

A :核心机制是状态匹配+重绘触发

  1. res/drawable中定义<selector>状态映射

  2. onStateChange()用状态数组匹配最佳item下标

  3. selectDrawable()切换当前Drawable并调用invalidateSelf()

Q4:自定义View如何优化重绘性能?

A:三级优化策略:

  1. 减少区域

    java 复制代码
    // 只刷新变化区域
    invalidate(dirtyRect);
  2. 避免过度绘制

    • 覆写hasOverlappingRendering()返回false

    • 使用canvas.clipRect()限制绘制区域

  3. 复用资源

    • 预初始化Paint/Path等对象

    • 使用View.setLayerType(LAYER_TYPE_HARDWARE)启用硬件加速

Q5:解释下 scheduleTraversals() 中发送异步消息的意义?是否在主线程执行?

A

核心是通过异步消息+同步屏障确保UI更新的及时性

  1. 异步消息DO_TRAVERSAL 消息被标记为异步类型,优先于普通消息处理

  2. 同步屏障

    • 在消息队列插入屏障,阻塞后续同步消息

    • 仅允许异步的UI更新消息通过

  3. 主线程执行

    • 消息最终由 ViewRootImplHandler 在主线程处理

    • 调用 performTraversals() 执行完整的视图树遍历

  4. 设计目的

    • 解决UI更新被业务消息阻塞的问题

    • 保证16ms内完成绘制(60Hz刷新率)

使用代码证明主线程执行

performTraversals() 中可检查线程:

java 复制代码
void performTraversals() {
    if (Thread.currentThread() != mThread) {
        throw new RuntimeException("Must be on UI thread!");
    }
    // ...measure/layout/draw...
}

其中 mThreadViewRootImpl 创建时的主线程。

Q6:View.postInvalidate() 和 invalidate() 区别?

A

维度 invalidate() postInvalidate()
调用线程 仅UI线程 任意线程
内部实现 直接操作视图树 通过Handler转发到UI线程
适用场景 视图内部状态变更 后台线程触发的UI更新

四、总结

Q:请解释Android视图状态变更如何触发界面更新?

A

整个过程分为四个关键阶段:

  1. 状态变更

    • 调用setPressed()/setSelected()等方法改变视图状态

    • 更新视图内部的mDrawableState状态数组

  2. Drawable响应

    • 触发drawableStateChanged()回调

    • StateListDrawable通过onStateChange()匹配新状态对应的Drawable资源

  3. 重绘调度

    • 调用invalidateSelf() → 触发View.invalidate()

    • 通过ViewParent链递归至ViewRootImpl

    • 通过scheduleTraversals()异步调度重绘

  4. 绘制执行

    • 下一帧触发performTraversals()

    • 根据标记位仅执行draw流程(measure/layout跳过)

    • 调用View.draw()Drawable.draw()渲染新状态对应的图片

性能优化要点

  • 优先使用invalidate(Rect)局部刷新

  • 复杂动画启用硬件加速(LAYER_TYPE_HARDWARE

  • 避免在draw()中创建对象

相关推荐
whysqwhw29 分钟前
安卓内存优化
android
用户2018792831671 小时前
TabLayout禁止滑动 + 左对齐排列实现
android
whysqwhw1 小时前
安卓Drawable分类
android
_祝你今天愉快2 小时前
SparseArray & ArrayMap
android·数据结构
2501_916007473 小时前
Charles中文版抓包工具使用指南 提高API调试和网络优化效率
android·ios·小程序·https·uni-app·iphone·webview
叽哥3 小时前
flutter学习第 6 节:按钮与交互组件
android·flutter·ios
用户2018792831673 小时前
为什么 Tab 文字默认会全大
android
用户2018792831673 小时前
Tablayout默认情况下,标签为什么会比文字宽?
android
用户2018792831673 小时前
如何设置可以让Tablayout中的tabview和tabview中的文字一样宽
android