性能优化(上):布局优化、内存优化、图片优化

性能优化这个话题,说大不大,说小不小。我见过太多项目,功能做得很全,但一跑起来就卡顿、闪退、OOM。说白了,用户不会关心你用了多酷的架构,他只关心App滑起来顺不顺、会不会崩。今天我们就来聊聊性能优化的三个核心方向:布局、内存、图片。

核心观点:性能优化不是后期补丁,而是贯穿开发始终的设计思维。

Android 性能优化三大支柱 布局优化 • Hierarchy Viewer • ConstraintLayout • 减少嵌套层级 • 使用 merge/ViewStub • 避免过度绘制 • 异步布局 内存优化 • LeakCanary • Android Profiler • 内存泄漏检测 • 大对象回收 • 弱引用使用 • 避免静态集合 图片优化 • Bitmap压缩 • 三级缓存策略 • 内存缓存 LruCache • 磁盘缓存 DiskLruCache • 图片尺寸适配 • Glide/Coil 框架 三者相互影响,共同决定 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 三级缓存策略

三级缓存是图片加载的经典策略:

  1. 内存缓存:使用 LruCache,读取最快,但容量有限
  2. 磁盘缓存:使用 DiskLruCache,速度次之,容量较大
  3. 网络加载:速度最慢,只在本地没有缓存时使用

为什么需要三级缓存?你想想看,如果每次滑动列表都从网络加载图片,那流量和速度都受不了。如果只做内存缓存,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