Bitmap高效加载和Cache(一)

时间:2022-08-14 22:43:17

Bitmap是我们开发当中无法避开的,因为它而引起的一系列问题也让人头疼不已,虽然现在我们可以利用开源库来极大程度地避免OOM的产生,但我们还是有必要来花点时间来搞清楚Bitmap。

我们从以下几个方面一次来分析Bitmap,首先我们加载一张图片所占用的内存是由哪些方面因素决定的?再就是我们如何对这些影响内存的因素进行合理化的调整。


内存是如何被占用的

我们在运行时可以通过Bitmap.getByteCount()来获取加载此Bitmap将要消耗的内存。所以通过这个方法入手我们就能知道加载图片是如何消耗内存的,我们来查看源码。

public final int getByteCount() {
return getRowBytes() * getHeight();
}

可以看到消耗内存由getRowBytes和图片高度共同决定,getRowBytes是什么?

public final int getRowBytes() {
return nativeRowBytes(mNativePtr);
}

nativeRowBytes()是底层函数。

size_t SkBitmap::ComputeRowBytes(Config c, int width) {
return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}

static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
return width * SkColorTypeBytesPerPixel(ct);
}
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
size_mismatch_with_SkColorType_enum);

SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
return gSize[ct];
}

我们层层查询发现getRowBytes()是由width宽度和SkBitmapConfigToColorType决定的,这个Type就是Bitmap的显示格式,我们常用的ARGGB_8888表明一个像素占用4个字节。所以我们得到了这样的结论,Bitmap的加载是这样的:显示格式*宽度*高度。

所以我们可以从显示格式和图片尺寸这两方面个进行下手。


Bitmap的高效加载

在上面的分析中,我们知道显示格式和图片尺寸是影响Bitmap加载的重要因素。我们如何将图片加载到Bitmap上,Android提供了BitmapFactory类专门来处理,它提供了一些静态方法来实现此功能,这些方法的区别在于加载图片的来源不同。

//将一个byte数组中的数据从offset位置开始解析length字节作为一个bitmap对象
decodeByteArray(byte[] data, int offset, int lenght);

//把文件pathName解析成bitmap对象
decodeFile(String pathName);

//从给定的流中解析出一个Bitmap对象,加载这个对象到内存中时应用options指定的选项
decodeStream(InputStream is, Rect outPadding, Options opts)

//根据id从给定的资源中解析出一个Bitmap对象,加载这个对象到内存中时应用options指定的选项
decodeResource(Resource res, int id, Options opts);

//从给定的流中解析出一个Bitmap对象
decodeStream(InputStream is);

我们查看上面的静态方法可以看到一个Options类,它是什么?做什么用的呢?实际上,它是BitmapFactory的一个静态内部类,我们通过查看它的属性可以知道,我们可以将它看做对bitmap的属性设置,比如设置bitmap的缩放系数,显示格式等等,通过Options这个类我们可以做很多事情,包括防止OOM。

public static class Options { 

public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}

public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍
public int inSampleSize; //采样率
public boolean inPremultiplied;
public boolean inDither; //是否开启抖动
public int inDensity; //上篇文章中的原始资源density
public int inTargetDensity; //目标屏幕密度
public boolean inScaled; //是否支持缩放
public int outWidth; //图片的原始宽度
public int outHeight; //图片的原始高度
...
}

通过Options内部类可以控制bitmap加载占用内存,那么我们如何进行操作呢?通过查看内部类以及我们之前分析bitmap如何占用的内存,我们发现应该从这几个方面入手。

1. bitmap显示格式

Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

bitmap默认的显示格式是ARGB_8888,从上文的分析中我们知道ARGB_8888占用4字节,那么如果我们将显示格式改为RGB_565占用两个字节的格式,那么bitmap消耗的内存应该就减少一半了。

2. 尺寸

在上文的分析中,我们知道除了显示格式影响bitmap占用的内存,更加具有决定性的因素是图片的宽高,我们想想一下,我们请求一个网络高清大图,其分辨率相当之高,而在我们的app中极有可能只是显示一张缩略图,如果愣头青地去加载一众高清大图,不OOM才怪呢,所以这时候Options中的inSampleSize即采样率出现了。

3. 采样率

当inSampleSize为1时加载原图,当inSampleSize=2时,图片的宽高都是1/2,所以所占内存为1/4。我们一般约定inSampleSize最好是2的整数倍。

我们来举个例子,一张800*800的图加载到imageView上,imageView为200*200,那么缩放系数为4,我们再设置options的显示格式为RGB_565,那么前后对比占用内存是多少呢?
before:4*800*800 = 2.4M;
after: 2*200*200 = 78k;
效果可见一斑。

如果imageView为200*200,但是图片为800*400那么采样率为4还是2呢?我们来假设一下,为4那么图片为200*100,为2图片为400*200,可以看到当为4时,图片200*100要放入到200*200的imageView中去就会被拉伸影响图片质量,所以当宽高采样率不同时,以小的为准。

4. 缩放系数

我们通过查看Options的属性可以看到一个inScaled属性,我们的解释是缩放系数,我们做过开发应该知道,UI给我们的图一般标注的px,我们要进行相应的转换才能去写xml,一般我们按照0.5去开发,为什么这样计算呢?这是因为density的概念,这是个什么东东,去看这篇文章吧,看完你立马就知道这个缩放系数是咋回事了。我们举个例子来探讨缩放系数对内存的影响。

华为xx手机(inTargetDensity=320)加载xxhdpi文件的图片(480)。图片的分辨率850*1203,图片地址

private void test(){
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl);
Log.i("华为手机的显示密度:", getResources().getDisplayMetrics().densityDpi+"");
Log.i("图片加载内存大小:", bitmap.getByteCount()+"b");
Log.i("图片加载内存大小:", bitmap.getByteCount()/1024+"k");
}

消耗内存:850*320/480*1203*320/480*4=1.73M。

Bitmap高效加载和Cache(一)

我们来手动设置显示密度:

private void test(){
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inDensity = 160; // 480改成了160
opts.inTargetDensity = 320; // 不变
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl, opts);
Log.i("华为手机的显示密度:", getResources().getDisplayMetrics().densityDpi+"");
Log.i("图片加载内存大小:", bitmap.getByteCount()+"b");
Log.i("图片加载内存大小:", bitmap.getByteCount()/1024+"k");
}

那么,850*320/160*1203*320/160*4=15.6M,所以消耗内存变大了,缩放系数是原来的3倍则消耗内存为原来的9倍。

Bitmap高效加载和Cache(一)

一般情况下,我们不处理缩放系数。

5. 干货在此

说了这么多,给个常见处理方案吧。

Resources res;  // 资源文件
int resId; // 资源文件id
int imageW; // imageView宽
int imageH; // imageView高

final BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = getSampleSize(options);
options.inJustDecodeBounds = false;

options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory,decodeResource(res, resId, options);

// 计算采样率
public int getSampleSize(BitmapFactory.Options options){
final int width = options.outWidth; //图片原始宽度
final int height = options.outHeight; //图片原始高度
int inSampleSize = 1;
if( width>imageW || height>imageH ){
final int halfW = width/2;
final int halfH = height/2;
while(
((halfW/inSampleSize) >= imageW) &&
((halfH/inSampleSize) >= imageH )){
inSampleSize *= 2;
}
}
return inSampleSize;
}

上面的代码只有一点你可能会有所疑惑,就是我们加载了两次bitmap,分别是options.inJustDecodeBounds设置了true和false之后各加载了一次,加载两次是失了智?

并不是,其实我们从这个变量的名称上也能看出当inJustDecodeBounds设置为true时,我们并不是真正地加载图片,而仅仅会解析一些原始信息(比如宽高),所以这个操作是极其轻量级的,我们获取宽高的目的是为了计算出采样率。

当计算出采样率后,将inJustDecodeBounds设置为false,然后再真正地去加载图片,此时加载的bitmap是经过我们一系列瘦身后的bitmap,从而达到了高效加载的初衷。

实际上,我们还有更多的方式让加载bitmap变得高效,比如缓存,下一篇文章再来看吧。