View 体系与 ViewBinding
问题
Android View 体系的层级结构是怎样的?ViewBinding 和 DataBinding 有什么区别?
答案
1. View 继承体系
- View:所有 UI 元素的基类,负责绘制和事件处理
- ViewGroup:View 的子类,可以包含子 View,负责子 View 的测量和布局
2. MeasureSpec
MeasureSpec 是父 View 传递给子 View 的测量约束,由 mode 和 size 组合而成:
| Mode | 含义 | 对应 XML |
|---|---|---|
EXACTLY | 精确尺寸 | match_parent 或固定 dp |
AT_MOST | 最大不超过 size | wrap_content |
UNSPECIFIED | 无限制 | ScrollView 内的子 View |
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth // UNSPECIFIED
}
setMeasuredDimension(width, height)
}
3. ViewBinding
ViewBinding 是 Android 推荐的视图绑定方式,编译时生成绑定类,替代 findViewById:
// build.gradle.kts
android {
buildFeatures {
viewBinding = true
}
}
// Activity 中使用
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 直接通过 binding 访问 View,类型安全
binding.textTitle.text = "Hello"
binding.buttonSubmit.setOnClickListener { }
}
}
// Fragment 中使用(注意生命周期)
class HomeFragment : Fragment(R.layout.fragment_home) {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentHomeBinding.bind(view)
binding.textTitle.text = "Home"
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 避免内存泄漏
}
}
4. ViewBinding vs DataBinding vs findViewById
| 特性 | findViewById | ViewBinding | DataBinding |
|---|---|---|---|
| 空安全 | ❌ 可能返回 null | ✅ 编译时检查 | ✅ 编译时检查 |
| 类型安全 | ❌ 需手动转型 | ✅ 自动 | ✅ 自动 |
| 性能 | 运行时遍历 View 树 | 编译时生成 | 编译时生成 |
| 布局表达式 | ❌ | ❌ | ✅ @{viewModel.name} |
| 双向绑定 | ❌ | ❌ | ✅ @={viewModel.text} |
| 编译速度 | 无影响 | 影响很小 | 明显增加 |
选择建议
- 新项目推荐 ViewBinding(简单、轻量)
- 需要布局内绑定表达式时用 DataBinding
- 如果使用 Jetpack Compose 则无需以上两者
5. 常用布局对比
| 布局 | 特点 | 适用场景 |
|---|---|---|
| ConstraintLayout | 扁平化约束布局,减少嵌套 | 复杂布局首选 |
| LinearLayout | 线性排列(水平/垂直) | 简单线性排列 |
| FrameLayout | 层叠布局 | 单一子 View 或叠加 |
| RelativeLayout | 相对定位 | 已被 ConstraintLayout 替代 |
| CoordinatorLayout | 协调子 View 行为 | 配合 AppBar 折叠效果 |
性能注意
布局嵌套层级越深,测量/布局耗时越长。应尽量使用 ConstraintLayout 实现扁平化布局,避免超过 3-4 层嵌套。
常见面试问题
Q1: requestLayout 和 invalidate 的区别?
答案:
invalidate():标记 View 需要重绘。只触发onDraw(),不触发onMeasure()和onLayout()。适用于视觉变化(颜色、文字内容)requestLayout():标记 View 需要重新测量和布局。触发完整的onMeasure()→onLayout()→onDraw()流程。适用于尺寸或位置变化
Q2: View.post(Runnable) 为什么能获取到 View 的宽高?
答案:
View.post() 将 Runnable 投递到 View 关联的 Handler 消息队列中。当 View attach 到 Window 后,这个 Runnable 会排在布局完成后执行。此时 measure 和 layout 已经完成,所以可以获取到正确的宽高。
如果 View 尚未 attach,Runnable 会被暂存在 HandlerActionQueue(RunQueue),等到 dispatchAttachedToWindow 时再投递。
Q3: Fragment 中使用 ViewBinding 为什么要在 onDestroyView 置空?
答案:
Fragment 的 View 生命周期和 Fragment 生命周期不同步。当 Fragment 进入回退栈时,View 被销毁(onDestroyView),但 Fragment 实例仍存活。如果不置空 _binding,Binding 对象会持有已销毁的 View 引用,导致内存泄漏。
Q4: ConstraintLayout 相比传统布局的优势?
答案:
- 扁平化:一层 ConstraintLayout 可替代多层嵌套的 LinearLayout/RelativeLayout,减少 View 层级
- 性能:更少的嵌套意味着更少的 measure/layout 遍历
- Guideline/Barrier/Chain:提供丰富的辅助布局工具
- 百分比布局:支持
layout_constraintWidth_percent等百分比约束 - MotionLayout:ConstraintLayout 的子类,支持复杂的动画过渡
Q5: 什么是过度绘制?如何检测和优化?
答案:
过度绘制(Overdraw) 是指同一个像素被多次绘制。开发者选项中可开启 "Debug GPU Overdraw",通过颜色标识绘制次数(蓝→绿→粉→红)。
优化方法:
- 去除不必要的背景色(Window 背景、ViewGroup 背景)
- 使用
clipRect()限制绘制区域 - 使用 ConstraintLayout 减少布局层级
canvas.quickReject()跳过不可见区域