跳到主要内容

View 体系与 ViewBinding

问题

Android View 体系的层级结构是怎样的?ViewBinding 和 DataBinding 有什么区别?

答案

1. View 继承体系

  • View:所有 UI 元素的基类,负责绘制和事件处理
  • ViewGroup:View 的子类,可以包含子 View,负责子 View 的测量和布局

2. MeasureSpec

MeasureSpec 是父 View 传递给子 View 的测量约束,由 modesize 组合而成:

Mode含义对应 XML
EXACTLY精确尺寸match_parent 或固定 dp
AT_MOST最大不超过 sizewrap_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

特性findViewByIdViewBindingDataBinding
空安全❌ 可能返回 null✅ 编译时检查✅ 编译时检查
类型安全❌ 需手动转型✅ 自动✅ 自动
性能运行时遍历 View 树编译时生成编译时生成
布局表达式@{viewModel.name}
双向绑定@={viewModel.text}
编译速度无影响影响很小明显增加
选择建议
  • 新项目推荐 ViewBinding(简单、轻量)
  • 需要布局内绑定表达式时用 DataBinding
  • 如果使用 Jetpack Compose 则无需以上两者

5. 常用布局对比

布局特点适用场景
ConstraintLayout扁平化约束布局,减少嵌套复杂布局首选
LinearLayout线性排列(水平/垂直)简单线性排列
FrameLayout层叠布局单一子 View 或叠加
RelativeLayout相对定位已被 ConstraintLayout 替代
CoordinatorLayout协调子 View 行为配合 AppBar 折叠效果
性能注意

布局嵌套层级越深,测量/布局耗时越长。应尽量使用 ConstraintLayout 实现扁平化布局,避免超过 3-4 层嵌套。


常见面试问题

Q1: requestLayoutinvalidate 的区别?

答案

  • invalidate():标记 View 需要重绘。只触发 onDraw(),不触发 onMeasure()onLayout()。适用于视觉变化(颜色、文字内容)
  • requestLayout():标记 View 需要重新测量和布局。触发完整的 onMeasure()onLayout()onDraw() 流程。适用于尺寸或位置变化

Q2: View.post(Runnable) 为什么能获取到 View 的宽高?

答案

View.post() 将 Runnable 投递到 View 关联的 Handler 消息队列中。当 View attach 到 Window 后,这个 Runnable 会排在布局完成后执行。此时 measurelayout 已经完成,所以可以获取到正确的宽高。

如果 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() 跳过不可见区域

相关链接