Bitmap内存优化:从原理到实战
Bitmap优化,说白了就是跟图片的「体重」做斗争。我早期做社交App时,一张1080p的图片直接撑爆了内存——那时候我才真正意识到,Bitmap的内存管理不是锦上添花,而是生死攸关。
Bitmap在Android中的内存计算
先搞清楚Bitmap到底吃多少内存。公式其实很简单:
内存占用 = 宽度 × 高度 × 每个像素占用的字节数
关键就在「每个像素占用的字节数」上。Android里最常用的两种格式:
| 格式 | 每像素字节数 | 色彩深度 | 典型场景 |
|---|---|---|---|
| ARGB_8888 | 4字节 | 32位(8位Alpha + 8位R + 8位G + 8位B) | 照片、需要透明度 |
| RGB_565 | 2字节 | 16位(5位R + 6位G + 5位B) | 无透明度的图片、缩略图 |
举个例子你就明白了。一张1920×1080的图片:
- ARGB_8888:1920 × 1080 × 4 = 8,294,400字节 ≈ 7.9MB
- RGB_565:1920 × 1080 × 2 = 4,147,200字节 ≈ 3.95MB
内存直接差了一倍。我在项目中遇到过这样的情况:一个图片列表页,用ARGB_8888加载20张图,内存直接飙到150MB+。换成RGB_565后,降到了80MB左右——而且用户根本看不出区别,因为那些图本身就不需要透明度。
图片压缩策略:三种武器
压缩不是玄学,是工程。我一般把压缩策略分成三个层次:
1. 质量压缩
质量压缩改变的是图片的「编码质量」,不改变像素尺寸。说白了就是降低JPEG的压缩率,或者PNG的量化级别。
// 质量压缩示例
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
originalBitmap.compress(Bitmap.CompressFormat.JPEG, 70, baos);
// 第二个参数70表示质量保留70%
byte[] compressedData = baos.toByteArray();
我建议:质量值设在60-80之间比较稳妥。低于60,肉眼就能看出块状失真了。
2. 采样压缩
采样压缩才是真正的「减重利器」。它通过降低图片的分辨率来减少内存占用。核心API是BitmapFactory.Options里的inSampleSize。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 先只读尺寸
BitmapFactory.decodeResource(getResources(), R.drawable.photo, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
int targetWidth = 480; // 目标宽度
// 计算采样率
int sampleSize = 1;
while (imageWidth / sampleSize > targetWidth) {
sampleSize *= 2; // 必须是2的幂
}
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 配合格式优化
Bitmap sampledBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo, options);
这里有个坑:inSampleSize必须是2的幂。如果你设成3,系统会向下取整到2。我曾经因为这个踩过坑——以为采样了3倍,结果实际只采样了2倍,内存没降到位。
3. Luban算法
Luban算法是微信团队开源的一种压缩策略。它的核心思路是:在保证视觉质量的前提下,把图片压缩到尽可能小的体积。
Luban的压缩逻辑大致是这样的:
- 判断图片的宽高,如果宽高都小于某个阈值(比如1920×1080),就不压缩
- 如果超出阈值,按比例缩放到目标尺寸
- 再用质量压缩把文件体积压到目标值(比如300KB以内)
说白了,Luban就是「采样压缩 + 质量压缩」的组合拳。我在项目中直接用了Luban的库,效果确实不错——一张5MB的照片,压完不到200KB,在手机上看着还挺清晰。
Glide/Picasso的内存缓存机制
这两个库是Android图片加载的「扛把子」。它们的缓存机制,说白了就是「三级缓存」的变体:内存缓存 → 磁盘缓存 → 网络加载。
Glide的内存缓存
Glide用的是LruCache,默认大小是应用最大内存的1/8。它的缓存key非常精细——不仅包含URL,还包含图片的宽高、变换操作、格式等。这意味着同一张图,不同尺寸的加载请求会缓存成不同的条目。
// Glide默认配置
int memoryCacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
LruCache<Key, Resource> cache = new LruCache<>(memoryCacheSize);
Glide还有一个很聪明的设计:活动资源缓存。正在被使用的图片不会进入LruCache,而是放在一个单独的ActiveResources里。这样避免了正在显示的图片被LruCache淘汰掉。
Picasso的内存缓存
Picasso也用了LruCache,但默认大小是应用最大内存的1/7。它的缓存key相对简单,主要基于URL。这意味着同一张图,不同尺寸的加载请求会复用同一个缓存条目——然后通过Bitmap.createScaledBitmap实时缩放。
这两种策略各有优劣:
| 特性 | Glide | Picasso |
|---|---|---|
| 缓存key粒度 | 细(含尺寸、变换) | 粗(仅URL) |
| 内存占用 | 较高(缓存多份) | 较低(缓存一份) |
| 加载速度 | 快(直接命中缓存) | 稍慢(需要实时缩放) |
| 适用场景 | 图片尺寸多变 | 图片尺寸统一 |
知识体系总览
下面这张图把Bitmap内存优化的核心脉络梳理了一遍:
嗯,Bitmap优化说到底就是三个维度的权衡:内存占用、图片质量、加载速度。你不可能同时把三个都做到极致,但可以根据业务场景找到最优解。
我个人习惯是:先确定图片是否需要透明度,不需要就直接上RGB_565;然后根据显示尺寸做采样压缩;最后用Glide做缓存管理。这套组合拳打下来,大部分场景的内存问题都能解决。
公众号:蓝海资料掘金营,微信deep3321