面试题 - Android UI 绘制相关 (一)

250 阅读35分钟

1. Android 补间动画和属性动画的区别

补间动画(Tween Animation)和属性动画(Property Animation)的主要区别:

  1. 本质区别
  • 补间动画: 只是改变 View 的显示效果,并不会真正改变 View 的属性
  • 属性动画: 通过直接修改控件的属性值实现动画效果
  1. 使用场景
  • 补间动画: 适用于简单的位移、缩放、旋转、透明度变化
  • 属性动画: 可以对任何对象的属性进行动画,不限于 View
  1. 点击事件
  • 补间动画: 执行动画后 View 的点击事件仍在原位置
  • 属性动画: View 的点击事件随着属性变化而变化

示例代码对比:

// 补间动画
val translateAnimation = TranslateAnimation(0f, 100f, 0f, 100f)
translateAnimation.duration = 1000
view.startAnimation(translateAnimation)

// 属性动画
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).apply {
    duration = 1000
    start()
}

2. Window 和 DecorView 的关系

  1. Window
  • Window 是一个抽象类,是所有视图的容器
  • PhoneWindow 是 Window 的唯一实现类
  • 每个 Activity 都会创建一个 Window 对象
  1. DecorView
  • DecorView 是整个 Window 的根 View
  • 包含了标题栏和内容栏(ContentView)
  • Activity 中通过 setContentView 设置的布局就是加载到 DecorView 的内容栏中

关系图:

Window(PhoneWindow)
    └── DecorView
        ├── TitleBar (可选)
        └── ContentView (setContentView)

建立联系的过程:

// 在 Activity 的 attach 方法中
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);

// 在 setContentView 中
mContentParent = generateLayout(mDecor); // 创建 DecorView
mContentParent.addView(view); // 添加内容视图

Window 和 DecorView 的关系及其底层原理:

一、基本概念形象类比

想象一下建房子的过程:

  • Window 就像是一块空地基(容器)
  • DecorView 就像是地基上的房子主体(根View)
  • ContentView 就像是房子里的装修(具体内容)

二、创建流程详解

// 1. Activity 创建时会初始化 PhoneWindow
class Activity {
    private lateinit var mWindow: Window
    
    final void attach(Context context) {
        // 创建 Window
        mWindow = PhoneWindow(this)
        mWindow.setCallback(this)
        // 设置 WindowManager
        mWindow.setWindowManager(null, mToken, mComponent.packageName, false)
    }
}

// 2. 设置内容时创建 DecorView
class PhoneWindow : Window() {
    private var mDecor: DecorView? = null
    
    override fun setContentView(layoutResID: Int) {
        // 确保 DecorView 已创建
        if (mDecor == null) {
            installDecor()
        }
        
        // 将内容添加到 DecorView 中
        mLayoutInflater.inflate(layoutResID, contentParent)
    }
    
    private fun installDecor() {
        mDecor = generateDecor()  // 创建 DecorView
        mContentParent = generateLayout(mDecor)  // 生成基础布局
    }
}

三、添加到 Window 的过程

// ActivityThread 中
final void handleResumeActivity(ActivityClientRecord r) {
    // 1. 执行 Activity 的 onResume
    r.activity.performResume()
    
    // 2. 将 DecorView 添加到 WindowManager
    ViewManager wm = a.getWindowManager()
    wm.addView(decor, l)  // 这里的 decor 就是 DecorView
}

// WindowManagerImpl 中
public void addView(View view, ViewGroup.LayoutParams params) {
    // 最终通过 WindowManagerGlobal 来添加视图
    WindowManagerGlobal.getInstance().addView(view, params, mContext.getDisplay())
}

四、流程图

graph TD
    A[Activity 创建] --> B[创建 PhoneWindow]
    B --> C[setContentView 调用]
    C --> D[创建 DecorView]
    D --> E[生成基础布局]
    E --> F[添加用户布局]
    F --> G[ActivityThread.handleResumeActivity]
    G --> H[WindowManager.addView]
    H --> I[WindowManagerGlobal.addView]
    I --> J[ViewRootImpl.setView]
    J --> K[建立 DecorView 和 Window 的连接]

五、核心类的职责

  1. Window(抽象类)
  • 管理窗口属性
  • 处理各种事件回调
  • 管理视图添加和删除
  1. PhoneWindow(具体实现)
  • 创建 DecorView
  • 管理各种特性(标题栏、进度条等)
  • 处理按键事件
  1. DecorView(视图层级根布局)
class DecorView : FrameLayout {
    // 标准布局
    |--DecorView
        |--StatusBar
        |--TitleBar (可选)
        |--ContentView (用户设置的布局)
        |--NavigationBar
}

六、关键点总结

  1. 创建时序
  • Activity 创建 → PhoneWindow 创建 → DecorView 创建 → 内容设置
  1. 职责分工
  • Window:窗口管理
  • DecorView:视图承载
  • WindowManager:窗口操作
  1. 通信机制
// Window 和 DecorView 通过 Callback 机制通信
window.setCallback(new Window.Callback() {
    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        // 事件分发
        return false
    }
})

七、使用示例

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 1. 设置窗口特性
        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
        
        // 2. 设置内容视图
        setContentView(R.layout.activity_main)
        
        // 3. 获取 DecorView
        val decorView = window.decorView
        
        // 4. 设置系统UI可见性
        decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
    }
}

这种实现方式的优势:

  1. 解耦了窗口管理和视图管理
  2. 提供了统一的窗口处理机制
  3. 方便实现各种窗口特性

通过这样的设计,Android 实现了灵活的窗口管理机制,使得应用可以方便地控制视图层级和窗口属性。

3. Android UI 刷新机制

Android 的 UI 刷新遵循以下机制:

  1. 垂直同步(VSync)
  • 16.6ms 发出一次 VSync 信号
  • 用于同步 CPU 计算和 GPU 渲染
  1. 刷新流程
触发重绘 → measure → layout → draw → swap buffers
  1. 主要方法
  • invalidate(): 请求重绘当前 View
  • requestLayout(): 请求重新布局
  1. 刷新原理
  • 通过 ViewRootImpl 的 scheduleTraversals() 调度
  • 使用 Choreographer 配合 VSync 进行同步

底层原理

一、基本概念

想象一个电影放映过程:

  1. VSync 信号 - 就像电影放映机的转动节奏
  2. 三级缓冲 - 就像准备好下一张胶片,等待当前胶片放映完
  3. 屏幕刷新率 - 一般是 60Hz,即每秒刷新 60 次,每次间隔约 16.6ms
二、核心角色
  1. Choreographer(编舞者)
class Choreographer {
    // 注册下一帧回调
    fun postFrameCallback(callback: FrameCallback) {
        // 等待下一个 VSync 信号
    }
    
    // 处理 VSync 信号
    private fun doFrame(frameTimeNanos: Long) {
        // 1. 处理输入事件
        // 2. 执行动画
        // 3. 执行绘制
    }
}
  1. ViewRootImpl(总指挥)
class ViewRootImpl {
    fun scheduleTraversals() {
        // 1. 设置同步屏障
        mTraversalBarrier = mHandler.postSyncBarrier()
        
        // 2. 请求下一帧回调
        Choreographer.getInstance().postCallback(
            Choreographer.CALLBACK_TRAVERSAL,
            mTraversalRunnable,
            null
        )
    }
}
三、刷新流程
  1. 触发刷新
// 可能的触发方式
view.invalidate()  // 请求重绘
view.requestLayout()  // 请求布局
  1. 调度过程
// ViewRootImpl 中
private fun performTraversals() {
    // 1. 测量 (Measure)
    performMeasure(...)
    
    // 2. 布局 (Layout)
    performLayout(...)
    
    // 3. 绘制 (Draw)
    performDraw(...)
}
四、底层实现
  1. Surface 机制
// 每个窗口都对应一个 Surface
class Surface {
    private var mNativeObject: Long = 0
    
    // 锁定 Canvas 进行绘制
    fun lockCanvas(): Canvas {
        // 获取后台缓冲区
        return nativeLockCanvas(...)
    }
    
    // 解锁并提交绘制内容
    fun unlockCanvasAndPost(canvas: Canvas) {
        // 提交到图形缓冲区
        nativeUnlockCanvasAndPost(...)
    }
}
  1. BufferQueue 机制
  • Producer(应用进程)生产图形缓冲区
  • Consumer(SurfaceFlinger)消费图形缓冲区
五、完整流程图
graph TD
    A[用户操作/程序逻辑] --> B[触发重绘/重布局]
    B --> C[ViewRootImpl.scheduleTraversals]
    C --> D[设置同步屏障]
    D --> E[注册 Choreographer 回调]
    E --> F[等待 VSync 信号]
    F --> G[performTraversals]
    G --> H[Measure - 测量]
    H --> I[Layout - 布局]
    I --> J[Draw - 绘制]
    J --> K[提交 Surface]
    K --> L[SurfaceFlinger 合成]
    L --> M[显示到屏幕]
    M --> N[移除同步屏障]
    N --> O[等待下一帧]
六、性能优化要点
  1. 避免丢帧
// 使用 Trace 监控性能
Trace.beginSection("performTraversals")
try {
    performTraversals()
} finally {
    Trace.endSection()
}
  1. 优化建议
  • 避免过度绘制
  • 扁平化视图层级
  • 使用硬件加速
  • 避免主线程耗时操作
七、监控方式
  1. Systrace
# 收集系统性能数据
python systrace.py -t 10 gfx view wm
  1. GPU Profile
// 开启 GPU 渲染监控
Settings.DEBUG_GPU_OVERDRAW = true
总结

核心流程:

  1. 触发刷新请求
  2. 设置同步屏障
  3. 等待 VSync 信号
  4. 执行 performTraversals
  5. 提交图形缓冲
  6. SurfaceFlinger 合成
  7. 显示到屏幕

优化重点:

  1. 保证每一帧的处理时间在 16ms 内
  2. 合理使用硬件加速
  3. 优化视图层级
  4. 避免主线程阻塞

通过这种方式,Android 实现了流畅的 UI 渲染,同时也为开发者提供了灵活的优化空间。

三级缓存原理

让我详细讲解下 Android 最新的三重缓冲(Triple Buffer)机制:

一、三重缓冲机制
graph LR
    A[后台缓冲区 Back Buffer] --> B[等待缓冲区 Middle Buffer]
    B --> C[显示缓冲区 Front Buffer]
    C --> D[显示设备 Display]
二、工作原理
  1. 三个缓冲区角色
class TripleBuffer {
    private val buffers = arrayOf(
        Buffer("Front"), // 当前显示的缓冲区
        Buffer("Middle"), // 等待显示的缓冲区
        Buffer("Back")  // 正在绘制的缓冲区
    )
}
  1. 缓冲区切换过程
while (true) {
    // 1. 应用进程在 Back Buffer 绘制
    backBuffer.draw()
    
    // 2. 等待 VSync 信号
    waitForVSync()
    
    // 3. 缓冲区轮转
    swapBuffers() {
        val temp = frontBuffer
        frontBuffer = middleBuffer
        middleBuffer = backBuffer
        backBuffer = temp
    }
}
三、优势分析
  1. 相比双缓冲的改进
双缓冲可能的问题:
┌────────────┐
│ 绘制帧1    │─┐
└────────────┘ │    ┌────────────┐
               ├─── │ 显示帧1    │
┌────────────┐ │    └────────────┘
│ 等待绘制帧2 │─┘
└────────────┘

三缓冲解决方案:
┌────────────┐
│ 绘制帧2    │─┐
└────────────┘ │    ┌────────────┐     ┌────────────┐
               ├─── │ 等待帧1    │ ─── │ 显示帧0    │
┌────────────┐ │    └────────────┘     └────────────┘
│ 准备缓冲   │─┘
└────────────┘
  1. 性能提升
  • 减少等待时间
  • 提高帧率稳定性
  • 降低画面撕裂风险
四、实现细节
  1. BufferQueue 实现
public class BufferQueue {
    // 生产者(应用进程)
    public void queueBuffer(Buffer buffer) {
        synchronized(mQueue) {
            mQueue.add(buffer)
            notify()
        }
    }
    
    // 消费者(SurfaceFlinger)
    public Buffer dequeueBuffer() {
        synchronized(mQueue) {
            while(mQueue.isEmpty()) {
                wait()
            }
            return mQueue.remove()
        }
    }
}
  1. VSync 同步
class DisplayEventReceiver {
    fun onVsync(timestampNanos: Long, physicalDisplayId: Long) {
        // 1. 处理缓冲区切换
        swapBuffers()
        
        // 2. 触发下一帧绘制
        scheduleNextFrame()
    }
}
五、完整流程图
graph TD
    A[应用进程] -->|绘制| B[Back Buffer]
    B -->|VSync| C[Middle Buffer]
    C -->|VSync| D[Front Buffer]
    D -->|显示| E[屏幕]
    
    F[VSync信号] -->|触发| G[缓冲区切换]
    G -->|循环| A
    
    H[SurfaceFlinger] -->|合成| I[图层混合]
    I -->|输出| E
六、性能监控
  1. 帧率监控
class FrameMetricsAggregator {
    fun addFrames(frameMetrics: FrameMetrics) {
        val duration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
        if (duration > 16_666_666) { // 16.6ms in nanos
            // 发现掉帧
            logFrameDrop()
        }
    }
}
  1. 缓冲区使用监控
class BufferQueueMonitor {
    fun onBufferQueued() {
        // 监控缓冲区使用情况
        val queuedBuffers = getQueuedBufferCount()
        if (queuedBuffers >= 3) {
            // 缓冲区已满,可能存在性能问题
            logBufferQueueFull()
        }
    }
}
七、优化建议
  1. CPU 优化
// 避免主线程耗时操作
launch(Dispatchers.Default) {
    // 耗时计算
    withContext(Dispatchers.Main) {
        // 更新 UI
    }
}
  1. GPU 优化
// 使用硬件加速
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)

// 避免过度绘制
view.setBackground(null) // 移除不必要的背景
  1. 内存优化
// 及时释放不需要的缓冲区
surface.release()
总结

三重缓冲的核心优势:

  1. 更好的性能表现
  2. 更流畅的画面表现
  3. 更低的延迟

使用注意事项:

  1. 内存占用增加
  2. 需要合理管理缓冲区资源
  3. 需要监控性能表现

通过三重缓冲机制,Android 系统能够更好地平衡性能和流畅度,为用户提供更好的视觉体验。

同步屏障机制

详细解释下同步屏障(Synchronization Barrier)机制:

一、基本概念

同步屏障就像是给消息队列设置的一个"路障",它的作用是:

  • 阻塞所有同步消息的执行
  • 优先处理异步消息
  • 确保 UI 绘制等高优先级任务能够及时执行
二、工作原理
class MessageQueue {
    // 消息分类
    class Message {
        var isAsynchronous: Boolean = false  // 是否异步消息
        var target: Handler? = null          // null 表示这是一个同步屏障
    }
    
    // 发送同步屏障
    fun postSyncBarrier(): Int {
        // 创建一个特殊的消息(target 为 null)
        val barrier = Message.obtain()
        barrier.target = null  // 标记为同步屏障
        
        // 将屏障消息插入队列
        synchronized(this) {
            enqueueMessage(barrier)
        }
        return barrier.arg1  // 返回屏障标记
    }
}
三、消息处理流程
graph TD
    A[消息队列] --> B{是否有同步屏障?}
    B -->|是| C{查找异步消息}
    B -->|否| D[按顺序处理消息]
    C -->|找到| E[处理异步消息]
    C -->|未找到| F[等待新消息]
四、实际应用示例
  1. UI 刷新中的应用
class ViewRootImpl {
    fun scheduleTraversals() {
        // 1. 设置同步屏障
        val token = mHandler.looper.queue.postSyncBarrier()
        
        try {
            // 2. 请求下一帧回调(异步消息)
            Choreographer.getInstance().postFrameCallback { 
                // 执行绘制
                doTraversal()
            }
        } finally {
            // 3. 移除同步屏障
            mHandler.looper.queue.removeSyncBarrier(token)
        }
    }
}
  1. 消息类型对比
// 同步消息(普通消息)
handler.post {
    // 可能被同步屏障阻塞
    updateUI()
}

// 异步消息
handler.postAtFrontOfQueue {
    // 不会被同步屏障阻塞
    doFrameUpdate()
}
五、实现细节
  1. 消息队列处理
class MessageQueue {
    fun next(): Message? {
        while (true) {
            // 1. 找到队列头部的消息
            val msg = messages
            
            // 2. 如果是同步屏障
            if (msg != null && msg.target == null) {
                // 寻找异步消息
                var asyncMsg = msg.next
                while (asyncMsg != null) {
                    if (asyncMsg.isAsynchronous) {
                        // 找到异步消息,优先处理
                        return asyncMsg
                    }
                    asyncMsg = asyncMsg.next
                }
            }
            
            // 3. 没有同步屏障,正常处理
            return msg
        }
    }
}
  1. 异步消息标记
val message = handler.obtainMessage().apply {
    what = MSG_DO_FRAME
    isAsynchronous = true  // 标记为异步消息
}
六、使用场景
  1. UI 渲染优化
// 在需要保证UI流畅性的场景
class CustomView {
    fun smoothScroll() {
        // 设置同步屏障
        val token = messageQueue.postSyncBarrier()
        
        try {
            // 执行滚动动画(异步消息)
            postAsyncAnimation()
        } finally {
            // 动画结束后移除屏障
            messageQueue.removeSyncBarrier(token)
        }
    }
}
  1. 关键任务优先级提升
class CriticalTaskHandler {
    fun handleCriticalTask() {
        // 设置同步屏障
        val token = messageQueue.postSyncBarrier()
        
        // 发送异步消息处理关键任务
        sendAsyncMessage {
            processCriticalTask()
            // 完成后移除屏障
            messageQueue.removeSyncBarrier(token)
        }
    }
}
七、注意事项
  1. 使用限制
// 必须成对使用
try {
    val token = messageQueue.postSyncBarrier()
    // 处理异步消息
} finally {
    // 一定要移除屏障
    messageQueue.removeSyncBarrier(token)
}
  1. 性能影响
// 不要滥用同步屏障
class PerformanceAwareHandler {
    private var barrierCount = 0
    
    fun postBarrier() {
        barrierCount++
        if (barrierCount > 3) {
            Log.w(TAG, "Too many barriers may affect performance")
        }
    }
}
总结

同步屏障的核心作用:

  1. 提高关键任务优先级
  2. 保证 UI 渲染流畅性
  3. 优化消息队列处理机制

使用建议:

  1. 谨慎使用,仅用于关键场景
  2. 确保正确移除屏障
  3. 合理使用异步消息

通过同步屏障机制,Android 系统能够更好地保证 UI 渲染等关键任务的及时执行,提供流畅的用户体验。

4. LinearLayout、FrameLayout 和 RelativeLayout 的效率比较

效率排序: FrameLayout > LinearLayout > RelativeLayout

原因分析:

  1. FrameLayout
  • 最简单的布局方式
  • 只需一次测量和布局
  • 子 View 直接叠加
  1. LinearLayout
  • 需要进行权重计算(如果使用 weight)
  • 按照方向逐个测量和布局
  1. RelativeLayout
  • 需要两次测量过程
  • 需要分析所有子 View 的依赖关系
  • 相对位置计算复杂

优化建议:

<!-- 避免嵌套过深 -->
<ConstraintLayout>  <!-- 使用 ConstraintLayout 替代复杂的相对布局 -->
    <FrameLayout/>  <!-- 简单叠加用 FrameLayout -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <!-- 线性排列用 LinearLayout -->
    </LinearLayout>
</ConstraintLayout>

ConstraintLayout的底层原理

让我为您详细讲解 ConstraintLayout 的底层原理:

一、基本工作原理

ConstraintLayout 的核心是一个约束求解引擎,名为 SolverVariable。整个过程类似于解决一个数学方程组:

// 简化的约束系统示例
class ConstraintSystem {
    // 1. 创建变量
    val leftVariable = SolverVariable()    // 左边界
    val rightVariable = SolverVariable()   // 右边界
    
    // 2. 添加约束条件
    fun addConstraint() {
        // 例如:A 的右边在 B 的左边,间距 10dp
        addEquation(A.right + 10 == B.left)
    }
    
    // 3. 求解
    fun solve() {
        // 使用 Cassowary 算法求解
    }
}
二、核心组件
  1. 约束解析器(Solver)
class ConstraintWidget {
    // 存储四个边界位置
    var left: Int = 0
    var top: Int = 0
    var right: Int = 0
    var bottom: Int = 0
    
    // 存储约束关系
    var leftConstraint: ConstraintAnchor? = null
    var rightConstraint: ConstraintAnchor? = null
    // ...
}
  1. 约束锚点(Anchor)
class ConstraintAnchor {
    var target: ConstraintAnchor? = null  // 目标锚点
    var margin: Int = 0                   // 边距
    var type: Type                        // 类型(LEFT/TOP/RIGHT/BOTTOM)
}
三、布局过程
  1. 测量阶段
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 1. 构建约束图
    mLayoutWidget.updateHierarchy()
    
    // 2. 确定尺寸模式
    val optimizationLevel = mOptimizationLevel
    
    // 3. 测量子视图
    measureChildren()
    
    // 4. 解析约束
    mLayoutWidget.layout()
}
  1. 布局阶段
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    // 应用解析结果
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        val widget = child.layoutParams as ConstraintLayout.LayoutParams
        
        child.layout(
            widget.left,
            widget.top,
            widget.right,
            widget.bottom
        )
    }
}
四、优化机制
  1. 直接解析(Direct Resolution)
  • 简单约束直接计算
  • 不需要求解器参与
  1. 批量解析(Batch Resolution)
  • 复杂约束组合处理
  • 使用完整求解器
五、流程图
graph TD
    A[开始布局] --> B[解析XML约束]
    B --> C[构建约束图]
    C --> D{是否是简单约束?}
    D -->|是| E[直接解析]
    D -->|否| F[构建方程组]
    F --> G[使用求解器]
    E --> H[确定位置]
    G --> H
    H --> I[应用到视图]
    I --> J[结束布局]
六、性能优化要点
  1. 约束优化
<!-- 使用 chains 替代多重约束 -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <View
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/next"/>
</androidx.constraintlayout.widget.ConstraintLayout>
  1. 维度优化
<!-- 使用 0dp (MATCH_CONSTRAINT) 配合约束 -->
<View
    android:layout_width="0dp"
    app:layout_constraintWidth_default="wrap"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>
  1. 层级优化
<!-- 使用 Group 和 Barrier 减少嵌套 -->
<androidx.constraintlayout.widget.Group
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:constraint_referenced_ids="id1,id2,id3"/>
七、工作流程总结
  1. 初始化阶段
  • 解析 XML 布局文件
  • 创建约束图
  • 初始化求解器
  1. 测量阶段
  • 收集所有约束
  • 构建约束方程组
  • 选择优化策略
  1. 求解阶段
  • 直接解析简单约束
  • 使用求解器处理复杂约束
  • 确定最终位置
  1. 应用阶段
  • 更新视图位置
  • 触发重绘
  • 完成布局

这种设计让 ConstraintLayout 能够:

  • 扁平化布局层级
  • 高效处理复杂约束
  • 提供灵活的布局能力
  • 保持较好的性能

通过这种方式,ConstraintLayout 在保持灵活性的同时,也能提供较好的性能表现。它的核心优势在于约束求解引擎的高效性,以及对各种布局场景的良好支持。

求解器(Solver)及 Cassowary 约束求解算法

让我详细解释 ConstraintLayout 中的求解器(Solver):

一、求解器概述

ConstraintLayout 使用的是 Cassowary 约束求解算法,这是一个用于解决线性等式和不等式系统的算法。

简单来说,就像解决这样的方程组:

方程组示例:
x + y = 100    (视图A的右边缘 + 视图B的左边缘 = 100dp)
y > 50         (视图B的左边缘必须大于50dp)
x < 40         (视图A的右边缘必须小于40dp)
二、核心组件
  1. 变量系统
class SolverVariable {
    var name: String        // 变量名称
    var type: Type         // 变量类型
    var computedValue: Float  // 计算值
    
    enum class Type {
        UNRESTRICTED,  // 无限制变量
        CONSTANT,      // 常量
        SLACK,         // 松弛变量
        ERROR,         // 错误变量
        UNKNOWN        // 未知类型
    }
}
  1. 约束方程
class ArrayRow {
    val variables: ArrayList<SolverVariable>  // 变量列表
    var constantValue: Float                  // 常量值
    
    // 添加变量到方程
    fun addVariable(variable: SolverVariable, coefficient: Float)
}
三、工作原理
  1. 创建约束
// 示例:设置视图A的右边缘到视图B的左边缘的约束
val equation = ArrayRow().apply {
    // A.right + margin = B.left
    addVariable(viewA.right, 1f)
    addVariable(viewB.left, -1f)
    constantValue = margin.toFloat()
}
  1. 优先级处理
class ConstraintWidget {
    // 设置约束优先级
    fun setHorizontalDimensionBehaviour(behaviour: DimensionBehaviour) {
        when (behaviour) {
            MATCH_CONSTRAINT -> priority = 3
            MATCH_PARENT -> priority = 2
            WRAP_CONTENT -> priority = 1
            FIXED -> priority = 0
        }
    }
}
四、求解过程
graph TD
    A[收集约束] --> B[创建方程组]
    B --> C[优化方程]
    C --> D[应用优先级]
    D --> E[迭代求解]
    E --> F[更新结果]
    F --> G[应用到视图]
五、实际应用示例
  1. 简单居中约束
// 伪代码表示
val centerConstraint = ArrayRow().apply {
    // view.left + view.width/2 = parent.width/2
    addVariable(view.left, 1f)
    addVariable(view.width, 0.5f)
    addVariable(parent.width, -0.5f)
    constantValue = 0f
}
  1. 链式约束(Chain)
// 处理水平链约束
class ChainHead {
    fun define() {
        // 创建头部约束
        val firstEquation = ArrayRow().apply {
            addVariable(firstView.left, 1f)
            constantValue = 0f
        }
        
        // 创建视图间约束
        views.zipWithNext { a, b ->
            val equation = ArrayRow().apply {
                addVariable(a.right, 1f)
                addVariable(b.left, -1f)
                constantValue = margin.toFloat()
            }
        }
    }
}
六、优化策略
  1. 直接求解
// 简单约束直接计算
if (isSimpleConstraint()) {
    widget.left = target.right + margin
    return true
}
  1. 批量求解
// 复杂约束使用完整求解器
class LinearSystem {
    fun solve() {
        // 1. 优化方程组
        optimize()
        
        // 2. 选择主元
        selectPivotVariable()
        
        // 3. 高斯消元
        solve()
        
        // 4. 更新结果
        updateVariables()
    }
}
七、性能考虑
  1. 缓存机制
class SolverCache {
    private val variableCache = mutableMapOf<String, SolverVariable>()
    private val equationCache = mutableMapOf<String, ArrayRow>()
    
    fun getCachedValue(key: String): Float? {
        return variableCache[key]?.computedValue
    }
}
  1. 增量更新
class IncrementalSolver {
    fun updateConstraint(constraint: Constraint) {
        // 只更新变化的约束
        if (hasChanged(constraint)) {
            solver.updateFromDelta(constraint)
        }
    }
}
八、实际应用建议
  1. 避免过度约束
<!-- 好的做法 -->
<View
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintWidth_default="wrap"/>

<!-- 避免的做法 -->
<View
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintWidth_percent="0.5"
    app:layout_constraintDimensionRatio="1:1"/>
  1. 使用辅助工具
<!-- 使用 Barrier 减少约束复杂度 -->
<androidx.constraintlayout.widget.Barrier
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="end"
    app:constraint_referenced_ids="id1,id2"/>

通过这个详细解释,我们可以看到求解器是 ConstraintLayout 的核心组件,它通过数学方法高效地解决复杂的布局约束问题。理解求解器的工作原理对于优化 ConstraintLayout 的使用非常重要。

5. Android 事件分发机制

事件分发的三个核心方法:

public boolean dispatchTouchEvent(MotionEvent ev)     // 分发
public boolean onInterceptTouchEvent(MotionEvent ev)  // 拦截
public boolean onTouchEvent(MotionEvent ev)          // 处理

分发流程:

Activity → Window → DecorView → ViewGroup → View

关键规则:

  1. 返回值的含义
  • true: 事件被消费,不继续传递
  • false: 事件未被消费,继续传递
  1. 传递顺序
dispatchTouchEvent() →
    onInterceptTouchEvent() →
        子View的dispatchTouchEvent() →
            onTouchEvent()

底层原理:

一、事件分发的整体流程

graph TD
    A[用户触摸屏幕] --> B[InputManager]
    B --> C[WindowManager]
    C --> D[ViewRootImpl]
    D --> E[DecorView]
    E --> F[Activity]
    F --> G[PhoneWindow]
    G --> H[ViewGroup]
    H --> I[View]

二、详细分发过程

  1. 事件产生
// 在 InputManagerService 中接收底层输入事件
class InputManagerService {
    private void processEventsLocked() {
        // 1. 收集触摸事件
        // 2. 寻找目标窗口
        // 3. 发送到目标窗口
    }
}
  1. 事件传递到 Activity
// Activity 接收事件
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 交给 Window 处理
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 都没处理则自己处理
    return onTouchEvent(ev);
}
  1. ViewGroup 的分发过程
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 1. 是否拦截
    if (onInterceptTouchEvent(ev)) {
        handled = onTouchEvent(ev);
    } else {
        // 2. 遍历子 View
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = children[i];
            // 3. 判断触摸点是否在子View内
            if (!canViewReceivePointerEvents(child) 
                || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
            // 4. 分发给子View
            if (dispatchTransformedTouchEvent(ev, child)) {
                handled = true;
                break;
            }
        }
    }
    return handled;
}

三、事件分发的三个重要方法

graph LR
    A[dispatchTouchEvent] --> B{onInterceptTouchEvent}
    B -->|true| C[onTouchEvent]
    B -->|false| D[子View的dispatchTouchEvent]
    D --> E[子View的onTouchEvent]

四、事件序列传递规则

// 事件序列示例
DOWN -> MOVE -> MOVE -> ... -> UP

// 规则:
1. DOWN 事件确定目标 View
2. 后续事件跟随 DOWN 事件的处理结果
3. 任何事件被消费,返回 true

五、实际案例分析

class CustomViewGroup : ViewGroup {
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        var handled = false
        
        // 1. 处理 DOWN 事件
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            // 清除之前的目标
            cancelAndClearTouchTargets()
        }
        
        // 2. 判断是否拦截
        val intercepted = onInterceptTouchEvent(ev)
        
        // 3. 如果不拦截,尝试分发给子View
        if (!intercepted) {
            handled = dispatchToChildren(ev)
        }
        
        // 4. 如果都没处理,自己处理
        if (!handled) {
            handled = onTouchEvent(ev)
        }
        
        return handled
    }
}

六、完整流程图

flowchart TD
    A[触摸事件] --> B{Activity.dispatchTouchEvent}
    B --> |true| C[事件结束]
    B --> |false| D{Window.superDispatchTouchEvent}
    D --> |true| C
    D --> |false| E{ViewGroup.dispatchTouchEvent}
    E --> |拦截| F[ViewGroup.onTouchEvent]
    E --> |不拦截| G{子View.dispatchTouchEvent}
    G --> |消费| C
    G --> |不消费| H{View.onTouchEvent}
    H --> |消费| C
    H --> |不消费| I[向上传递]
    I --> F

七、关键点总结

  1. 事件传递顺序
  • Activity -> Window -> DecorView -> ViewGroup -> View
  1. 事件分发三个核心方法
  • dispatchTouchEvent:分发事件
  • onInterceptTouchEvent:拦截事件
  • onTouchEvent:处理事件
  1. 返回值含义
  • true:事件被消费,停止传递
  • false:事件未被消费,继续传递
  1. 事件传递特点
  • 从上到下分发(dispatchTouchEvent)
  • 从下到上传递(onTouchEvent)
  • 中间可拦截(onInterceptTouchEvent)
  1. 实际应用建议
// 1. 处理滑动冲突
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> {
            mLastX = ev.x
            mLastY = ev.y
        }
        MotionEvent.ACTION_MOVE -> {
            val deltaX = ev.x - mLastX
            val deltaY = ev.y - mLastY
            // 根据滑动方向判断是否拦截
            if (abs(deltaX) > abs(deltaY)) {
                return true // 水平滑动,拦截
            }
        }
    }
    return super.onInterceptTouchEvent(ev)
}

// 2. 处理点击事件
override fun onTouchEvent(ev: MotionEvent): Boolean {
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> {
            // 记录按下位置
            return true
        }
        MotionEvent.ACTION_UP -> {
            // 处理点击事件
            performClick()
        }
    }
    return super.onTouchEvent(ev)
}

这就是 Android 事件分发机制的完整流程,通过理解这个机制,我们可以更好地处理复杂的触摸交互场景。

6. 自定义 View 的流程

基本流程:

  1. 构造方法
class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    init {
        // 初始化工作
    }
}
  1. 测量阶段(MeasureSpec)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)
    setMeasuredDimension(width, height)
}
  1. 布局阶段
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    // 确定位置
}
  1. 绘制阶段
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 绘制内容
}

底层原理

让我详细讲解自定义 View 的底层原理:

一、整体流程图

graph TD
    A[构造方法] --> B[onMeasure 测量]
    B --> C[onLayout 布局]
    C --> D[onDraw 绘制]
    
    B1[MeasureSpec] --> |决定测量模式和大小| B
    B --> B2[setMeasuredDimension]
    
    C1[父View布局] --> |决定位置| C
    C --> C2[确定四个顶点坐标]
    
    D1[背景绘制] --> D
    D --> D2[内容绘制]
    D --> D3[前景绘制]

二、详细原理解析

1. 构造方法阶段
class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    init {
        // 1. 获取自定义属性
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        // 2. 初始化画笔等工具
        paint = Paint().apply {
            color = typedArray.getColor(R.styleable.CustomView_custom_color, Color.BLACK)
        }
        // 3. 回收 TypedArray
        typedArray.recycle()
    }
}

工作原理:

  • 解析 XML 布局文件中的属性
  • 初始化绘制工具
  • 准备绘制相关的初始状态
2. 测量阶段 (onMeasure)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 1. 解析 MeasureSpec
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    
    // 2. 计算实际大小
    val resultWidth = when (widthMode) {
        MeasureSpec.EXACTLY -> widthSize // 精确值
        MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) // 不超过父容器
        else -> desiredWidth // 包裹内容
    }
    
    // 3. 保存测量结果
    setMeasuredDimension(resultWidth, resultHeight)
}

测量模式说明:

/**
 * EXACTLY: 精确值 (match_parent 或具体数值)
 * AT_MOST: 最大值 (wrap_content)
 * UNSPECIFIED: 不限制 (ScrollView 等滚动容器)
 */
3. 布局阶段 (onLayout)
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    
    // 1. 保存位置信息
    mLeft = left
    mTop = top
    mRight = right
    mBottom = bottom
    
    // 2. 计算实际显示区域
    mWidth = right - left
    mHeight = bottom - top
}

布局过程:

  1. ViewGroup 确定子 View 位置
  2. 子 View 保存自己的位置信息
  3. 计算实际显示区域
4. 绘制阶段 (onDraw)
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    
    // 1. 绘制背景
    if (background != null) {
        background.draw(canvas)
    }
    
    // 2. 绘制内容
    canvas.save()
    try {
        // 绘制自定义内容
        drawContent(canvas)
    } finally {
        canvas.restore()
    }
    
    // 3. 绘制前景
    if (foreground != null) {
        foreground.draw(canvas)
    }
}

绘制流程:

  1. 背景绘制(系统自动调用)
  2. 内容绘制(自定义实现)
  3. 前景绘制(系统自动调用)

三、实际应用示例

class CircleProgressView : View {
    // 1. 初始化工具
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var progress = 0f
    
    // 2. 测量阶段
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val size = 200.dp
        val width = resolveSize(size, widthMeasureSpec)
        val height = resolveSize(size, heightMeasureSpec)
        setMeasuredDimension(width, height)
    }
    
    // 3. 绘制阶段
    override fun onDraw(canvas: Canvas) {
        val center = width / 2f
        val radius = width / 2f - paint.strokeWidth / 2f
        
        // 绘制底圈
        paint.style = Paint.Style.STROKE
        paint.color = Color.GRAY
        canvas.drawCircle(center, center, radius, paint)
        
        // 绘制进度
        paint.color = Color.BLUE
        val sweepAngle = progress * 360
        canvas.drawArc(
            center - radius,
            center - radius,
            center + radius,
            center + radius,
            -90f,
            sweepAngle,
            false,
            paint
        )
    }
    
    // 4. 对外接口
    fun setProgress(value: Float) {
        progress = value
        invalidate() // 请求重绘
    }
}

四、性能优化建议

  1. 构造阶段
  • 避免在构造方法中进行耗时操作
  • 及时回收 TypedArray
  1. 测量阶段
  • 避免多次测量
  • 合理使用 MeasureSpec
  1. 布局阶段
  • 减少布局层级
  • 避免嵌套布局
  1. 绘制阶段
  • 使用 canvas.clipRect 减少过度绘制
  • 合理使用 canvas.save/restore
  • 避免在 onDraw 中创建对象

五、总结

自定义 View 的流程是一个完整的系统,从构造到最终显示,每个阶段都有其特定的职责:

  1. 构造方法:初始化准备
  2. onMeasure:确定大小
  3. onLayout:确定位置
  4. onDraw:绘制内容

理解这个流程对于开发高性能、体验好的自定义 View 至关重要。

7. RecyclerView 优化方法

主要优化方向:

  1. 布局优化
// 固定大小
recyclerView.setHasFixedSize(true)

// 关闭动画
recyclerView.itemAnimator = null
  1. 数据处理优化
// 差异化更新
val diffCallback = object : DiffUtil.Callback() {
    // 实现比较方法
}
DiffUtil.calculateDiff(diffCallback).dispatchUpdatesTo(adapter)
  1. 缓存优化
// 增加缓存池大小
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)
  1. 其他优化
  • 使用 ViewHolder 预处理
  • 数据预加载
  • 布局层级优化

让我详细讲解 RecyclerView 的优化原理:

一、RecyclerView 的核心工作原理

graph TD
    A[用户滑动] --> B[onScrolled触发]
    B --> C{是否需要新View}
    C -->|是| D[从缓存获取View]
    C -->|否| E[继续使用当前View]
    D --> F{四级缓存查找}
    F --> G[1.Scrap缓存]
    F --> H[2.Cache缓存]
    F --> I[3.ViewCacheExtension]
    F --> J[4.RecycledViewPool]
    G & H & I & J --> K[找到缓存]
    K --> L[绑定数据]
    L --> M[显示到屏幕]

二、主要优化方向及原理

1. 布局优化
// 固定大小优化
recyclerView.setHasFixedSize(true)

原理解析

  • 当 Item 的大小固定时,避免重新计算大小
  • 跳过 requestLayout 过程,减少布局计算
  • 提高滑动性能
graph LR
    A[普通模式] --> B[触发requestLayout] --> C[重新测量] --> D[重新布局]
    E[固定大小] --> F[跳过requestLayout] --> G[直接复用尺寸]
2. 数据处理优化 - DiffUtil
val diffCallback = object : DiffUtil.Callback() {
    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos].id == newList[newPos].id
    }
    
    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos] == newList[newPos]
    }
}

原理解析

graph TD
    A[新数据到达] --> B[DiffUtil比对]
    B --> C{是否相同Item}
    C -->|是| D{内容是否相同}
    C -->|否| E[整项更新]
    D -->|是| F[无需更新]
    D -->|否| G[局部更新]
3. 缓存机制优化

四级缓存工作流程:

graph TD
    A[需要新View] --> B{检查Scrap缓存}
    B -->|有| C[直接复用]
    B -->|无| D{检查Cache缓存}
    D -->|有| E[位置匹配复用]
    D -->|无| F{检查扩展缓存}
    F -->|有| G[自定义缓存复用]
    F -->|无| H{检查RecycledViewPool}
    H -->|有| I[类型匹配复用]
    H -->|无| J[创建新View]

优化代码示例:

// 1. 增加缓存池大小
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)

// 2. 自定义缓存扩展
recyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
    override fun getViewForPositionAndType(
        recycler: RecyclerView.Recycler,
        position: Int,
        type: Int
    ): View? {
        // 自定义缓存逻辑
        return customCache.get(position)
    }
})
4. 预加载优化
graph LR
    A[监听滑动] --> B[计算当前位置]
    B --> C{是否接近末尾}
    C -->|是| D[触发预加载]
    C -->|否| E[继续滑动]
    D --> F[异步加载数据]
    F --> G[准备好数据]

代码实现:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val lastPosition = layoutManager.findLastVisibleItemPosition()
        if (lastPosition >= adapter.itemCount - 5) {
            // 预加载下一页数据
            loadNextPage()
        }
    }
})
5. 性能优化总结
graph TD
    A[RecyclerView优化] --> B[布局优化]
    A --> C[数据优化]
    A --> D[缓存优化]
    A --> E[预加载优化]
    
    B --> B1[固定大小]
    B --> B2[降低层级]
    
    C --> C1[DiffUtil]
    C --> C2[局部刷新]
    
    D --> D1[增加缓存]
    D --> D2[自定义缓存]
    
    E --> E1[滑动监听]
    E --> E2[异步加载]

三、最佳实践建议

  1. 布局优化

    • 使用 setHasFixedSize(true)
    • 降低 Item 布局层级
    • 使用 ConstraintLayout
  2. 数据优化

    • 使用 DiffUtil 进行差异化更新
    • 避免全量刷新
    • 使用 notifyItemChanged 等局部刷新方法
  3. 缓存优化

    • 根据实际需求设置合适的缓存大小
    • 考虑使用自定义缓存扩展
    • 复用 ViewHolder
  4. 预加载优化

    • 实现滑动监听
    • 异步预加载数据
    • 使用分页加载

通过以上优化,可以显著提升 RecyclerView 的性能和用户体验。记住,优化是一个持续的过程,需要根据具体场景选择合适的优化策略。

四、Scrap缓存 Cache缓存 ViewCacheExtension RecycledViewPool

让我用生动的比喻来详细解释这四级缓存机制:

RecyclerView 的四级缓存机制
graph TD
    A[RecyclerView缓存系统] --> B[一级: Scrap缓存]
    A --> C[二级: Cache缓存]
    A --> D[三级: ViewCacheExtension]
    A --> E[四级: RecycledViewPool]
1. Scrap缓存(一级缓存)

生活比喻: 想象你正在看书,暂时合上书准备喝口水。这本书你并没有放回书架,而是直接放在手边的桌子上,随时可以继续阅读。

// Scrap缓存的工作原理
class Recycler {
    // 暂时分离的ViewHolder列表
    private val mAttachedScrap = ArrayList<ViewHolder>()
    
    // 预布局状态的ViewHolder
    private val mChangedScrap = ArrayList<ViewHolder>()
}

特点

  • 最快的缓存
  • 存储当前屏幕上的ViewHolder
  • 数据不会被清除
  • 生命周期最短
graph LR
    A[屏幕内Item] --> B[临时离开屏幕]
    B --> C[放入Scrap缓存]
    C --> D[快速复用]
2. Cache缓存(二级缓存)

生活比喻: 类似于你的办公桌抽屉,虽然不在桌面上,但是伸手就能拿到。存放着最近用过的文件,方便快速取用。

// Cache缓存的实现
class Recycler {
    // 默认大小为2,可配置
    private val mCachedViews = ArrayList<ViewHolder>()
    
    fun setCacheSize(size: Int) {
        mViewCacheMax = size
    }
}

特点

  • 默认大小为2个位置
  • 保存最近滑出屏幕的ViewHolder
  • 位置信息保留
  • 可以直接复用不需要重新绑定数据
graph TD
    A[Item滑出屏幕] --> B{Cache是否已满?}
    B -->|是| C[移除最老的到Pool]
    B -->|否| D[放入Cache]
    D --> E[保留位置信息]
    E --> F[快速复用]
3. ViewCacheExtension(三级缓存)

生活比喻: 就像是你的私人助理,可以按照你的特殊需求来存取文件,完全自定义的管理方式。

class CustomViewCacheExtension : RecyclerView.ViewCacheExtension() {
    // 自定义缓存Map
    private val customCache = mutableMapOf<Int, View>()
    
    override fun getViewForPositionAndType(
        recycler: RecyclerView.Recycler,
        position: Int,
        type: Int
    ): View? {
        // 自定义缓存逻辑
        return customCache[position]
    }
}

特点

  • 完全自定义的缓存策略
  • 开发者自己负责管理View
  • 可以实现特殊的缓存需求
graph LR
    A[需要View] --> B[检查自定义缓存]
    B --> C{是否命中?}
    C -->|是| D[返回缓存View]
    C -->|否| E[继续下一级缓存]
4. RecycledViewPool(四级缓存)

生活比喻: 像是一个大型文件柜,存放着按照类型分类的文件。虽然取出来需要重新整理内容,但是省去了创建新文件夹的时间。

class RecyclerView {
    // ViewPool的实现
    class RecycledViewPool {
        // 按ViewType分组存储
        private val mScrap = SparseArray<ScrapData>()
        
        // 设置某个类型的最大缓存数
        fun setMaxRecycledViews(viewType: Int, max: Int) {
            val scrapData = getScrapDataForType(viewType)
            scrapData.mMaxScrap = max
        }
    }
}

特点

  • 根据ViewType分类存储
  • 可以多个RecyclerView共享
  • 需要重新绑定数据
  • 缓存数量最大
graph TD
    A[ViewHolder进入Pool] --> B[按ViewType分类]
    B --> C[存入对应队列]
    D[需要新ViewHolder] --> E[查找对应Type]
    E --> F[取出并重新绑定数据]
缓存优化实战示例
class OptimizedRecyclerView : RecyclerView {
    init {
        // 1. 增加Cache缓存大小
        setItemViewCacheSize(20)
        
        // 2. 配置RecycledViewPool
        recycledViewPool.setMaxRecycledViews(0, 30)
        
        // 3. 自定义ViewCacheExtension
        setViewCacheExtension(object : ViewCacheExtension() {
            private val specialCache = mutableMapOf<Int, View>()
            
            override fun getViewForPositionAndType(
                recycler: Recycler,
                position: Int,
                type: Int
            ): View? {
                // 特殊位置使用自定义缓存
                return if (position % 10 == 0) specialCache[position] else null
            }
        })
    }
}
缓存查找流程
graph TD
    A[需要新View] --> B[检查Scrap缓存]
    B -->|未找到| C[检查Cache缓存]
    C -->|未找到| D[检查ViewCacheExtension]
    D -->|未找到| E[检查RecycledViewPool]
    E -->|未找到| F[创建新View]
    
    B -->|找到| G[直接复用]
    C -->|找到| G
    D -->|找到| G
    E -->|找到| H[重新绑定数据]

通过合理使用这四级缓存机制,可以显著提升RecyclerView的性能。关键是要理解每一级缓存的特点和适用场景,根据实际需求进行优化配置。

8. ListView 优化方法

主要优化方向:

  1. ViewHolder 模式
private class ViewHolder {
    var textView: TextView? = null
    var imageView: ImageView? = null
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    var holder: ViewHolder
    var view = convertView
    
    if (view == null) {
        view = inflater.inflate(R.layout.list_item, null)
        holder = ViewHolder()
        holder.textView = view.findViewById(R.id.text)
        holder.imageView = view.findViewById(R.id.image)
        view.tag = holder
    } else {
        holder = view.tag as ViewHolder
    }
    // 绑定数据
    return view
}
  1. 其他优化方案
  • 异步加载图片
  • 减少 item 布局层级
  • 数据分页加载
  • 滑动时停止加载

底层原理

让我详细讲解 ListView 优化的底层原理:

1. ListView 的工作原理

基本工作流程:

graph TD
A[ListView创建] --> B[getView调用]
B --> C{是否有可复用的View?}
C -->|否| D[inflate新View]
C -->|是| E[复用convertView]
D --> F[绑定数据]
E --> F
F --> G[显示到屏幕]

2. 主要性能问题及解决方案

2.1 布局加载问题
graph LR
A[传统方式] --> B[每次getView都inflate]
B --> C[性能损耗大]

D[优化方式] --> E[使用convertView复用]
E --> F[性能提升显著]

代码示例:

// 优化前
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    // 每次都inflate,性能差
    val view = inflater.inflate(R.layout.list_item, null)
    val textView = view.findViewById<TextView>(R.id.text)
    textView.text = data[position]
    return view
}

// 优化后
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    // 复用convertView,性能好
    val view = convertView ?: inflater.inflate(R.layout.list_item, parent, false)
    val textView = view.findViewById<TextView>(R.id.text)
    textView.text = data[position]
    return view
}
2.2 findViewById 问题
graph TD
A[传统方式] --> B[每次getView都findViewById]
B --> C[大量反射操作]
C --> D[性能损耗]

E[优化方式] --> F[使用ViewHolder缓存View引用]
F --> G[避免重复findViewById]
G --> H[性能提升]

ViewHolder 模式实现:

class ViewHolder {
    var textView: TextView? = null
    var imageView: ImageView? = null
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val holder: ViewHolder
    val view: View
    
    if (convertView == null) {
        view = inflater.inflate(R.layout.list_item, parent, false)
        holder = ViewHolder().apply {
            textView = view.findViewById(R.id.text)
            imageView = view.findViewById(R.id.image)
        }
        view.tag = holder
    } else {
        view = convertView
        holder = view.tag as ViewHolder
    }
    
    // 直接使用holder中缓存的View
    holder.textView?.text = data[position]
    holder.imageView?.setImageResource(images[position])
    
    return view
}

3. 图片加载优化

graph TD
A[滑动状态检测] --> B{是否快速滑动?}
B -->|是| C[暂停图片加载]
B -->|否| D[加载图片]
D --> E{是否已缓存?}
E -->|是| F[从缓存加载]
E -->|否| G[异步加载]

实现示例:

class OptimizedListView : ListView {
    private var isScrolling = false
    
    override fun onScrollStateChanged(state: Int) {
        super.onScrollStateChanged(state)
        isScrolling = when (state) {
            SCROLL_STATE_IDLE -> false
            SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING -> true
            else -> false
        }
    }
}

// 适配器中使用
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    // ... ViewHolder 相关代码 ...
    
    if (!isScrolling) {
        // 使用图片加载框架异步加载
        Glide.with(context)
            .load(imageUrl)
            .into(holder.imageView)
    } else {
        // 滑动时显示占位图
        holder.imageView.setImageResource(R.drawable.placeholder)
    }
    
    return view
}

4. 完整优化流程图

graph TD
A[ListView优化] --> B[布局优化]
A --> C[View复用]
A --> D[图片加载]
A --> E[滑动优化]

B --> B1[使用ViewHolder]
B --> B2[减少布局层级]
B --> B3[避免复杂布局]

C --> C1[复用convertView]
C --> C2[缓存findViewById结果]

D --> D1[异步加载]
D --> D2[图片缓存]
D --> D3[滑动时暂停加载]

E --> E1[分页加载]
E --> E2[滑动监听]
E --> E3[数据预加载]

5. 优化效果总结

  1. 内存优化
  • 减少View对象创建
  • 减少GC频率
  • 降低内存占用
  1. 性能优化
  • 减少布局渲染时间
  • 减少主线程工作量
  • 提升滑动流畅度
  1. 用户体验
  • 更流畅的滑动
  • 更快的加载速度
  • 更少的卡顿现象

通过以上优化,ListView 的性能可以得到显著提升,特别是在大量数据展示时,优化效果更为明显。关键是要根据具体场景选择合适的优化策略组合使用。

9. 自定义 RecyclerView.LayoutManager 流程

核心步骤:

  1. 继承 LayoutManager
class CustomLayoutManager : RecyclerView.LayoutManager() {
    
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
    }
}
  1. 实现布局逻辑
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
    // 移除所有附加的View
    detachAndScrapAttachedViews(recycler)
    
    // 布局逻辑
    var offsetY = 0
    for (i in 0 until itemCount) {
        val view = recycler.getViewForPosition(i)
        addView(view)
        measureChildWithMargins(view, 0, 0)
        
        val width = getDecoratedMeasuredWidth(view)
        val height = getDecoratedMeasuredHeight(view)
        
        layoutDecorated(view, 0, offsetY, width, offsetY + height)
        offsetY += height
    }
}

底层原理

一、核心工作流程

graph TD
A[onLayoutChildren开始布局] --> B[预布局阶段]
B --> C[布局阶段]
C --> D[填充布局]
D --> E[回收超出范围的View]

F[滑动处理] --> G[计算滑动距离]
G --> H[移动子View]
H --> I[填充新的View]
I --> J[回收不可见View]

二、详细实现原理

  1. 初始化阶段
class CustomLayoutManager : RecyclerView.LayoutManager() {
    
    // 是否支持水平滚动
    override fun canScrollHorizontally() = true
    
    // 是否支持垂直滚动
    override fun canScrollVertically() = true
    
    // 生成默认布局参数
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
    }
}
  1. 布局阶段
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
    // 1. 分离并缓存所有View
    detachAndScrapAttachedViews(recycler)
    
    // 2. 计算可见区域
    val viewWidth = width - paddingLeft - paddingRight
    val viewHeight = height - paddingTop - paddingBottom
    
    // 3. 填充布局
    var offsetY = 0
    for (i in 0 until itemCount) {
        // 获取 View
        val view = recycler.getViewForPosition(i)
        // 添加到 RecyclerView
        addView(view)
        // 测量 View
        measureChildWithMargins(view, 0, 0)
        // 布局 View
        layoutDecorated(
            view,
            paddingLeft,
            offsetY + paddingTop,
            paddingLeft + viewWidth,
            offsetY + getDecoratedMeasuredHeight(view) + paddingTop
        )
        // 更新偏移量
        offsetY += getDecoratedMeasuredHeight(view)
    }
}
  1. 滑动处理
override fun scrollVerticallyBy(
    dy: Int,
    recycler: RecyclerView.Recycler,
    state: RecyclerView.State
): Int {
    // 1. 移动所有子View
    offsetChildrenVertical(-dy)
    
    // 2. 填充新的View
    fill(recycler, state)
    
    // 3. 回收不可见的View
    recycleViews(recycler)
    
    return dy
}

private fun fill(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
    // 填充顶部空白
    fillTop(recycler)
    // 填充底部空白
    fillBottom(recycler)
}

private fun recycleViews(recycler: RecyclerView.Recycler) {
    // 获取可见区域
    val displayRect = Rect(
        0, scrollY,
        width, scrollY + height
    )
    
    // 遍历所有子View
    for (i in 0 until childCount) {
        val child = getChildAt(i) ?: continue
        // 如果不在可见区域,则回收
        if (!Rect().apply { 
            getDecoratedBoundsWithMargins(child, this)
        }.intersect(displayRect)) {
            removeAndRecycleView(child, recycler)
        }
    }
}

三、核心机制

  1. View 复用机制
graph LR
A[Scrap缓存] --> B[Cache缓存]
B --> C[ViewCacheExtension]
C --> D[RecycledViewPool]
  1. 布局过程
graph TD
A[测量阶段] --> B[布局阶段]
B --> C[绘制阶段]
C --> D[回收阶段]
  1. 滑动处理流程
graph TD
A[触摸事件] --> B[计算滑动距离]
B --> C[移动现有View]
C --> D[填充新View]
D --> E[回收旧View]

四、关键要点

  1. View 的获取与回收
  • getViewForPosition(): 获取 View
  • recycleView(): 回收 View
  • detachAndScrapAttachedViews(): 分离所有 View
  1. 布局相关方法
  • measureChildWithMargins(): 测量子 View
  • layoutDecorated(): 布局子 View
  • getDecoratedMeasuredWidth/Height(): 获取实际尺寸
  1. 滑动处理
  • scrollHorizontallyBy()/scrollVerticallyBy(): 处理滑动
  • offsetChildrenHorizontal()/offsetChildrenVertical(): 移动子 View

五、优化建议

  1. 性能优化
// 1. 预加载
private val preloadItemCount = 3

// 2. 复用优化
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)

// 3. 局部刷新
notifyItemChanged(position)
  1. 内存优化
// 及时回收不可见 View
private fun recycleInvisibleViews(recycler: RecyclerView.Recycler) {
    val displayRect = Rect(...)
    for (i in 0 until childCount) {
        val child = getChildAt(i) ?: continue
        if (!Rect().apply { 
            getDecoratedBoundsWithMargins(child, this)
        }.intersect(displayRect)) {
            removeAndRecycleView(child, recycler)
        }
    }
}

六、总结流程图

graph TD
    A[LayoutManager初始化] --> B[onLayoutChildren]
    B --> C{是否首次布局?}
    C -->|是| D[detachAndScrapAttachedViews]
    C -->|否| E[计算可见区域]
    D --> E
    E --> F[填充布局fill]
    F --> G[测量子View]
    G --> H[布局子View]
    H --> I[处理滑动]
    I --> J[移动现有View]
    J --> K[填充新View]
    K --> L[回收不可见View]
    L --> M{是否继续滑动?}
    M -->|是| I
    M -->|否| N[结束]

这个流程完整展示了 LayoutManager 的工作原理,从初始化到布局再到滑动处理的整个生命周期。理解这个流程对于自定义 LayoutManager 非常重要。

10. RemoteViews 使用场景

RemoteViews 主要用于:

  1. 通知栏界面
val remoteViews = RemoteViews(packageName, R.layout.notification_layout)
remoteViews.setTextViewText(R.id.title, "标题")
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon)

val notification = NotificationCompat.Builder(context, channelId)
    .setCustomContentView(remoteViews)
    .build()
  1. 桌面小部件
class MyAppWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        val remoteViews = RemoteViews(context.packageName, R.layout.widget_layout)
        // 更新界面
        appWidgetManager.updateAppWidget(appWidgetIds, remoteViews)
    }
}

RemoteViews 底层原理详解

1. 基本概念

RemoteViews 是一种特殊的 View 容器,用于在其他进程中显示和更新 UI。主要应用在:

  • 通知栏(Notification)
  • 桌面小部件(AppWidget)
2. 工作原理详解
  1. 跨进程通信机制
// 1. 创建 RemoteViews
val remoteViews = RemoteViews(packageName, R.layout.widget_layout)

// 2. 设置操作
remoteViews.setTextViewText(R.id.text, "更新文本")
remoteViews.setImageViewResource(R.id.image, R.drawable.icon)

这些操作会被转换为 Action 对象:

// 内部实现原理
class RemoteViews implements Parcelable {
    private ArrayList<Action> mActions;
    
    // Action 基类
    private abstract static class Action implements Parcelable {
        public abstract void apply(View root);
    }
    
    // 具体 Action 实现
    private class SetTextViewText extends Action {
        final int viewId;
        final CharSequence text;
        
        public void apply(View root) {
            TextView target = root.findViewById(viewId);
            target.setText(text);
        }
    }
}
3. 执行流程
  1. 序列化过程
// 系统将 RemoteViews 对象序列化
val parcel = Parcel.obtain()
remoteViews.writeToParcel(parcel, 0)
  1. 跨进程传输
// 通过 Binder 机制传输到目标进程
// 例如:通知栏进程 SystemUI
NotificationManager.notify(notification)
  1. 重建和应用
// 目标进程中
class RemoteViewsAdapter {
    fun onReceive(remoteViews: RemoteViews) {
        // 1. 解析布局
        val view = inflateView()
        
        // 2. 执行所有 Action
        for (action in remoteViews.actions) {
            action.apply(view)
        }
    }
}
4. 流程图
graph TD
    A[应用进程] --> B[创建 RemoteViews]
    B --> C[添加更新操作<br/>转换为 Action 列表]
    C --> D[序列化 RemoteViews<br/>和 Action 列表]
    D --> E[通过 Binder 跨进程传输]
    E --> F[目标进程<br/>SystemUI/桌面]
    F --> G[反序列化 RemoteViews]
    G --> H[解析布局文件]
    H --> I[执行 Action 列表]
    I --> J[更新界面]
5. 重要限制
  1. 支持的 View 类型有限
// 仅支持基础 View
- TextView
- ImageView
- Button
- ProgressBar
- ViewFlipper
- ListView
- GridView
- StackView
- AdapterViewFlipper
- ViewStub
  1. 支持的操作有限
// 常用操作示例
remoteViews.apply {
    setTextViewText()    // 设置文本
    setTextColor()       // 设置颜色
    setImageViewResource() // 设置图片
    setViewVisibility()  // 设置可见性
    setOnClickPendingIntent() // 设置点击事件
}
6. 性能优化建议
  1. 减少更新频率
// 批量更新
remoteViews.apply {
    setTextViewText(R.id.text1, "文本1")
    setTextViewText(R.id.text2, "文本2")
    // 一次性提交多个更新
}
  1. 优化布局层级
<!-- 保持布局简单扁平 -->
<LinearLayout>
    <TextView/>
    <ImageView/>
    <!-- 避免复杂嵌套 -->
</LinearLayout>
  1. 合理使用缓存
// 缓存 RemoteViews 实例
private var cachedRemoteViews: RemoteViews? = null

fun updateWidget() {
    if (cachedRemoteViews == null) {
        cachedRemoteViews = RemoteViews(...)
    }
    // 复用已有实例进行更新
}
总结要点:
  1. RemoteViews 本质是一个跨进程的 View 更新方案
  2. 通过 Action 序列化机制实现界面更新
  3. 使用 Binder 机制进行跨进程通信
  4. 在目标进程中重建视图并执行更新
  5. 有限的 View 类型和操作支持
  6. 需要注意性能优化和资源消耗

这种机制保证了在不同进程中可以安全地更新 UI,同时也带来了一定的使用限制和性能开销。在实际应用中,需要根据具体场景合理使用并做好优化。

11. 获取 View 宽高的几种方法

  1. onWindowFocusChanged
override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    if (hasFocus) {
        val width = view.width
        val height = view.height
    }
}
  1. ViewTreeObserver
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
    override fun onGlobalLayout() {
        view.viewTreeObserver.removeOnGlobalLayoutListener(this)
        val width = view.width
        val height = view.height
    }
})
  1. post 方法
view.post {
    val width = view.width
    val height = view.height
}
  1. layout 监听
view.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
    val width = right - left
    val height = bottom - top
}

底层原理

让我详细讲解获取 View 宽高的几种方法的底层原理:

1. View 测量流程图
graph TD
    A[Activity Created] --> B[setContentView]
    B --> C[ViewRootImpl.performTraversals]
    C --> D[measure]
    D --> E[layout]
    E --> F[draw]
    F --> G[View完全被绘制]
2. 各种获取方法的原理
2.1 View.post()
view.post {
    val width = view.width
    val height = view.height
}

工作原理:

graph TD
    A[调用view.post] --> B{检查是否有AttachInfo}
    B -->|有| C[直接通过Handler发送消息]
    B -->|没有| D[加入HandlerAction队列]
    D --> E[等待View被添加到Window]
    E --> F[dispatchAttachedToWindow]
    F --> G[执行HandlerAction队列]
    G --> H[获取宽高]
2.2 ViewTreeObserver.addOnGlobalLayoutListener
view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
    override fun onGlobalLayout() {
        val width = view.width
        val height = view.height
    }
})

工作原理:

graph TD
    A[注册监听器] --> B[ViewRootImpl.performTraversals]
    B --> C[performLayout]
    C --> D[触发onGlobalLayout]
    D --> E[获取宽高]
2.3 Activity.onWindowFocusChanged
override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    if (hasFocus) {
        val width = view.width
        val height = view.height
    }
}

工作原理:

graph TD
    A[Window获得焦点] --> B[DecorView.onWindowFocusChanged]
    B --> C[Activity.onWindowFocusChanged]
    C --> D[此时View已完成测量]
    D --> E[获取宽高]
3. 详细解释
  1. 为什么直接获取宽高会为0?
// 在 onCreate 中直接获取
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    // 此时为0,因为还没有完成测量
    println("width: ${view.width}, height: ${view.height}")
}

原因:

  • View 的测量是在 setContentView 之后异步进行的
  • 测量流程由 ViewRootImpl 的 performTraversals 触发
  • 需要等待 View 被添加到 Window 并完成测量
  1. 测量流程的关键节点
class ViewRootImpl {
    private fun performTraversals() {
        // 1. 执行测量
        performMeasure(...)
        
        // 2. 执行布局
        performLayout(...)
        
        // 3. 执行绘制
        performDraw(...)
    }
}
  1. View.post 的工作机制
public boolean post(Runnable action) {
    // 1. 获取 AttachInfo
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 2.1 已添加到窗口,直接发送消息
        return attachInfo.mHandler.post(action);
    }
    // 2.2 未添加到窗口,加入队列等待
    getRunQueue().post(action);
    return true;
}
4. 总结流程图
graph TB
    A[View创建] --> B[测量流程开始]
    B --> C{是否已添加到Window}
    C -->|是| D[执行测量]
    C -->|否| E[等待添加到Window]
    D --> F[执行布局]
    F --> G[执行绘制]
    E --> D
    
    H[获取宽高方式] --> I[View.post]
    H --> J[ViewTreeObserver]
    H --> K[onWindowFocusChanged]
    
    I --> L[等待测量完成]
    J --> L
    K --> L
    
    L --> M[成功获取宽高]
5. 最佳实践建议
  1. 优先使用 ViewTreeObserver
  • 可以及时移除监听
  • 时机最准确
  1. View.post 最简便
  • 代码简洁
  • 自动处理生命周期
  1. onWindowFocusChanged 适合特殊场景
  • 需要处理窗口焦点变化
  • 时机相对较晚
// 推荐的写法
view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
    override fun onGlobalLayout() {
        view.viewTreeObserver.removeOnGlobalLayoutListener(this)
        val width = view.width
        val height = view.height
        // 使用宽高
    }
})

这样的实现既保证了获取到正确的宽高,又避免了内存泄漏的风险。

12. View.post() 原理

View.post() 的工作原理:

  1. 消息机制
  • 将 Runnable 加入到消息队列
  • 依附于 ViewRootImpl 的 Handler
  1. 执行时机
// 简化的流程
if (mAttachInfo != null) {
    // View已经被添加到窗口
    mAttachInfo.mHandler.post(action);
} else {
    // View还未添加到窗口
    getRunQueue().post(action);
}
  1. 保证执行
  • 如果 View 还未附加到窗口,会将任务保存到队列
  • 在 dispatchAttachedToWindow 时执行队列中的任务

底层原理

让我详细讲解 View.post() 的底层原理:

1. 基本使用

view.post {
    // 获取宽高
    val width = view.width
    val height = view.height
}

2. 源码分析

View.java 中的实现:

public boolean post(Runnable action) {
    // 获取 AttachInfo
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果已经 attach 到窗口,直接通过 Handler 发送
        return attachInfo.mHandler.post(action);
    }
    // 还没 attach,先保存到 HandlerActionQueue
    getRunQueue().post(action);
    return true;
}

3. 两种情况分析

情况一:View 已经 attach 到窗口

// 1. AttachInfo 中的 Handler 实际上是 ViewRootImpl 的 Handler
// 2. 最终通过主线程 Handler 执行 Runnable
attachInfo.mHandler.post(action);

情况二:View 还未 attach 到窗口

// 1. 将 Runnable 存入 HandlerActionQueue
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

// 2. 在 View 被 attach 到窗口时执行
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // ...
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // ...
}

4. 完整流程图

graph TD
    A[View.post调用] --> B{是否已attach到窗口?}
    B -->|是| C[通过ViewRootImpl的Handler执行]
    B -->|否| D[存入HandlerActionQueue]
    D --> E[等待View被attach]
    E --> F[dispatchAttachedToWindow]
    F --> G[执行HandlerActionQueue中的任务]
    C --> H[在主线程执行Runnable]
    G --> H

5. 关键知识点总结

  1. 两种执行路径

    • 直接通过 Handler 执行
    • 先存队列,等 attach 后执行
  2. Handler 来源

    • ViewRootImpl 中创建
    • 确保在主线程执行
  3. HandlerActionQueue 作用

    • 临时存储 Runnable
    • 保证任务不丢失
  4. 为什么能获取宽高

    • View 测量完成后才会 attach
    • 执行时已完成测量布局

6. 实际应用示例

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    init {
        // 1. 构造时立即post
        post {
            println("宽度:$width") // 正确值
        }
        
        // 2. 直接获取宽度
        println("宽度:$width") // 0
    }
    
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // 3. attach时获取宽度
        println("宽度:$width") // 正确值
    }
}

7. 注意事项

  1. 内存泄漏风险
// 错误示例
view.post { 
    // 持有外部类引用
    someOuterVariable.doSomething()
}

// 正确示例
val runnable = WeakReference(Runnable { 
    // 使用弱引用
})
view.post(runnable.get())
  1. 生命周期问题
// 建议在合适的生命周期调用
override fun onResume() {
    super.onResume()
    view.post { /* 安全的操作 */ }
}
  1. 执行时机
// 确保在主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
    view.post { /* UI操作 */ }
}

通过这种机制,View.post() 巧妙地解决了在 View 未完成测量时获取宽高的问题,同时保证了任务一定会被执行,是 Android 中常用的异步处理机制之一。

13. 属性动画的插值器和估值器

  1. 插值器(Interpolator)
  • 决定动画变化的速率

常用插值器:

// 线性插值器
val animator = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
    interpolator = LinearInterpolator()
}

// 加速减速插值器
animator.interpolator = AccelerateDecelerateInterpolator()

// 自定义插值器
class CustomInterpolator : TimeInterpolator {
    override fun getInterpolation(input: Float): Float {
        return input * input  // 示例:二次方加速
    }
}
  1. 估值器(TypeEvaluator)
  • 计算属性值的具体变化数值
// 自定义估值器示例
class ColorEvaluator : TypeEvaluator<Int> {
    override fun evaluate(fraction: Float, startValue: Int, endValue: Int): Int {
        val startA = (startValue shr 24) and 0xff
        val startR = (startValue shr 16) and 0xff
        // ... 计算颜色变化
        return Color.argb(a, r, g, b)
    }
}

14. getDimension 相关方法区别

  1. getDimension
  • 返回 float
  • 保留小数点
  • 将 dp/sp 转换为 px
  1. getDimensionPixelOffset
  • 返回 int
  • 向下取整
  • 将 dp/sp 转换为 px
  1. getDimensionPixelSize
  • 返回 int
  • 四舍五入
  • 将 dp/sp 转换为 px
val resources = context.resources
// 假设 dimen.xml 中定义了 <dimen name="test">10.6dp</dimen>
val dim = resources.getDimension(R.dimen.test)           // 返回 10.6
val offset = resources.getDimensionPixelOffset(R.dimen.test)  // 返回 10
val size = resources.getDimensionPixelSize(R.dimen.test)     // 返回 11

15. StaticLayout 的用法和应用场景

StaticLayout 主要用于文字的静态布局:

// 创建 StaticLayout
val staticLayout = StaticLayout.Builder.obtain(
    text,           // 文本内容
    0,             // 起始位置
    text.length,   // 结束位置
    textPaint,     // 画笔
    width          // 宽度
).build()

// 在 Canvas 上绘制
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.save()
    staticLayout.draw(canvas)
    canvas.restore()
}

应用场景:

  • 多行文本绘制
  • 文本自动换行
  • 自定义文本排版

底层原理

让我详细讲解 StaticLayout 的底层原理:

1. StaticLayout 基本概念

StaticLayout 是 Android 中用于文字排版的核心类,主要负责:

  • 文本的换行计算
  • 行间距的测量
  • 文字的对齐方式
  • 段落的布局排版

2. 工作原理详解

// 基本使用示例
val staticLayout = StaticLayout.Builder
    .obtain(text, 0, text.length, textPaint, width)
    .setAlignment(Layout.Alignment.ALIGN_NORMAL)
    .setLineSpacing(0f, 1f)
    .setIncludePad(true)
    .build()
核心步骤:
  1. 文本分段
// 内部会将文本按照段落分隔符(\n)分成多个段落
private fun breakText() {
    var start = 0
    text.forEach { char ->
        if (char == '\n') {
            addParagraph(start, position)
            start = position + 1
        }
    }
}
  1. 计算换行位置
// 使用 Paint.breakText 计算每行能显示的字符数
private fun calculateLineBreaks() {
    var lineWidth = 0f
    var lineStart = 0
    
    text.forEachIndexed { index, char ->
        val charWidth = paint.measureText(char.toString())
        if (lineWidth + charWidth > maxWidth) {
            // 需要换行
            addLine(lineStart, index)
            lineStart = index
            lineWidth = charWidth
        } else {
            lineWidth += charWidth
        }
    }
}
  1. 测量行高
private fun measureLineHeight() {
    val fontMetrics = paint.fontMetrics
    val lineHeight = fontMetrics.bottom - fontMetrics.top
    val lineSpacing = lineHeight * spacingMultiplier + spacingAdd
}
  1. 文字对齐
private fun alignText(line: TextLine) {
    when (alignment) {
        ALIGN_NORMAL -> { /* 左对齐 */ }
        ALIGN_CENTER -> {
            // 居中对齐
            val offset = (maxWidth - line.width) / 2
            line.offset = offset
        }
        ALIGN_OPPOSITE -> {
            // 右对齐
            val offset = maxWidth - line.width
            line.offset = offset
        }
    }
}

3. 流程图

graph TD
    A[创建 StaticLayout] --> B[文本分段处理]
    B --> C[计算换行位置]
    C --> D[测量行高]
    D --> E[确定每行文字位置]
    E --> F[应用对齐方式]
    F --> G[缓存行信息]
    
    C --> C1[Paint.breakText 计算]
    C1 --> C2[判断是否需要换行]
    C2 --> C3[记录换行位置]
    
    D --> D1[获取 FontMetrics]
    D1 --> D2[计算基准行高]
    D2 --> D3[应用行间距]
    
    E --> E1[计算基线位置]
    E1 --> E2[处理字符间距]
    E2 --> E3[应用文字方向]

4. 关键优化点

  1. 文本测量缓存
// 缓存已测量的文本宽度
private val measuredTextCache = LruCache<String, Float>(100)

private fun measureText(text: String): Float {
    return measuredTextCache.get(text) ?: run {
        val width = paint.measureText(text)
        measuredTextCache.put(text, width)
        width
    }
}
  1. 行信息复用
// 复用行对象,避免频繁创建
private val linePool = Pool<TextLine>()

private fun obtainLine(): TextLine {
    return linePool.acquire() ?: TextLine()
}

5. 实际应用示例

class MultiLineTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    private var staticLayout: StaticLayout? = null
    
    fun setText(text: String) {
        staticLayout = StaticLayout.Builder
            .obtain(text, 0, text.length, textPaint, width)
            .setAlignment(Layout.Alignment.ALIGN_NORMAL)
            .setLineSpacing(0f, 1f)
            .setIncludePad(true)
            .setMaxLines(3)
            .setEllipsize(TextUtils.TruncateAt.END)
            .build()
        invalidate()
    }
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        staticLayout?.draw(canvas)
    }
}

6. 性能考虑

  1. 避免频繁创建
  • StaticLayout 创建成本较高
  • 尽可能复用实例
  • 考虑使用对象池
  1. 合理设置容量
  • 根据实际需求设置最大行数
  • 避免过度分配内存
  1. 异步处理
// 对于大段文本,考虑在后台线程处理
coroutineScope.launch(Dispatchers.Default) {
    val layout = StaticLayout.Builder
        .obtain(longText, 0, longText.length, textPaint, width)
        .build()
    
    withContext(Dispatchers.Main) {
        staticLayout = layout
        invalidate()
    }
}

通过以上详细解析,我们可以看到 StaticLayout 是一个功能强大的文本排版工具,它通过精确的文本测量和布局计算,实现了复杂的文本排版功能。理解其工作原理对于开发高性能的文本显示界面非常重要。