Bitmap高效加载和Cache(一)
2017-01-09 21:17
274 查看
Bitmap是我们开发当中无法避开的,因为它而引起的一系列问题也让人头疼不已,虽然现在我们可以利用开源库来极大程度地避免OOM的产生,但我们还是有必要来花点时间来搞清楚Bitmap。
我们从以下几个方面一次来分析Bitmap,首先我们加载一张图片所占用的内存是由哪些方面因素决定的?再就是我们如何对这些影响内存的因素进行合理化的调整。
可以看到消耗内存由getRowBytes和图片高度共同决定,getRowBytes是什么?
nativeRowBytes()是底层函数。
我们层层查询发现getRowBytes()是由width宽度和SkBitmapConfigToColorType决定的,这个Type就是Bitmap的显示格式,我们常用的ARGGB_8888表明一个像素占用4个字节。所以我们得到了这样的结论,Bitmap的加载是这样的:显示格式*宽度*高度。
所以我们可以从显示格式和图片尺寸这两方面个进行下手。
我们查看上面的静态方法可以看到一个Options类,它是什么?做什么用的呢?实际上,它是BitmapFactory的一个静态内部类,我们通过查看它的属性可以知道,我们可以将它看做对bitmap的属性设置,比如设置bitmap的缩放系数,显示格式等等,通过Options这个类我们可以做很多事情,包括防止OOM。
通过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,图片地址。
消耗内存:850*320/480*1203*320/480*4=1.73M。
我们来手动设置显示密度:
那么,850*320/160*1203*320/160*4=15.6M,所以消耗内存变大了,缩放系数是原来的3倍则消耗内存为原来的9倍。
一般情况下,我们不处理缩放系数。
5. 干货在此
说了这么多,给个常见处理方案吧。
上面的代码只有一点你可能会有所疑惑,就是我们加载了两次bitmap,分别是options.inJustDecodeBounds设置了true和false之后各加载了一次,加载两次是失了智?
并不是,其实我们从这个变量的名称上也能看出当inJustDecodeBounds设置为true时,我们并不是真正地加载图片,而仅仅会解析一些原始信息(比如宽高),所以这个操作是极其轻量级的,我们获取宽高的目的是为了计算出采样率。
当计算出采样率后,将inJustDecodeBounds设置为false,然后再真正地去加载图片,此时加载的bitmap是经过我们一系列瘦身后的bitmap,从而达到了高效加载的初衷。
实际上,我们还有更多的方式让加载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。
我们来手动设置显示密度:
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倍。
一般情况下,我们不处理缩放系数。
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变得高效,比如缓存,下一篇文章再来看吧。
相关文章推荐
- Bitmap高效加载、Cache和优化(二)
- Bitmap高效加载、Cache和优化(一)
- Bitmap的高效加载和 Cache
- Bitmap的加载和Cache
- Bitmap加载和Cache
- BitMap高效显示策略(二):在ListView上异步加载网络图片
- Android开发艺术探索------Bitmap的高效加载
- 高效显示Bitmap之高效加载较大的 Bitmaps
- Bitmap的高效加载
- Android使用BitMap压缩图片(高效加载大图)Code+详解
- android中的bitmap的加载和cache
- 高效地加载大Bitmap(位图)
- 高效使用Bitmaps(一) 大Bitmap的加载
- Bitmap的加载与Cache(一)
- (十六)Bitmap的加载和Cache
- 高效的加载Bitmap
- BitMap高效显示策略(一):大图的缩放和加载
- FindJpg(2)-BitMap的高效加载和缓存
- 高效使用Bitmaps(二) 后台加载Bitmap
- Bitmap的高效加载