1. Android 补间动画和属性动画的区别
补间动画(Tween Animation)和属性动画(Property Animation)的主要区别:
- 本质区别
- 补间动画: 只是改变 View 的显示效果,并不会真正改变 View 的属性
- 属性动画: 通过直接修改控件的属性值实现动画效果
- 使用场景
- 补间动画: 适用于简单的位移、缩放、旋转、透明度变化
- 属性动画: 可以对任何对象的属性进行动画,不限于 View
- 点击事件
- 补间动画: 执行动画后 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 的关系
- Window
- Window 是一个抽象类,是所有视图的容器
- PhoneWindow 是 Window 的唯一实现类
- 每个 Activity 都会创建一个 Window 对象
- 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 的连接]
五、核心类的职责
- Window(抽象类)
- 管理窗口属性
- 处理各种事件回调
- 管理视图添加和删除
- PhoneWindow(具体实现)
- 创建 DecorView
- 管理各种特性(标题栏、进度条等)
- 处理按键事件
- DecorView(视图层级根布局)
class DecorView : FrameLayout {
// 标准布局
|--DecorView
|--StatusBar
|--TitleBar (可选)
|--ContentView (用户设置的布局)
|--NavigationBar
}
六、关键点总结
- 创建时序
- Activity 创建 → PhoneWindow 创建 → DecorView 创建 → 内容设置
- 职责分工
- Window:窗口管理
- DecorView:视图承载
- WindowManager:窗口操作
- 通信机制
// 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
}
}
这种实现方式的优势:
- 解耦了窗口管理和视图管理
- 提供了统一的窗口处理机制
- 方便实现各种窗口特性
通过这样的设计,Android 实现了灵活的窗口管理机制,使得应用可以方便地控制视图层级和窗口属性。
3. Android UI 刷新机制
Android 的 UI 刷新遵循以下机制:
- 垂直同步(VSync)
- 16.6ms 发出一次 VSync 信号
- 用于同步 CPU 计算和 GPU 渲染
- 刷新流程
触发重绘 → measure → layout → draw → swap buffers
- 主要方法
- invalidate(): 请求重绘当前 View
- requestLayout(): 请求重新布局
- 刷新原理
- 通过 ViewRootImpl 的 scheduleTraversals() 调度
- 使用 Choreographer 配合 VSync 进行同步
底层原理
一、基本概念
想象一个电影放映过程:
- VSync 信号 - 就像电影放映机的转动节奏
- 三级缓冲 - 就像准备好下一张胶片,等待当前胶片放映完
- 屏幕刷新率 - 一般是 60Hz,即每秒刷新 60 次,每次间隔约 16.6ms
二、核心角色
- Choreographer(编舞者)
class Choreographer {
// 注册下一帧回调
fun postFrameCallback(callback: FrameCallback) {
// 等待下一个 VSync 信号
}
// 处理 VSync 信号
private fun doFrame(frameTimeNanos: Long) {
// 1. 处理输入事件
// 2. 执行动画
// 3. 执行绘制
}
}
- ViewRootImpl(总指挥)
class ViewRootImpl {
fun scheduleTraversals() {
// 1. 设置同步屏障
mTraversalBarrier = mHandler.postSyncBarrier()
// 2. 请求下一帧回调
Choreographer.getInstance().postCallback(
Choreographer.CALLBACK_TRAVERSAL,
mTraversalRunnable,
null
)
}
}
三、刷新流程
- 触发刷新
// 可能的触发方式
view.invalidate() // 请求重绘
view.requestLayout() // 请求布局
- 调度过程
// ViewRootImpl 中
private fun performTraversals() {
// 1. 测量 (Measure)
performMeasure(...)
// 2. 布局 (Layout)
performLayout(...)
// 3. 绘制 (Draw)
performDraw(...)
}
四、底层实现
- Surface 机制
// 每个窗口都对应一个 Surface
class Surface {
private var mNativeObject: Long = 0
// 锁定 Canvas 进行绘制
fun lockCanvas(): Canvas {
// 获取后台缓冲区
return nativeLockCanvas(...)
}
// 解锁并提交绘制内容
fun unlockCanvasAndPost(canvas: Canvas) {
// 提交到图形缓冲区
nativeUnlockCanvasAndPost(...)
}
}
- 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[等待下一帧]
六、性能优化要点
- 避免丢帧
// 使用 Trace 监控性能
Trace.beginSection("performTraversals")
try {
performTraversals()
} finally {
Trace.endSection()
}
- 优化建议
- 避免过度绘制
- 扁平化视图层级
- 使用硬件加速
- 避免主线程耗时操作
七、监控方式
- Systrace
# 收集系统性能数据
python systrace.py -t 10 gfx view wm
- GPU Profile
// 开启 GPU 渲染监控
Settings.DEBUG_GPU_OVERDRAW = true
总结
核心流程:
- 触发刷新请求
- 设置同步屏障
- 等待 VSync 信号
- 执行 performTraversals
- 提交图形缓冲
- SurfaceFlinger 合成
- 显示到屏幕
优化重点:
- 保证每一帧的处理时间在 16ms 内
- 合理使用硬件加速
- 优化视图层级
- 避免主线程阻塞
通过这种方式,Android 实现了流畅的 UI 渲染,同时也为开发者提供了灵活的优化空间。
三级缓存原理
让我详细讲解下 Android 最新的三重缓冲(Triple Buffer)机制:
一、三重缓冲机制
graph LR
A[后台缓冲区 Back Buffer] --> B[等待缓冲区 Middle Buffer]
B --> C[显示缓冲区 Front Buffer]
C --> D[显示设备 Display]
二、工作原理
- 三个缓冲区角色
class TripleBuffer {
private val buffers = arrayOf(
Buffer("Front"), // 当前显示的缓冲区
Buffer("Middle"), // 等待显示的缓冲区
Buffer("Back") // 正在绘制的缓冲区
)
}
- 缓冲区切换过程
while (true) {
// 1. 应用进程在 Back Buffer 绘制
backBuffer.draw()
// 2. 等待 VSync 信号
waitForVSync()
// 3. 缓冲区轮转
swapBuffers() {
val temp = frontBuffer
frontBuffer = middleBuffer
middleBuffer = backBuffer
backBuffer = temp
}
}
三、优势分析
- 相比双缓冲的改进
双缓冲可能的问题:
┌────────────┐
│ 绘制帧1 │─┐
└────────────┘ │ ┌────────────┐
├─── │ 显示帧1 │
┌────────────┐ │ └────────────┘
│ 等待绘制帧2 │─┘
└────────────┘
三缓冲解决方案:
┌────────────┐
│ 绘制帧2 │─┐
└────────────┘ │ ┌────────────┐ ┌────────────┐
├─── │ 等待帧1 │ ─── │ 显示帧0 │
┌────────────┐ │ └────────────┘ └────────────┘
│ 准备缓冲 │─┘
└────────────┘
- 性能提升
- 减少等待时间
- 提高帧率稳定性
- 降低画面撕裂风险
四、实现细节
- 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()
}
}
}
- 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
六、性能监控
- 帧率监控
class FrameMetricsAggregator {
fun addFrames(frameMetrics: FrameMetrics) {
val duration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
if (duration > 16_666_666) { // 16.6ms in nanos
// 发现掉帧
logFrameDrop()
}
}
}
- 缓冲区使用监控
class BufferQueueMonitor {
fun onBufferQueued() {
// 监控缓冲区使用情况
val queuedBuffers = getQueuedBufferCount()
if (queuedBuffers >= 3) {
// 缓冲区已满,可能存在性能问题
logBufferQueueFull()
}
}
}
七、优化建议
- CPU 优化
// 避免主线程耗时操作
launch(Dispatchers.Default) {
// 耗时计算
withContext(Dispatchers.Main) {
// 更新 UI
}
}
- GPU 优化
// 使用硬件加速
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 避免过度绘制
view.setBackground(null) // 移除不必要的背景
- 内存优化
// 及时释放不需要的缓冲区
surface.release()
总结
三重缓冲的核心优势:
- 更好的性能表现
- 更流畅的画面表现
- 更低的延迟
使用注意事项:
- 内存占用增加
- 需要合理管理缓冲区资源
- 需要监控性能表现
通过三重缓冲机制,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[等待新消息]
四、实际应用示例
- 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)
}
}
}
- 消息类型对比
// 同步消息(普通消息)
handler.post {
// 可能被同步屏障阻塞
updateUI()
}
// 异步消息
handler.postAtFrontOfQueue {
// 不会被同步屏障阻塞
doFrameUpdate()
}
五、实现细节
- 消息队列处理
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
}
}
}
- 异步消息标记
val message = handler.obtainMessage().apply {
what = MSG_DO_FRAME
isAsynchronous = true // 标记为异步消息
}
六、使用场景
- UI 渲染优化
// 在需要保证UI流畅性的场景
class CustomView {
fun smoothScroll() {
// 设置同步屏障
val token = messageQueue.postSyncBarrier()
try {
// 执行滚动动画(异步消息)
postAsyncAnimation()
} finally {
// 动画结束后移除屏障
messageQueue.removeSyncBarrier(token)
}
}
}
- 关键任务优先级提升
class CriticalTaskHandler {
fun handleCriticalTask() {
// 设置同步屏障
val token = messageQueue.postSyncBarrier()
// 发送异步消息处理关键任务
sendAsyncMessage {
processCriticalTask()
// 完成后移除屏障
messageQueue.removeSyncBarrier(token)
}
}
}
七、注意事项
- 使用限制
// 必须成对使用
try {
val token = messageQueue.postSyncBarrier()
// 处理异步消息
} finally {
// 一定要移除屏障
messageQueue.removeSyncBarrier(token)
}
- 性能影响
// 不要滥用同步屏障
class PerformanceAwareHandler {
private var barrierCount = 0
fun postBarrier() {
barrierCount++
if (barrierCount > 3) {
Log.w(TAG, "Too many barriers may affect performance")
}
}
}
总结
同步屏障的核心作用:
- 提高关键任务优先级
- 保证 UI 渲染流畅性
- 优化消息队列处理机制
使用建议:
- 谨慎使用,仅用于关键场景
- 确保正确移除屏障
- 合理使用异步消息
通过同步屏障机制,Android 系统能够更好地保证 UI 渲染等关键任务的及时执行,提供流畅的用户体验。
4. LinearLayout、FrameLayout 和 RelativeLayout 的效率比较
效率排序: FrameLayout > LinearLayout > RelativeLayout
原因分析:
- FrameLayout
- 最简单的布局方式
- 只需一次测量和布局
- 子 View 直接叠加
- LinearLayout
- 需要进行权重计算(如果使用 weight)
- 按照方向逐个测量和布局
- 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 算法求解
}
}
二、核心组件
- 约束解析器(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
// ...
}
- 约束锚点(Anchor)
class ConstraintAnchor {
var target: ConstraintAnchor? = null // 目标锚点
var margin: Int = 0 // 边距
var type: Type // 类型(LEFT/TOP/RIGHT/BOTTOM)
}
三、布局过程
- 测量阶段
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 1. 构建约束图
mLayoutWidget.updateHierarchy()
// 2. 确定尺寸模式
val optimizationLevel = mOptimizationLevel
// 3. 测量子视图
measureChildren()
// 4. 解析约束
mLayoutWidget.layout()
}
- 布局阶段
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
)
}
}
四、优化机制
- 直接解析(Direct Resolution)
- 简单约束直接计算
- 不需要求解器参与
- 批量解析(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[结束布局]
六、性能优化要点
- 约束优化
<!-- 使用 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>
- 维度优化
<!-- 使用 0dp (MATCH_CONSTRAINT) 配合约束 -->
<View
android:layout_width="0dp"
app:layout_constraintWidth_default="wrap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
- 层级优化
<!-- 使用 Group 和 Barrier 减少嵌套 -->
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="id1,id2,id3"/>
七、工作流程总结
- 初始化阶段
- 解析 XML 布局文件
- 创建约束图
- 初始化求解器
- 测量阶段
- 收集所有约束
- 构建约束方程组
- 选择优化策略
- 求解阶段
- 直接解析简单约束
- 使用求解器处理复杂约束
- 确定最终位置
- 应用阶段
- 更新视图位置
- 触发重绘
- 完成布局
这种设计让 ConstraintLayout 能够:
- 扁平化布局层级
- 高效处理复杂约束
- 提供灵活的布局能力
- 保持较好的性能
通过这种方式,ConstraintLayout 在保持灵活性的同时,也能提供较好的性能表现。它的核心优势在于约束求解引擎的高效性,以及对各种布局场景的良好支持。
求解器(Solver)及 Cassowary 约束求解算法
让我详细解释 ConstraintLayout 中的求解器(Solver):
一、求解器概述
ConstraintLayout 使用的是 Cassowary 约束求解算法,这是一个用于解决线性等式和不等式系统的算法。
简单来说,就像解决这样的方程组:
方程组示例:
x + y = 100 (视图A的右边缘 + 视图B的左边缘 = 100dp)
y > 50 (视图B的左边缘必须大于50dp)
x < 40 (视图A的右边缘必须小于40dp)
二、核心组件
- 变量系统
class SolverVariable {
var name: String // 变量名称
var type: Type // 变量类型
var computedValue: Float // 计算值
enum class Type {
UNRESTRICTED, // 无限制变量
CONSTANT, // 常量
SLACK, // 松弛变量
ERROR, // 错误变量
UNKNOWN // 未知类型
}
}
- 约束方程
class ArrayRow {
val variables: ArrayList<SolverVariable> // 变量列表
var constantValue: Float // 常量值
// 添加变量到方程
fun addVariable(variable: SolverVariable, coefficient: Float)
}
三、工作原理
- 创建约束
// 示例:设置视图A的右边缘到视图B的左边缘的约束
val equation = ArrayRow().apply {
// A.right + margin = B.left
addVariable(viewA.right, 1f)
addVariable(viewB.left, -1f)
constantValue = margin.toFloat()
}
- 优先级处理
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[应用到视图]
五、实际应用示例
- 简单居中约束
// 伪代码表示
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
}
- 链式约束(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()
}
}
}
}
六、优化策略
- 直接求解
// 简单约束直接计算
if (isSimpleConstraint()) {
widget.left = target.right + margin
return true
}
- 批量求解
// 复杂约束使用完整求解器
class LinearSystem {
fun solve() {
// 1. 优化方程组
optimize()
// 2. 选择主元
selectPivotVariable()
// 3. 高斯消元
solve()
// 4. 更新结果
updateVariables()
}
}
七、性能考虑
- 缓存机制
class SolverCache {
private val variableCache = mutableMapOf<String, SolverVariable>()
private val equationCache = mutableMapOf<String, ArrayRow>()
fun getCachedValue(key: String): Float? {
return variableCache[key]?.computedValue
}
}
- 增量更新
class IncrementalSolver {
fun updateConstraint(constraint: Constraint) {
// 只更新变化的约束
if (hasChanged(constraint)) {
solver.updateFromDelta(constraint)
}
}
}
八、实际应用建议
- 避免过度约束
<!-- 好的做法 -->
<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"/>
- 使用辅助工具
<!-- 使用 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
关键规则:
- 返回值的含义
- true: 事件被消费,不继续传递
- false: 事件未被消费,继续传递
- 传递顺序
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]
二、详细分发过程
- 事件产生
// 在 InputManagerService 中接收底层输入事件
class InputManagerService {
private void processEventsLocked() {
// 1. 收集触摸事件
// 2. 寻找目标窗口
// 3. 发送到目标窗口
}
}
- 事件传递到 Activity
// Activity 接收事件
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 交给 Window 处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 都没处理则自己处理
return onTouchEvent(ev);
}
- 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
七、关键点总结
- 事件传递顺序:
- Activity -> Window -> DecorView -> ViewGroup -> View
- 事件分发三个核心方法:
- dispatchTouchEvent:分发事件
- onInterceptTouchEvent:拦截事件
- onTouchEvent:处理事件
- 返回值含义:
- true:事件被消费,停止传递
- false:事件未被消费,继续传递
- 事件传递特点:
- 从上到下分发(dispatchTouchEvent)
- 从下到上传递(onTouchEvent)
- 中间可拦截(onInterceptTouchEvent)
- 实际应用建议:
// 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 的流程
基本流程:
- 构造方法
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
// 初始化工作
}
}
- 测量阶段(MeasureSpec)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
- 布局阶段
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 确定位置
}
- 绘制阶段
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
}
布局过程:
- ViewGroup 确定子 View 位置
- 子 View 保存自己的位置信息
- 计算实际显示区域
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)
}
}
绘制流程:
- 背景绘制(系统自动调用)
- 内容绘制(自定义实现)
- 前景绘制(系统自动调用)
三、实际应用示例
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() // 请求重绘
}
}
四、性能优化建议
- 构造阶段
- 避免在构造方法中进行耗时操作
- 及时回收 TypedArray
- 测量阶段
- 避免多次测量
- 合理使用 MeasureSpec
- 布局阶段
- 减少布局层级
- 避免嵌套布局
- 绘制阶段
- 使用 canvas.clipRect 减少过度绘制
- 合理使用 canvas.save/restore
- 避免在 onDraw 中创建对象
五、总结
自定义 View 的流程是一个完整的系统,从构造到最终显示,每个阶段都有其特定的职责:
- 构造方法:初始化准备
- onMeasure:确定大小
- onLayout:确定位置
- onDraw:绘制内容
理解这个流程对于开发高性能、体验好的自定义 View 至关重要。
7. RecyclerView 优化方法
主要优化方向:
- 布局优化
// 固定大小
recyclerView.setHasFixedSize(true)
// 关闭动画
recyclerView.itemAnimator = null
- 数据处理优化
// 差异化更新
val diffCallback = object : DiffUtil.Callback() {
// 实现比较方法
}
DiffUtil.calculateDiff(diffCallback).dispatchUpdatesTo(adapter)
- 缓存优化
// 增加缓存池大小
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)
- 其他优化
- 使用 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[异步加载]
三、最佳实践建议
-
布局优化:
- 使用
setHasFixedSize(true) - 降低 Item 布局层级
- 使用 ConstraintLayout
- 使用
-
数据优化:
- 使用 DiffUtil 进行差异化更新
- 避免全量刷新
- 使用 notifyItemChanged 等局部刷新方法
-
缓存优化:
- 根据实际需求设置合适的缓存大小
- 考虑使用自定义缓存扩展
- 复用 ViewHolder
-
预加载优化:
- 实现滑动监听
- 异步预加载数据
- 使用分页加载
通过以上优化,可以显著提升 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 优化方法
主要优化方向:
- 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
}
- 其他优化方案
- 异步加载图片
- 减少 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. 优化效果总结
- 内存优化
- 减少View对象创建
- 减少GC频率
- 降低内存占用
- 性能优化
- 减少布局渲染时间
- 减少主线程工作量
- 提升滑动流畅度
- 用户体验
- 更流畅的滑动
- 更快的加载速度
- 更少的卡顿现象
通过以上优化,ListView 的性能可以得到显著提升,特别是在大量数据展示时,优化效果更为明显。关键是要根据具体场景选择合适的优化策略组合使用。
9. 自定义 RecyclerView.LayoutManager 流程
核心步骤:
- 继承 LayoutManager
class CustomLayoutManager : RecyclerView.LayoutManager() {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}
- 实现布局逻辑
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]
二、详细实现原理
- 初始化阶段
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
)
}
}
- 布局阶段
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)
}
}
- 滑动处理
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)
}
}
}
三、核心机制
- View 复用机制
graph LR
A[Scrap缓存] --> B[Cache缓存]
B --> C[ViewCacheExtension]
C --> D[RecycledViewPool]
- 布局过程
graph TD
A[测量阶段] --> B[布局阶段]
B --> C[绘制阶段]
C --> D[回收阶段]
- 滑动处理流程
graph TD
A[触摸事件] --> B[计算滑动距离]
B --> C[移动现有View]
C --> D[填充新View]
D --> E[回收旧View]
四、关键要点
- View 的获取与回收
- getViewForPosition(): 获取 View
- recycleView(): 回收 View
- detachAndScrapAttachedViews(): 分离所有 View
- 布局相关方法
- measureChildWithMargins(): 测量子 View
- layoutDecorated(): 布局子 View
- getDecoratedMeasuredWidth/Height(): 获取实际尺寸
- 滑动处理
- scrollHorizontallyBy()/scrollVerticallyBy(): 处理滑动
- offsetChildrenHorizontal()/offsetChildrenVertical(): 移动子 View
五、优化建议
- 性能优化
// 1. 预加载
private val preloadItemCount = 3
// 2. 复用优化
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)
// 3. 局部刷新
notifyItemChanged(position)
- 内存优化
// 及时回收不可见 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 主要用于:
- 通知栏界面
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()
- 桌面小部件
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. 创建 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. 执行流程
- 序列化过程
// 系统将 RemoteViews 对象序列化
val parcel = Parcel.obtain()
remoteViews.writeToParcel(parcel, 0)
- 跨进程传输
// 通过 Binder 机制传输到目标进程
// 例如:通知栏进程 SystemUI
NotificationManager.notify(notification)
- 重建和应用
// 目标进程中
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. 重要限制
- 支持的 View 类型有限
// 仅支持基础 View
- TextView
- ImageView
- Button
- ProgressBar
- ViewFlipper
- ListView
- GridView
- StackView
- AdapterViewFlipper
- ViewStub
- 支持的操作有限
// 常用操作示例
remoteViews.apply {
setTextViewText() // 设置文本
setTextColor() // 设置颜色
setImageViewResource() // 设置图片
setViewVisibility() // 设置可见性
setOnClickPendingIntent() // 设置点击事件
}
6. 性能优化建议
- 减少更新频率
// 批量更新
remoteViews.apply {
setTextViewText(R.id.text1, "文本1")
setTextViewText(R.id.text2, "文本2")
// 一次性提交多个更新
}
- 优化布局层级
<!-- 保持布局简单扁平 -->
<LinearLayout>
<TextView/>
<ImageView/>
<!-- 避免复杂嵌套 -->
</LinearLayout>
- 合理使用缓存
// 缓存 RemoteViews 实例
private var cachedRemoteViews: RemoteViews? = null
fun updateWidget() {
if (cachedRemoteViews == null) {
cachedRemoteViews = RemoteViews(...)
}
// 复用已有实例进行更新
}
总结要点:
- RemoteViews 本质是一个跨进程的 View 更新方案
- 通过 Action 序列化机制实现界面更新
- 使用 Binder 机制进行跨进程通信
- 在目标进程中重建视图并执行更新
- 有限的 View 类型和操作支持
- 需要注意性能优化和资源消耗
这种机制保证了在不同进程中可以安全地更新 UI,同时也带来了一定的使用限制和性能开销。在实际应用中,需要根据具体场景合理使用并做好优化。
11. 获取 View 宽高的几种方法
- onWindowFocusChanged
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
val width = view.width
val height = view.height
}
}
- ViewTreeObserver
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val width = view.width
val height = view.height
}
})
- post 方法
view.post {
val width = view.width
val height = view.height
}
- 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. 详细解释
- 为什么直接获取宽高会为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 并完成测量
- 测量流程的关键节点
class ViewRootImpl {
private fun performTraversals() {
// 1. 执行测量
performMeasure(...)
// 2. 执行布局
performLayout(...)
// 3. 执行绘制
performDraw(...)
}
}
- 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. 最佳实践建议
- 优先使用 ViewTreeObserver
- 可以及时移除监听
- 时机最准确
- View.post 最简便
- 代码简洁
- 自动处理生命周期
- 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() 的工作原理:
- 消息机制
- 将 Runnable 加入到消息队列
- 依附于 ViewRootImpl 的 Handler
- 执行时机
// 简化的流程
if (mAttachInfo != null) {
// View已经被添加到窗口
mAttachInfo.mHandler.post(action);
} else {
// View还未添加到窗口
getRunQueue().post(action);
}
- 保证执行
- 如果 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. 关键知识点总结
-
两种执行路径
- 直接通过 Handler 执行
- 先存队列,等 attach 后执行
-
Handler 来源
- ViewRootImpl 中创建
- 确保在主线程执行
-
HandlerActionQueue 作用
- 临时存储 Runnable
- 保证任务不丢失
-
为什么能获取宽高
- 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. 注意事项
- 内存泄漏风险
// 错误示例
view.post {
// 持有外部类引用
someOuterVariable.doSomething()
}
// 正确示例
val runnable = WeakReference(Runnable {
// 使用弱引用
})
view.post(runnable.get())
- 生命周期问题
// 建议在合适的生命周期调用
override fun onResume() {
super.onResume()
view.post { /* 安全的操作 */ }
}
- 执行时机
// 确保在主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
view.post { /* UI操作 */ }
}
通过这种机制,View.post() 巧妙地解决了在 View 未完成测量时获取宽高的问题,同时保证了任务一定会被执行,是 Android 中常用的异步处理机制之一。
13. 属性动画的插值器和估值器
- 插值器(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 // 示例:二次方加速
}
}
- 估值器(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 相关方法区别
- getDimension
- 返回 float
- 保留小数点
- 将 dp/sp 转换为 px
- getDimensionPixelOffset
- 返回 int
- 向下取整
- 将 dp/sp 转换为 px
- 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()
核心步骤:
- 文本分段
// 内部会将文本按照段落分隔符(\n)分成多个段落
private fun breakText() {
var start = 0
text.forEach { char ->
if (char == '\n') {
addParagraph(start, position)
start = position + 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
}
}
}
- 测量行高
private fun measureLineHeight() {
val fontMetrics = paint.fontMetrics
val lineHeight = fontMetrics.bottom - fontMetrics.top
val lineSpacing = lineHeight * spacingMultiplier + spacingAdd
}
- 文字对齐
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. 关键优化点
- 文本测量缓存
// 缓存已测量的文本宽度
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
}
}
- 行信息复用
// 复用行对象,避免频繁创建
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. 性能考虑
- 避免频繁创建
- StaticLayout 创建成本较高
- 尽可能复用实例
- 考虑使用对象池
- 合理设置容量
- 根据实际需求设置最大行数
- 避免过度分配内存
- 异步处理
// 对于大段文本,考虑在后台线程处理
coroutineScope.launch(Dispatchers.Default) {
val layout = StaticLayout.Builder
.obtain(longText, 0, longText.length, textPaint, width)
.build()
withContext(Dispatchers.Main) {
staticLayout = layout
invalidate()
}
}
通过以上详细解析,我们可以看到 StaticLayout 是一个功能强大的文本排版工具,它通过精确的文本测量和布局计算,实现了复杂的文本排版功能。理解其工作原理对于开发高性能的文本显示界面非常重要。