29、内存与性能平衡:时间换空间,空间换时间,缓存策略选择

做Android开发久了,你会发现一个很有意思的现象——内存和性能,就像跷跷板的两头。你压下去一头,另一头就翘起来。说白了,这就是我们常说的「时间换空间」和「空间换时间」。

我个人习惯把这个问题想得简单一点:你要快,就得舍得给内存;你要省内存,就得接受慢一点。没有银弹,只有权衡。

29.1 时间换空间:压缩与懒加载

什么叫时间换空间?就是用更多的CPU计算时间,来换取更少的内存占用。我举个例子你就明白了。

你在做一个图片列表,用户一口气滑了200张图。如果每张图都加载原图到内存里,那内存直接炸了。怎么办?压缩。把图片从4MB压缩到200KB,内存占用降了20倍,但代价是每次显示前都要做一次解码运算。

核心思路: 数据不提前准备,用的时候再算。或者把大东西变小,但计算量增加。

常见的「时间换空间」策略:

  • 图片压缩:BitmapFactory.Options 里设置 inSampleSize,牺牲解码速度换内存
  • 懒加载:ViewPager + Fragment 的懒加载模式,页面滑到才初始化
  • 数据序列化:对象转JSON存本地,用的时候再反序列化
  • 按需计算:比如列表里某个复杂字段,只在显示时才计算

我在项目中遇到过一个问题:一个列表页加载了500条数据,每条数据都带一个很大的JSON字段。一开始我图省事,全部解析成对象存内存里。结果你猜怎么着?内存直接飙到200MB,低端机直接闪退。后来改成懒解析——只在用户点击某条数据时才解析它的JSON字段。内存降到40MB,虽然点击时会有几百毫秒的等待,但用户感知不强,这个交换是值得的。

我的建议: 时间换空间适合那些「不常用但占内存大」的数据。如果用户每次操作都要等,那就不划算了。

29.2 空间换时间:缓存与预加载

反过来,空间换时间就更好理解了——多占点内存,换来更快的响应速度

你想想看,为什么我们要用缓存?因为从内存里读数据比从磁盘读快1000倍,比从网络读快100万倍。多花几十KB内存,换来毫秒级的响应,这笔账怎么算都划算。

常见的「空间换时间」策略:

  • 内存缓存:LruCache、HashMap 存计算结果或网络响应
  • 对象池:复用Message、Handler、线程池里的对象,避免频繁创建销毁
  • 预加载:ViewPager 默认预加载相邻页面,滑到的时候直接展示
  • 数据冗余:比如把计算好的布局参数存起来,下次直接用

我记得有一次做聊天列表,每条消息都要计算时间格式("刚刚"、"3分钟前"、"昨天")。一开始每次刷新都重新算,列表滑动时肉眼可见的卡顿。后来我加了一个缓存,把计算结果存到HashMap里,只有消息时间变了才重新算。内存多了几十KB,但滑动流畅度从30帧直接拉到60帧。

注意: 空间换时间不是无脑堆内存。你得有个上限。我见过有人把整个图片列表都缓存到内存里,结果OOM了。用LruCache,设置好最大容量,超出就淘汰。

29.3 缓存策略的选择:LRU、LFU、FIFO

说到缓存,就绕不开淘汰策略。你不可能把所有东西都留在内存里,总得有个「谁该走」的规则。

Android里最常用的是LRU(最近最少使用)。LruCache 这个类你应该不陌生。它的逻辑很简单:当缓存满了,优先淘汰那些「最久没被用过」的数据。

// 一个典型的 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; // 按KB计算大小
    }
};

为什么Android官方推荐LRU?因为大多数场景下,刚刚用过的数据,接下来很可能还会再用。比如你滑动列表,刚滑过的item可能马上滑回来。

其他策略也有用武之地:

策略 原理 适用场景
LRU(最近最少使用) 淘汰最久未访问的数据 通用场景,列表缓存、图片缓存
LFU(最不经常使用) 淘汰访问次数最少的数据 热点数据集中,比如排行榜
FIFO(先进先出) 淘汰最早进入的数据 数据时效性强,比如新闻列表

我曾经踩过一个坑:用FIFO做图片缓存。结果用户快速滑动列表时,刚缓存的图片还没用上就被新图片挤走了,缓存命中率极低。后来换成LRU,命中率从20%提升到70%。所以策略选不对,缓存等于白做。

一句话总结: 大多数场景无脑选LRU。如果你能确定数据的访问模式,再考虑LFU或FIFO。

29.4 实战中的平衡点

说了这么多,到底怎么选?我个人的经验是:先测量,再决策

别上来就拍脑袋说「我要用空间换时间」。你得先知道:

  • 当前内存占用是多少?有没有余量?
  • 用户操作的延迟瓶颈在哪?是IO?是计算?还是网络?
  • 这个数据被访问的频率有多高?是一次性的还是反复用的?

举个例子。你在做一个图片编辑功能,用户要加滤镜。滤镜计算很耗时,但用户可能反复试不同滤镜。这时候怎么做?

我的做法是:两层缓存。第一层是内存缓存(空间换时间),存最近5张滤镜效果图,用户切换滤镜时秒出。第二层是磁盘缓存(时间换空间),存更多历史效果,但加载时有几百毫秒延迟。用户快速切换时走内存,偶尔翻旧图时走磁盘。这样既保证了流畅度,又没让内存爆炸。

一个小技巧: 用 Profiler 先跑一遍,看看内存和CPU的瓶颈在哪。别靠猜,靠数据说话。

29.5 知识体系图

下面这张图帮你理清今天讲的核心逻辑:

内存与性能平衡 时间换空间 图片压缩 懒加载 按需计算 空间换时间 内存缓存 对象池 预加载 缓存策略:LRU / LFU / FIFO

嗯,这张图把今天的内容串起来了。左边是时间换空间,右边是空间换时间,底部是缓存策略。你实际做项目时,就是在这些选项里做组合。

最后说一句:没有绝对正确的选择,只有适合当前场景的选择。多测量、多对比,慢慢你就会有感觉了。


公众号:蓝海资料掘金营,微信deep3321