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左右——而且用户根本看不出区别,因为那些图本身就不需要透明度。

核心结论:不需要透明通道的图片,优先用RGB_565。别让用户为你看不到的Alpha通道买单。

图片压缩策略:三种武器

压缩不是玄学,是工程。我一般把压缩策略分成三个层次:

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的压缩逻辑大致是这样的:

  1. 判断图片的宽高,如果宽高都小于某个阈值(比如1920×1080),就不压缩
  2. 如果超出阈值,按比例缩放到目标尺寸
  3. 再用质量压缩把文件体积压到目标值(比如300KB以内)

说白了,Luban就是「采样压缩 + 质量压缩」的组合拳。我在项目中直接用了Luban的库,效果确实不错——一张5MB的照片,压完不到200KB,在手机上看着还挺清晰。

我的习惯:头像、缩略图用RGB_565 + 采样压缩;详情页大图用ARGB_8888 + Luban算法。别一刀切,不同场景用不同策略。

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)
内存占用 较高(缓存多份) 较低(缓存一份)
加载速度 快(直接命中缓存) 稍慢(需要实时缩放)
适用场景 图片尺寸多变 图片尺寸统一
我曾经踩过的坑:在一个瀑布流页面里,每张图片的宽高都不一样。用Picasso时,每次都要实时缩放,导致列表滑动卡顿。换成Glide后,因为缓存了不同尺寸的图片,滑动流畅了很多。所以,如果你的图片尺寸变化大,Glide更合适。

知识体系总览

下面这张图把Bitmap内存优化的核心脉络梳理了一遍:

Bitmap内存优化知识体系 Bitmap内存优化 内存计算 ARGB_8888: 4字节/像素 RGB_565: 2字节/像素 公式: 宽×高×字节数 压缩策略 质量压缩: 降低编码质量 采样压缩: 降低分辨率 Luban算法: 组合策略 缓存机制 Glide: 细粒度缓存key Picasso: 粗粒度缓存key LruCache + 活动资源 核心原则:按场景选格式,按需求定压缩,按特点选缓存库 没有银弹,只有权衡

嗯,Bitmap优化说到底就是三个维度的权衡:内存占用、图片质量、加载速度。你不可能同时把三个都做到极致,但可以根据业务场景找到最优解。

我个人习惯是:先确定图片是否需要透明度,不需要就直接上RGB_565;然后根据显示尺寸做采样压缩;最后用Glide做缓存管理。这套组合拳打下来,大部分场景的内存问题都能解决。

一句话总结:Bitmap优化的本质,是用「合适的精度」和「合适的尺寸」,在「合适的时间」展示给用户。多一分浪费,少一分不够。

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