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 知识体系图
下面这张图帮你理清今天讲的核心逻辑:
嗯,这张图把今天的内容串起来了。左边是时间换空间,右边是空间换时间,底部是缓存策略。你实际做项目时,就是在这些选项里做组合。
最后说一句:没有绝对正确的选择,只有适合当前场景的选择。多测量、多对比,慢慢你就会有感觉了。
公众号:蓝海资料掘金营,微信deep3321