01-02-10 View开发最佳实践与优化完全指南
View开发最佳实践与优化指南 本文系统性地介绍了Android自定义View的开发流程与优化策略: View体系结构:采用组合模式设计,View负责绘制和事件处理,ViewGroup还管理子View布局 开发流程: 六步开发法:需求分析→自定义属性→构造函数→测量→绘制→交互 四构造函数调用链处理不同创建场景 属性解析模板避免内存泄漏 性能优化: 复用Paint对象 避免onDraw中创建对象
·
01-02-10 View开发最佳实践与优化完全指南
难度等级:⭐⭐⭐⭐
标签:View开发规范 性能优化 最佳实践 自定义View 架构设计
🎯 核心问题
如何系统性地开发高质量的自定义View?有哪些关键的开发流程和规范?如何避免常见的性能陷阱?如何优化View的测量、布局和绘制性能?
本文深入探讨自定义View的完整开发流程,从体系结构理解到构造函数设计,从开发规范到性能优化,提供全方位的最佳实践指南,帮助开发者构建高性能、可维护的自定义View。
💡 核心概念体系
View继承体系
Android View采用**组合模式(Composite Pattern)**设计,形成树形结构:
Object
↓
View (所有UI组件的基类)
↓
├─ TextView (文本显示)
├─ ImageView (图片显示)
├─ Button (按钮)
└─ ViewGroup (容器)
↓
├─ LinearLayout (线性布局)
├─ RelativeLayout (相对布局)
├─ ConstraintLayout (约束布局)
└─ FrameLayout (帧布局)
职责划分:
- View:负责绘制自己、处理事件、管理状态
- ViewGroup:除View职责外,还负责管理子View、分发事件、布局控制
六步开发流程
1. 需求分析 → 确定功能和UI效果
2. 自定义属性 → 定义attrs.xml配置
3. 构造函数 → 处理属性初始化
4. 测量 → 实现onMeasure计算尺寸
5. 绘制 → 实现onDraw绘制内容
6. 交互 → 实现onTouchEvent处理事件
📝 完整开发实战
1. 构造函数设计与属性解析
View有4个构造函数,对应不同的创建场景。
构造函数调用链
View(Context)
→ View(Context, AttributeSet?)
→ View(Context, AttributeSet?, Int)
→ View(Context, AttributeSet?, Int, Int)
调用场景:
- 单参数:代码创建View
CustomView(context) - 双参数:XML布局加载(最常用)
- 三参数:指定默认样式属性
CustomView(context, null, R.attr.customViewStyle) - 四参数:指定默认样式资源(API 21+)
CustomView(context, null, 0, R.style.CustomViewStyle)
标准实现模板
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.customViewStyle,
defStyleRes: Int = R.style.DefaultCustomViewStyle
) : View(context, attrs, defStyleAttr, defStyleRes) {
// 自定义属性
private var customText: String = ""
private var customColor: Int = Color.BLUE
private var customSize: Float = 14f
private var customEnabled: Boolean = true
// 画笔对象(复用,避免在onDraw中创建)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
initAttributes(attrs, defStyleAttr, defStyleRes)
setupPaint()
}
private fun initAttributes(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
// 使用obtainStyledAttributes获取属性
context.theme.obtainStyledAttributes(
attrs,
R.styleable.CustomView,
defStyleAttr,
defStyleRes
).apply {
try {
customText = getString(R.styleable.CustomView_customText) ?: ""
customColor = getColor(R.styleable.CustomView_customColor, Color.BLUE)
customSize = getDimension(R.styleable.CustomView_customSize, 14f)
customEnabled = getBoolean(R.styleable.CustomView_customEnabled, true)
} finally {
recycle() // ⚠️ 必须回收
}
}
}
private fun setupPaint() {
paint.apply {
color = customColor
style = Paint.Style.FILL
}
textPaint.apply {
color = Color.WHITE
textSize = customSize
}
}
}
自定义属性定义
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
<attr name="customSize" format="dimension" />
<attr name="customEnabled" format="boolean" />
<attr name="customGravity" format="enum">
<enum name="left" value="0" />
<enum name="center" value="1" />
<enum name="right" value="2" />
</attr>
</declare-styleable>
<!-- 定义样式属性引用 -->
<attr name="customViewStyle" format="reference" />
</resources>
<!-- res/values/styles.xml -->
<resources>
<style name="DefaultCustomViewStyle">
<item name="customColor">@color/primary</item>
<item name="customSize">16sp</item>
<item name="customEnabled">true</item>
</style>
</resources>
2. 完整案例:彩色标签View
展示从需求分析到最终实现的完整流程。
Step 1: 需求分析
设计一个彩色标签View,具备以下特性:
- 可自定义文本、颜色、大小
- 圆角背景
- 支持点击反馈
- 自动计算合适的尺寸
Step 2: 完整实现
class TagView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.tagViewStyle,
defStyleRes: Int = R.style.DefaultTagViewStyle
) : View(context, attrs, defStyleAttr, defStyleRes) {
// 属性
private var tagText: String = ""
private var tagColor: Int = Color.BLUE
private var tagTextSize: Float = 14f.sp
private var tagPadding: Float = 8f.dp
// 画笔(复用对象)
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textAlign = Paint.Align.CENTER
}
// 缓存对象
private val tempRect = RectF()
// 状态
private var isPressed = false
init {
initAttributes(attrs, defStyleAttr, defStyleRes)
isClickable = true
}
private fun initAttributes(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.TagView,
defStyleAttr,
defStyleRes
).apply {
try {
tagText = getString(R.styleable.TagView_tagText) ?: ""
tagColor = getColor(R.styleable.TagView_tagColor, Color.BLUE)
tagTextSize = getDimension(R.styleable.TagView_tagTextSize, 14f.sp)
tagPadding = getDimension(R.styleable.TagView_tagPadding, 8f.dp)
backgroundPaint.color = tagColor
textPaint.textSize = tagTextSize
} finally {
recycle()
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 计算文本宽度
val textWidth = textPaint.measureText(tagText)
val fontMetrics = textPaint.fontMetrics
val textHeight = fontMetrics.descent - fontMetrics.ascent
// 加上padding计算总尺寸
val desiredWidth = (textWidth + tagPadding * 2).toInt()
val desiredHeight = (textHeight + tagPadding * 2).toInt()
setMeasuredDimension(
resolveSize(desiredWidth, widthMeasureSpec),
resolveSize(desiredHeight, heightMeasureSpec)
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制圆角背景(按下时改变透明度)
backgroundPaint.alpha = if (isPressed) 180 else 255
tempRect.set(0f, 0f, width.toFloat(), height.toFloat())
canvas.drawRoundRect(tempRect, height / 2f, height / 2f, backgroundPaint)
// 绘制文本(居中)
val fontMetrics = textPaint.fontMetrics
val baseline = height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
canvas.drawText(tagText, width / 2f, baseline, textPaint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isPressed = true
invalidate()
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isPressed) {
isPressed = false
invalidate()
if (event.action == MotionEvent.ACTION_UP) {
performClick()
}
}
}
}
return super.onTouchEvent(event)
}
override fun performClick(): Boolean {
return super.performClick()
}
// 公开API
fun setTagText(text: String) {
if (tagText != text) {
tagText = text
requestLayout() // 尺寸可能改变
}
}
fun setTagColor(color: Int) {
if (tagColor != color) {
tagColor = color
backgroundPaint.color = color
invalidate() // 仅颜色改变
}
}
// 状态保存与恢复
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return Bundle().apply {
putParcelable("superState", superState)
putString("tagText", tagText)
putInt("tagColor", tagColor)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
tagText = state.getString("tagText", "")
tagColor = state.getInt("tagColor", Color.BLUE)
backgroundPaint.color = tagColor
super.onRestoreInstanceState(state.getParcelable("superState"))
} else {
super.onRestoreInstanceState(state)
}
}
// 可访问性支持
init {
contentDescription = "标签: $tagText"
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
}
// 扩展属性
private val Float.dp: Float
get() = this * context.resources.displayMetrics.density
private val Float.sp: Float
get() = this * context.resources.displayMetrics.scaledDensity
}
Step 3: XML使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<com.example.TagView
android:id="@+id/tagHot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tagText="热门"
app:tagColor="#FF5722"
app:tagTextSize="12sp"
app:tagPadding="6dp" />
<com.example.TagView
android:id="@+id/tagNew"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:tagText="新品"
app:tagColor="#4CAF50" />
</LinearLayout>
🚀 性能优化核心策略
1. 避免对象分配
原则:永远不要在onDraw中创建对象。
// ❌ 错误示例:每次绘制都创建新对象
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 每次都创建!
val rect = RectF() // 每次都创建!
paint.color = Color.RED
canvas.drawRect(rect, paint)
}
// ✅ 正确示例:复用Paint对象
class OptimizedView : View {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG) // 在init或构造函数中创建
private val tempRect = RectF() // 缓存对象
override fun onDraw(canvas: Canvas) {
paint.color = Color.RED
tempRect.set(0f, 0f, width.toFloat(), height.toFloat())
canvas.drawRect(tempRect, paint)
}
}
2. 减少不必要的刷新
// ❌ 错误示例:不检查就刷新
fun setColor(color: Int) {
this.color = color
invalidate() // 即使颜色相同也刷新
}
// ✅ 正确示例:检查是否真的需要刷新
fun setColor(color: Int) {
if (this.color != color) {
this.color = color
invalidate() // 只在变化时刷新
}
}
// ✅ 区分invalidate和requestLayout
fun setText(text: String) {
if (this.text != text) {
this.text = text
// 如果文本长度改变,可能影响尺寸
if (this.text.length != text.length) {
requestLayout() // 触发measure → layout → draw
} else {
invalidate() // 仅触发draw
}
}
}
3. 使用Canvas裁剪
override fun onDraw(canvas: Canvas) {
// 裁剪到可见区域
canvas.clipRect(dirtyRect)
// 只绘制可见内容
items.forEach { item ->
if (item.rect.intersects(dirtyRect)) {
drawItem(canvas, item)
}
}
}
4. 缓存复杂图形
class ComplexView : View {
private var cachedBitmap: Bitmap? = null
private var isDirty = true
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 尺寸变化时重建缓存
cachedBitmap?.recycle()
cachedBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
isDirty = true
}
override fun onDraw(canvas: Canvas) {
// 只在内容变化时重绘到缓存
if (isDirty) {
val cacheCanvas = Canvas(cachedBitmap!!)
drawComplexContent(cacheCanvas) // 复杂的绘制操作
isDirty = false
}
// 直接绘制缓存的Bitmap
cachedBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
}
fun updateContent() {
isDirty = true
invalidate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
cachedBitmap?.recycle()
cachedBitmap = null
}
}
5. 优化measure和layout
class OptimizedMeasureView : View {
// 缓存计算结果
private var cachedWidth = 0
private var cachedHeight = 0
private var measureDirty = true
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
// 如果尺寸已确定且未变化,直接返回
if (widthMode == MeasureSpec.EXACTLY &&
heightMode == MeasureSpec.EXACTLY &&
!measureDirty) {
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec)
)
return
}
// 只在需要时重新计算
if (measureDirty) {
cachedWidth = calculateWidth() // 耗时操作
cachedHeight = calculateHeight()
measureDirty = false
}
setMeasuredDimension(
resolveSize(cachedWidth, widthMeasureSpec),
resolveSize(cachedHeight, heightMeasureSpec)
)
}
fun updateContent() {
measureDirty = true
requestLayout()
}
}
6. 减少过度绘制
class OptimizedOverdrawView : View {
override fun onDraw(canvas: Canvas) {
// ✅ 使用clipOut排除重叠区域
canvas.save()
// 先绘制背景
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
// 排除前景区域
canvas.clipOutRect(foregroundRect)
canvas.restore()
// 绘制前景
canvas.drawRect(foregroundRect, fgPaint)
}
}
7. 使用硬件加速
class HardwareAcceleratedView : View {
init {
// 启用硬件加速(API 11+)
setLayerType(LAYER_TYPE_HARDWARE, null)
}
override fun onDraw(canvas: Canvas) {
// 某些操作不支持硬件加速时,临时切换
if (needSoftwareRendering) {
setLayerType(LAYER_TYPE_SOFTWARE, null)
// 执行不支持的操作
setLayerType(LAYER_TYPE_HARDWARE, null)
}
// 正常绘制
drawContent(canvas)
}
}
8. 使用SurfaceView处理高频绘制
对于游戏、视频等高频刷新场景,使用SurfaceView。
class GameView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
private var drawingThread: DrawingThread? = null
init {
holder.addCallback(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {
drawingThread = DrawingThread(holder).apply {
isRunning = true
start()
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
drawingThread?.let {
it.isRunning = false
it.join()
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
private class DrawingThread(private val holder: SurfaceHolder) : Thread() {
var isRunning = false
override fun run() {
while (isRunning) {
var canvas: Canvas? = null
try {
canvas = holder.lockCanvas()
canvas?.let { drawFrame(it) }
} finally {
canvas?.let { holder.unlockCanvasAndPost(it) }
}
}
}
private fun drawFrame(canvas: Canvas) {
// 绘制操作
}
}
}
9. 异步加载资源
class AsyncImageView : View {
private var bitmap: Bitmap? = null
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun loadImage(url: String) {
scope.launch {
val loadedBitmap = withContext(Dispatchers.IO) {
// 在IO线程加载图片
loadBitmapFromUrl(url)
}
// 回到主线程更新UI
bitmap = loadedBitmap
invalidate()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
}
10. 性能监控
class PerformanceMonitorView : View {
private var frameCount = 0
private var totalTime = 0L
override fun onDraw(canvas: Canvas) {
val startTime = System.nanoTime()
// 绘制操作
drawContent(canvas)
val duration = (System.nanoTime() - startTime) / 1_000_000 // ms
frameCount++
totalTime += duration
if (duration > 16) { // 超过1帧时间(60fps)
Log.w("Performance", "Frame drop! onDraw took ${duration}ms")
}
if (frameCount >= 60) {
val avgTime = totalTime / frameCount
Log.d("Performance", "Average frame time: ${avgTime}ms")
frameCount = 0
totalTime = 0
}
}
}
📋 开发规范清单
构造函数规范
// ✅ 推荐:使用@JvmOverloads
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)
// ❌ 不推荐:手动重载
class MyView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
// ...
}
资源管理规范
// ✅ 及时回收TypedArray
context.obtainStyledAttributes(attrs, R.styleable.MyView).apply {
// 读取属性
recycle() // ⚠️ 必须调用
}
// ✅ 复用对象
private val paint = Paint()
private val tempRect = RectF()
private val path = Path()
// ❌ 不推荐
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 每次创建新对象
}
状态保存规范
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return Bundle().apply {
putParcelable("superState", superState)
putInt("customColor", customColor)
putString("customText", customText)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle) {
customColor = state.getInt("customColor")
customText = state.getString("customText", "")
super.onRestoreInstanceState(state.getParcelable("superState"))
} else {
super.onRestoreInstanceState(state)
}
}
可访问性规范
init {
// 设置内容描述
contentDescription = "自定义按钮"
// 设置重要性
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
}
override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
super.onInitializeAccessibilityEvent(event)
event.className = MyView::class.java.name
}
⚡ 关键要点总结
开发流程
- 需求分析 → 确定功能和UI效果
- 定义属性 → 创建attrs.xml,支持XML配置
- 实现构造函数 → 使用@JvmOverloads,解析属性
- 测量 → onMeasure计算尺寸,使用resolveSize
- 绘制 → onDraw绘制内容,复用Paint对象
- 交互 → onTouchEvent处理事件,调用performClick
- 状态保存 → 实现onSaveInstanceState/onRestoreInstanceState
- 可访问性 → 添加contentDescription
性能优化
| 优化点 | 方法 | 收益 |
|---|---|---|
| 对象复用 | 在init中创建Paint等对象 | 避免GC,提升流畅度 |
| 减少刷新 | 检查状态变化再invalidate | 减少不必要的重绘 |
| 缓存图形 | 使用Bitmap缓存复杂绘制 | 大幅提升复杂View性能 |
| 局部刷新 | 使用invalidate(Rect) | 减少绘制区域 |
| 硬件加速 | setLayerType(HARDWARE) | GPU加速,性能提升明显 |
| 测量优化 | 缓存计算结果 | 避免重复计算 |
| 异步加载 | 协程加载资源 | 避免阻塞主线程 |
| SurfaceView | 高频绘制场景 | 独立绘制线程,不阻塞UI |
常见陷阱
- onDraw中创建对象 → 导致频繁GC
- 不区分invalidate和requestLayout → 触发不必要的measure/layout
- 忘记recycle TypedArray → 内存泄漏
- 过度绘制 → 性能下降
- measure中耗时计算 → 影响布局性能
- 不处理状态保存 → 屏幕旋转后数据丢失
🔗 相关知识点
- View绘制流程:measure、layout、draw三大步骤
- 事件分发机制:onTouchEvent、dispatchTouchEvent
- LayoutParams:自定义布局参数
- ViewGroup实现:自定义容器布局
- 属性动画:动态修改View属性
- Canvas绘制:Path、Matrix、Shader等高级特性
📖 实战建议
开发流程
- 先确定需求:明确View的功能和交互方式
- 设计API:定义公开的属性和方法
- 实现核心逻辑:measure、draw、touch
- 测试边界情况:不同尺寸、不同数据、横竖屏切换
- 性能优化:使用Profiler工具分析性能瓶颈
- 文档和示例:提供清晰的使用文档和示例代码
调试技巧
// 开启布局边界显示
Settings → Developer Options → Show layout bounds
// 开启GPU过度绘制检测
Settings → Developer Options → Debug GPU overdraw
// 使用Hierarchy Viewer分析View树
Android Studio → Tools → Layout Inspector
// 性能监控
class DebugView : View {
override fun onDraw(canvas: Canvas) {
Trace.beginSection("MyView.onDraw")
try {
drawContent(canvas)
} finally {
Trace.endSection()
}
}
}
常见问题
Q: 为什么自定义View不显示?
A: 检查以下几点:
- onMeasure是否正确调用setMeasuredDimension
- onDraw是否有绘制内容
- 布局参数是否设置为0dp或GONE
- View是否在可见区域内
Q: 如何实现子View居中?
A: 在onLayout中计算偏移量:
val offsetX = (width - child.measuredWidth) / 2
val offsetY = (height - child.measuredHeight) / 2
child.layout(offsetX, offsetY,
offsetX + child.measuredWidth,
offsetY + child.measuredHeight)
Q: 何时使用invalidate,何时使用requestLayout?
A:
- 仅内容改变(颜色、文本内容)→
invalidate() - 尺寸改变(文本长度、View大小)→
requestLayout()
Q: 如何支持dp和sp?
A: 使用扩展属性转换:
private val Float.dp: Float
get() = this * context.resources.displayMetrics.density
private val Float.sp: Float
get() = this * context.resources.displayMetrics.scaledDensity
📚 扩展阅读
- View源码分析:深入理解View的实现原理
- Canvas与Paint详解:掌握高级绘制技巧
- 硬件加速原理:理解GPU渲染机制
- Material Design规范:学习优秀的UI设计
- Jetpack Compose:了解声明式UI的未来
更多推荐

所有评论(0)