布局优化(下):ViewStub延迟加载、merge标签合并、include复用、自定义View优化
上一章我们聊了布局层级和绘制原理,算是把「地基」打牢了。这一章咱们来点实战技巧——那些能直接塞进项目里、立竿见影的优化手段。
说实话,我早期做项目时,布局文件写得那叫一个「豪放」。一个Activity的XML能堆到七八百行,各种嵌套、各种重复。后来被性能问题折磨得够呛,才老老实实把这些技巧捡起来。嗯,咱们一个一个说。
1. ViewStub:别急着加载
有些布局,不是一进页面就要显示的。比如网络加载失败的提示、用户引导浮层、或者某些「高级设置」面板。这些玩意儿如果一开始就inflate进来,纯属浪费。
ViewStub就是干这个的。它本身不占布局空间,也不参与绘制。只有当你调用 inflate() 或 setVisibility(View.VISIBLE) 时,它才会把真正的布局加载进来。
看个例子:
<!-- activity_main.xml -->
<LinearLayout ...>
<!-- 其他内容 -->
<ViewStub
android:id="@+id/stub_error"
android:layout="@layout/layout_error"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
在代码里触发加载:
val stub = findViewById<ViewStub>(R.id.stub_error)
stub.inflate() // 或者 stub.visibility = View.VISIBLE
我个人习惯用 inflate(),因为它会返回目标布局的根View,方便后续操作。比如:
val errorView = stub.inflate() as ViewGroup
errorView.findViewById<Button>(R.id.btn_retry).setOnClickListener { ... }
findViewById 去查目标布局的根节点是否存在——但说实话,我更喜欢用一个布尔变量自己记着,简单粗暴。
2. merge标签:干掉多余的嵌套
这个标签我特别喜欢。它不生成任何View节点,只是把子布局「合并」到父布局中。说白了,就是帮你省掉一层没用的布局容器。
最常见的场景:你写了一个自定义布局,根节点是 LinearLayout,然后你在另一个布局里用 include 把它引进来。如果include的父布局也是 LinearLayout,那这两层LinearLayout就是多余的。
举个例子:
<!-- my_custom_view.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="描述" />
</merge>
然后在主布局里:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/my_custom_view" />
</LinearLayout>
这样,两个TextView就直接挂到了外层的LinearLayout下,中间没有多余的节点。你想想看,如果不用merge,那就会多一层LinearLayout,白白增加测量和布局的开销。
android:layout_gravity 或 android:layout_ 开头的属性——因为这些属性需要父布局来解析,而merge本身没有父布局的概念。
我曾经在一个项目里,把一个复杂的列表项布局用merge重构了一遍,结果布局层级从5层降到了3层。列表滑动流畅了不少。嗯,这种优化虽然不起眼,但积少成多。
3. include复用:别重复造轮子
这个大家应该都用过。但我想说的是——include不只是复制粘贴的替代品。它还能帮你统一管理样式和逻辑。
比如你的App里有很多页面都有「加载中」的动画。你写一个 layout_loading.xml,然后在各个页面里include进来。哪天想换动画了,只改一个文件就行。
<!-- layout_loading.xml -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp" />
</FrameLayout>
include的时候,你还可以覆盖一些属性:
<include
layout="@layout/layout_loading"
android:layout_width="match_parent"
android:layout_height="200dp"
android:visibility="gone" />
注意:只有 android:layout_ 开头的属性可以被覆盖。而且前提是include的根布局支持这些属性。
4. 自定义View优化:从根上解决问题
有时候,系统提供的布局容器和控件不够用,或者性能不够好。这时候就得自己动手写自定义View了。
但自定义View不是随便写的。我见过不少开发者,一上来就重写 onDraw,然后在里面new对象、做复杂的计算。结果界面卡成PPT。
这里我总结几个关键点:
- 减少onDraw中的对象创建。每次绘制都new一个Paint或Path?别这样。把它们提到成员变量里,复用。
- 避免在onDraw中做耗时计算。比如字符串拼接、正则匹配、文件读写——这些都不该出现在绘制方法里。
- 使用硬件加速。如果你的自定义View涉及复杂的图形变换,记得开启硬件加速(默认是开的,但有些操作会关闭它)。
- 合理使用clipRect。只绘制可见区域,能省不少GPU开销。
举个例子,一个简单的圆形头像自定义View:
class CircleImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rectF = RectF()
override fun onDraw(canvas: Canvas) {
// 复用paint和rectF,不在onDraw里new对象
rectF.set(0f, 0f, width.toFloat(), height.toFloat())
canvas.drawRoundRect(rectF, width / 2f, height / 2f, paint)
super.onDraw(canvas)
}
}
你看,paint和rectF都是成员变量,只初始化一次。onDraw里只是设置参数和调用绘制方法。这样性能就好很多。
Path 做复杂的贝塞尔曲线动画,结果在低端机上掉帧严重。后来发现是每次onDraw都重新计算Path路径。改成缓存Path后,帧率从20fps直接拉到了55fps。嗯,教训深刻。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个「决策流程图」:
说白了,布局优化没有银弹。你得根据实际情况,组合使用这些技巧。ViewStub解决「要不要现在加载」的问题,merge解决「能不能少一层」的问题,include解决「能不能复用」的问题,自定义View解决「能不能自己控制」的问题。
我个人的经验是:先做减法,再做加法。先把布局层级减到最少,把重复的布局抽出来,然后再考虑要不要写自定义View。别一上来就搞复杂了。
公众号:蓝海资料掘金营,微信deep3321