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()中创建对象

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android