性能优化(上):布局优化、内存优化、图片优化
性能优化这个话题,说大不大,说小不小。我见过太多项目,功能做得很全,但一跑起来就卡顿、闪退、OOM。说白了,用户不会关心你用了多酷的架构,他只关心App滑起来顺不顺、会不会崩。今天我们就来聊聊性能优化的三个核心方向:布局、内存、图片。
核心观点:性能优化不是后期补丁,而是贯穿开发始终的设计思维。
一、布局优化:别让界面成为性能瓶颈
布局优化是我每次做性能排查时第一个下手的地方。为什么?因为布局是用户最先感知到的部分。一个卡顿的启动页,足以让用户在第一秒就失去耐心。
1.1 Hierarchy Viewer:看清你的布局树
Hierarchy Viewer 是 Android Studio 自带的布局分析工具。我个人习惯在开发阶段就频繁使用它,而不是等到出了问题再查。
怎么用?很简单:
- 连接设备或模拟器,打开你的页面
- Tools → Layout Inspector
- 查看布局层级树,红色节点就是性能瓶颈
我的经验:有一次排查一个列表卡顿问题,用 Hierarchy Viewer 一看,发现一个 item 竟然嵌套了 8 层 LinearLayout。去掉冗余布局后,列表滑动流畅度提升了 40%。
1.2 ConstraintLayout:扁平化布局的利器
ConstraintLayout 的出现,说白了就是为了解决布局嵌套过深的问题。它用相对约束关系替代了多层嵌套,一个 ConstraintLayout 能搞定以前需要 3-4 层布局才能实现的效果。
<!-- 传统嵌套方式 -->
<LinearLayout>
<LinearLayout>
<TextView />
<Button />
</LinearLayout>
</LinearLayout>
<!-- ConstraintLayout 方式 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
注意:ConstraintLayout 虽好,但不要滥用。如果一个布局只有两三个控件,用 LinearLayout 反而更清晰。我见过有人为了用 ConstraintLayout 而用,结果约束关系写得比嵌套还复杂。
1.3 布局优化的其他技巧
- 使用 merge 标签:减少一层不必要的 ViewGroup 嵌套
- ViewStub 延迟加载:不常用的布局,比如引导弹窗,用 ViewStub 按需加载
- 避免过度绘制:检查是否有重叠的背景色,去掉不必要的背景设置
- 异步布局:对于复杂页面,可以考虑使用 AsyncLayoutInflater
避坑指南:我曾经在一个项目中,为了追求"完美"的 UI 效果,在 RecyclerView 的 item 里用了三层嵌套。结果在低端机上滑动时明显掉帧。后来用 Hierarchy Viewer 一查,发现每层都有不必要的背景绘制。去掉后,问题解决。
二、内存优化:别让 App 成为内存杀手
内存优化,说白了就是管好你的对象生命周期。Android 给每个 App 分配的内存是有限的,尤其是低端机,可能只有 64MB 甚至更少。你想想看,一张 1920x1080 的图片加载到内存里,就要占 8MB 左右。稍微不注意,OOM 就来了。
2.1 LeakCanary:内存泄漏的照妖镜
LeakCanary 是我在每个项目里都会集成的库。它能在内存泄漏发生时自动弹出通知,告诉你哪个对象泄漏了、泄漏路径是什么。
集成方式:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
就这么一行。运行 App,正常使用,如果有泄漏,通知栏会提示。点进去就能看到详细的引用链。
我的习惯:在开发阶段,我会把 LeakCanary 的检测阈值调低,让它在泄漏发生的第一时间就报警。上线前再恢复默认配置,避免影响用户体验。
2.2 Android Profiler:实时监控内存
Android Profiler 是 Android Studio 自带的性能分析工具。它可以实时查看内存使用情况、CPU 占用、网络请求等。
使用场景:
- 观察内存曲线是否平稳,有没有异常波动
- 执行某个操作后,内存是否回落到正常水平
- 录制一段操作,分析内存分配情况
注意:Profiler 本身会消耗一定性能,不要在用户真机上长时间开启。我一般是在开发机上用模拟器做分析。
2.3 常见内存泄漏场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 静态变量持有 Activity | Activity 销毁后仍被静态引用 | 使用弱引用,或在 onDestroy 中置空 |
| Handler 未移除 | Handler 持有外部类引用 | 使用静态内部类 + 弱引用 |
| 匿名内部类 | 隐式持有外部类引用 | 改为静态内部类 |
| 未取消的注册 | BroadcastReceiver 未解注册 | 在 onDestroy 中 unregister |
避坑指南:我曾经遇到过一个诡异的 Bug——App 在后台运行一段时间后就会闪退。用 LeakCanary 一查,发现是一个单例的图片加载器持有了所有 Activity 的引用。原因是图片加载的回调用了匿名内部类,而匿名内部类隐式持有外部 Activity 的引用。改成静态内部类后,问题解决。
三、图片优化:吃内存的大户
图片是 Android 内存消耗的最大来源之一。一张未经处理的图片,加载到内存中占用的空间是原始文件大小的几十倍。举个例子,一张 2MB 的 JPEG 图片,解码成 Bitmap 后可能占用 20MB 甚至更多。
3.1 Bitmap 压缩
Bitmap 压缩主要有两种方式:
- 质量压缩:改变图片的存储质量,适合 JPEG 格式。不会改变图片尺寸。
- 尺寸压缩:缩小图片的宽高,直接减少内存占用。
// 质量压缩
Bitmap original = BitmapFactory.decodeResource(getResources(), R.drawable.image);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
original.compress(Bitmap.CompressFormat.JPEG, 80, baos); // 80% 质量
// 尺寸压缩 - 使用 inSampleSize
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 宽高各缩小一半
Bitmap scaled = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
我的建议:不要直接使用原图。在加载图片前,先获取图片的原始尺寸,再根据 ImageView 的实际大小计算合适的采样率。Glide 和 Coil 这些框架已经帮我们做了这件事。
3.2 三级缓存策略
三级缓存是图片加载的经典策略:
- 内存缓存:使用 LruCache,读取最快,但容量有限
- 磁盘缓存:使用 DiskLruCache,速度次之,容量较大
- 网络加载:速度最慢,只在本地没有缓存时使用
为什么需要三级缓存?你想想看,如果每次滑动列表都从网络加载图片,那流量和速度都受不了。如果只做内存缓存,App 退出后缓存就没了。磁盘缓存则能持久化保存。
// LruCache 简单使用
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8; // 分配 1/8 内存给图片缓存
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
注意:LruCache 的容量要根据设备内存动态计算,不要写死。我见过有人写死 50MB,结果在 512MB 内存的旧手机上直接 OOM。
3.3 图片加载框架的选择
现在很少有人自己写图片加载了。主流框架有 Glide、Coil、Picasso 等。我个人推荐 Glide,因为它对生命周期管理做得很完善,能自动在 Activity 销毁时取消加载任务。
| 框架 | 特点 | 适用场景 |
|---|---|---|
| Glide | 生命周期感知、支持 GIF、缓存策略完善 | 大多数项目 |
| Coil | Kotlin 原生、协程支持、体积小 | Kotlin 项目 |
| Picasso | 轻量、API 简洁 | 简单图片加载场景 |
避坑指南:我曾经接手过一个项目,里面自己实现了一套图片加载逻辑,没有做内存缓存,也没有做尺寸压缩。结果在图片较多的页面上,每次滑动都会触发 GC,导致界面卡顿。后来替换成 Glide,问题迎刃而解。所以,除非你有特殊需求,否则直接用成熟的框架就好。
总结
性能优化不是一蹴而就的事。布局优化让你界面流畅,内存优化让你 App 稳定,图片优化让你省内存省流量。这三者相辅相成,缺一不可。
嗯,今天的内容就到这里。记住一句话:好的性能是设计出来的,不是调试出来的。
公众号:蓝海资料掘金营,微信deep3321