14、图片加载框架:Glide与Fresco的内存优化策略,三级缓存原理

说到Android图片加载,这几乎是每个App都绕不开的坎。我做了这么多年性能优化,见过太多因为图片加载不当导致OOM的案例。今天咱们就聊聊Glide和Fresco这两个主流框架,看看它们是怎么在内存管理上各显神通的。

14.1 三级缓存:图片加载的基石

先说说三级缓存。说白了,就是给图片准备三个不同层级的"仓库",按访问速度和成本排序。

三级缓存结构:

  • 一级缓存(内存缓存):最快,但容量有限。存的是Bitmap对象或解码后的图片数据。
  • 二级缓存(磁盘缓存):速度中等,容量大。存的是压缩后的图片文件。
  • 三级缓存(网络/资源):最慢,但数据源无限。从服务器或本地资源加载原始图片。

加载流程是这样的:先查内存缓存,命中就直接用;没命中就去磁盘缓存找,找到就解码显示并回填到内存;磁盘也没有,才去网络下载,下载后同时写入磁盘和内存。

我当年接手一个相册App,用户翻照片时总是卡顿。一查发现,团队直接把所有图片都塞进内存,没有磁盘缓存。翻到第50张时,前面49张的Bitmap全在内存里躺着,不崩才怪。

三级缓存加载流程 图片请求 一级:内存缓存 LRU算法,最快存取 未命中 二级:磁盘缓存 压缩文件,容量大 未命中 三级:网络/资源 原始数据源 命中回填

14.2 Glide的内存优化策略

Glide是我个人用得最多的框架。它的设计哲学很明确:能省则省,能复用就复用

14.2.1 内存缓存:LruCache + 弱引用

Glide的内存缓存分两层。第一层是LruCache,存的是最近使用的图片。第二层是弱引用缓存,存的是正在使用的图片。

为什么要搞两层?你想想看,如果一张图片正在被某个View显示,这时候LruCache把它淘汰了,但弱引用还在。等View需要重新加载时,直接从弱引用拿,省得再去磁盘读。

我的经验:Glide默认的内存缓存大小是App可用内存的1/8。如果你的App图片特别多,可以适当调大。但别超过1/4,否则容易OOM。

14.2.2 图片复用:BitmapPool

Glide内部维护了一个BitmapPool。当一张图片不再需要时,它不会立刻回收,而是把Bitmap对象放回池子里。下次加载图片时,如果池子里有尺寸合适的Bitmap,就直接拿来用。

这招很聪明。因为Bitmap的创建和回收都很耗时,频繁GC会导致卡顿。复用能减少内存分配次数,也降低GC压力。

// Glide的BitmapPool配置示例
Glide.get(context).getBitmapPool().getMaxSize(); // 获取当前池大小

// 自定义池大小
GlideBuilder builder = new GlideBuilder(context);
builder.setBitmapPool(new LruBitmapPool(20 * 1024 * 1024)); // 20MB

14.2.3 图片缩放:Downsampler

Glide在解码图片时会自动计算采样率。比如一张2000x2000的图片要显示在100x100的ImageView上,Glide会直接把图片缩小到100x100再解码。这样内存里存的Bitmap就是100x100,而不是2000x2000。

我记得有个项目,设计师上传了4000x3000的壁纸图。如果不做缩放,一张图就占48MB内存。Glide自动缩放到屏幕尺寸后,只占不到2MB。差距就是这么明显。

14.3 Fresco的内存优化策略

Fresco是Facebook开源的框架,它的内存管理思路和Glide不太一样。Fresco把图片数据放在Native堆里,而不是Java堆。

14.3.1 三级缓存:Fresco的独特设计

缓存层级 存储位置 存储内容 特点
Bitmap缓存 Java堆 解码后的Bitmap 用于直接显示,容量较小
未解码缓存 Native堆 压缩后的图片数据 不占用Java堆,避免GC
磁盘缓存 文件系统 原始图片文件 持久化存储

Fresco的亮点是未解码缓存。图片数据以压缩格式存在Native堆里,不占用宝贵的Java堆内存。只有真正要显示时,才解码成Bitmap放到Java堆。

为什么这很重要?

Android的GC只管理Java堆。如果大量Bitmap在Java堆里,GC频繁触发,卡顿就来了。Fresco把大部分数据放在Native堆,Java堆的压力小很多,GC次数也少。

14.3.2 图片复用:共享内存

Fresco支持多个View共享同一份图片数据。比如列表里同一个头像出现多次,Fresco不会为每个View都创建一份Bitmap,而是共享同一份数据。

这个特性在聊天App里特别有用。你想想,一个群聊里几百条消息,头像都是那几个人。如果每个头像都单独加载,内存浪费太严重了。

14.3.3 渐进式加载

Fresco支持渐进式JPEG。图片从网络下载时,先显示模糊的缩略图,随着数据增多逐渐变清晰。这不仅是用户体验优化,也是内存优化——因为一开始只需要很小的Bitmap。

我曾经踩过的坑:Fresco的Native堆内存不会自动回收,需要手动调用ImagePipeline.clearMemoryCaches()。有次我在一个H5页面里大量使用Fresco,退出页面后Native堆内存一直不降。后来加上了页面销毁时的清理逻辑,才解决问题。

14.4 Glide vs Fresco:怎么选?

这两个框架各有千秋。我个人的建议是:

  • 普通App、列表场景:选Glide。它轻量、易用,大多数场景下性能足够。
  • 图片密集型App、大图场景:选Fresco。它的Native堆管理对内存更友好,GC更少。
  • 需要WebP、GIF、自定义解码:两个都支持,但Fresco的扩展性更好。

不过说实话,现在Glide的BitmapPool和Downsampler已经做得非常好了。除非你的App对内存极度敏感,否则Glide完全够用。

14.5 避坑指南

最后分享几个我踩过的坑:

1. 不要禁用缓存

我曾经为了测试,把Glide的缓存全关了。结果列表滑动时每张图都要重新加载,卡成PPT。缓存是性能的基石,别轻易动它。

2. 注意图片尺寸

Glide的override()方法可以指定加载尺寸。如果你知道图片要显示多大,一定要设置。否则Glide会按View尺寸来,有时候View还没布局完成,尺寸是0,Glide就会加载原图。

3. 及时清理

Fragment或Activity销毁时,记得取消正在进行的加载请求。Glide的clear()方法会取消请求并释放资源。Fresco的shutdown()会清理所有缓存。

嗯,关于图片加载框架的内存优化,今天就聊到这儿。记住一个核心原则:能不加载就不加载,能复用就复用,能压缩就压缩。做到这三点,你的App内存基本稳了。

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