概述
Bitmap 经常用于 安卓app中图片处理。同时它也是 内存消耗的大户,当bitmap要使用的内存空间超过了剩余可用内存,就会报OOM。
内存占用分析
Bitmap用来描述一张图片的宽高颜色等信息。 我们可以使用BitmapFactory来将某路径下的一张图片转化成bitmap对象。
那么具体如何转化,以及转化之后会占用多大内存呢?
将一张图片test.png
放在 res/mipmap,然后用以下代码来加载图片并打印其占用空间:
java
// 图片资源转化为内存中的bitmap对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", bitmap.getAllocationByteCount());
// 将bitmap对象设置给imageView
imageView.setImageBitmap(bitmap);
默认情况下,转化图片资源时采用的图片编码Config是 ARGB8888
. 在这种情况下,每个像素占用的字节是4个
。
所以一个 440 x 423 的 图片,最终占用的内存空间会是 440 x 423 x 4 x 9
= 6700320 byte
屏幕自适应
在代码不动的前提下,如果将图片移动到 mipmap-xhdpi
下,再次计算图片大小:
同样还是这张图 : 440 x 423
的 图片,最终占用的内存空间会是 1676400 byte
。
实际上,BitmapFactory
的图片解析过程,与图片所在目录dpi密度
有关。
具体公式为:
- 缩放比例 scale = 当前设备屏幕密度 / 图片所在dpi密度
- bitmap实际大小 = 宽 * scale * 高 * scale * Config存储像素数(ARGB_8888 每个像素占用的字节是
4个
)
加载assets中的图片
java
InputStream open = getAssets().open("test.png");
Bitmap bitmap = BitmapFactory.decodeStream(open);
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));
打印结果为: 744480 (440 x 423 x 4
)
由此可见,图片放在 drawable/mipmap 下 的任何目录,通过 bitmapFactory去加载时都会有 不同程度的缩放。 但是放在 assets中就能保持原图大小。
图片加载优化
由于上面所说的sacle参数的问题,比如一个65K的图,加载到内存之后,就有可能占用 2.5M的空间。此时,就要适当对bitmap的加载做出优化。有以下方式:
修改Config
默认的存储像素参数:
上面所说的 ARGB_8888 每个像素占用4个字节。 而 RGB_565 每个像素占用2个字节。
java
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));
这样就可以节省一半的空间。但是代价就是:
使得图像的质量下降。这是因为 RGB_565 只有16位,只有5位用于红色通道,6位用于绿色通道,5位用于蓝色通道。相比之下,ARGB_8888 则具有更高的色彩深度,即8位,每个颜色通道具有256个可能值。
另外,由于 RGB_565 无法支持透明度通道,所以对于需要使用透明度的图像来说,使用 RGB_565 格式就不再适用。
修改采样密度
java
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 修改采样密度,宽高上都每隔2个像素采样一次
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));
options.inSampleSize = 2;
这句话,修改采样密度,宽高上都每隔2个像素采样一次。最终结果就是图片的大小缩小为原来的1/4
bitmap复用
Bitmap是吃内存的大户。所以,如果一个安卓的页面上存在大量的bitmap实例,并且存在频繁的内存申请和回收,就会造成app内存吃紧,内存抖动,交互卡顿。
比如下面的页面:
java
public class MainActivity2 extends AppCompatActivity {
private ActivityMainBinding binding;
private int count;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
initView();
load();
}
private void initView() {
binding.imageView.setOnClickListener(v -> {
count++;
load();
});
}
private void load() {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), count % 2 == 1 ? R.drawable.test : R.drawable.test2);
binding.imageView.setImageBitmap(bitmap);
}
}
频繁切换之后的内存情况如下:
此时,要做出优化,
java
public class MainActivity2 extends AppCompatActivity {
private ActivityMainBinding binding;
private int count;
private Bitmap lastBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
initView();
load();
}
private void initView() {
binding.imageView.setOnClickListener(v -> {
count++;
load();
});
}
private void load() {
// 根据 count 的奇偶性选择不同的资源
int resId = count % 2 == 1 ? R.drawable.test : R.drawable.test2;
// 复用上一次的 Bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inBitmap = lastBitmap;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
lastBitmap = bitmap; // 保存当前的 Bitmap
binding.imageView.setImageBitmap(bitmap);
}
}
复用的做法为:
- 创建一个 BitmapFactory.Options,设置 isMutable=true,inBitmap=lastBitmap
- 尝试加载 BitmapFactory.decodeResource 加载bitmap
- 将加载后的bitmap赋值给 lastBitmap
优化之后,频繁切换bitmap,内存情况如下:
但是这种复用有一定的条件,那就是:
- 新的bitmap所占用的空间一定要不能大于 已有的bitmap。否则就会重新创建bitmap。
- isMutable必须是true
超大图的加载
有的长图,或者特别精密的图片,不建议一次性全部加载到内存,否则用户会感觉到等待很久图片都没加载完毕。而是采用分片加载的方式,结合放大手势分区块加载。
此时就要用到 BitmapRegionDecoder
.
基本使用如下:
java
InputStream open = getAssets().open("test2.png");
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(open, false);
BitmapFactory.Options option = new BitmapFactory.Options();
Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, 300, 100), option);
几个关键点:
- 创建 BitmapRegionDecoder 时使用静态newInstance方法,支持传入 绝对路径,文件描述符,输入流的方式
- BitmapRegionDecoder的decodeRegion方法的第一个参数是一个矩形 Rect实例,它确定了本次要加载的图片范围。
可以通过自定义View,结合 BitmapRegionDecoder来动态的 获取不同Rect范围内的bitmap对象。结合滑动手势,就能实现图片的分步加载。
图片缓存
如果在列表的item中要用到 bitmap,比如RecyclerView,上下滑动,不停地去bindViewHolder,就有可能重复创建bitmap,此时,引入缓存机制,就有可能造成大量的内存分配和回收。比较成熟的方式是LRU策略。
- 手动指定Bitmap能占用的最大空间(上图中是20M)
- 用 图片名 和bitmap对象的对应关系,在recyclerView中去调用 getBitmapFromCache来获取bitmap对象,避免重复创建
- 当缓存空间满载之后,如果再次 addBitmapToCache,那么 Lru缓存池就会自动清除相对不常使用的bitmap
总结
本文讲述了
- 一张图被加载成bitmap之后的实际占用内存的计算方式
- 通过Bitmap.Option.isBitmap可以实现 bitmap对象的复用
- 当图片超大的时候,采用 BitmapRegionDecoder 来分片分布加载
- 当界面显示多张图或者列表中显示很多图的时候,可以采用缓存算法