如需转载请评论或简信,并注明出处,未经允许不得转载
目录
前言
Bitmap
在Android中的使用非常的广泛,几乎每个页面都有使用它,特别是在一些ListView
/GridView
/RecyclerVIew
等需要大量用到图片的场景,合理的使用Bitmap就显得尤为重要了,稍有不慎就可能会导致OOM
Bitmap内存占用计算方式
重要:将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的
比如一张图片是1500 * 1000像素,使用的ARGB_8888
颜色类型,即(8+8+8+8)32位,那么每个像素点就会占用4个字节,总内存就是1500 * 1000 * 4字节,也就是5.7MB
优化技巧
- 设置合适的Bitmap.Config
public static final Bitmap.Config ALPHA_8
public static final Bitmap.Config ARGB_4444
public static final Bitmap.Config ARGB_8888
public static final Bitmap.Config RGB_565
根据bitmap的计算方式我们知道了,bitmap的配置参数也是影响bitmap占用内存大小的一个因素。比如如果你不考虑图片透明度的话,ARGB_8888
和RGB_565
他们的表现效果在用户看来是一样的,但是RGB_565(5+6+5 = 16位 = 2字节)的内存占用只有ARGB_8888(8+8+8+8 = 32位 = 4字节)的一半。所以在不影响用户体验的情况下,应该采用内存占用更少的方案。在某些低性能设备上,甚至可以采用牺牲部分视觉效果换取更流畅的用户体验的降级方案,如用ARGB_4444
代替ARGB_8888
- 设置合适的inSampleSize
有时候我们页面上所需要展示的图片宽高(单位像素)远远小于图片的原始宽高,如果把图片直接展示不做任何处理的话,拿我们上面那个例子来说,一张图片就是5.7MB,显然我们是无法接受的。那么我们就可以通过设置inSampleSize属性来压缩我们的图片(对用户来说视觉上是没有任何影响的)
inSampleSize可以理解为采样率,它的默认值和最小值为1(当小于1时,解码器将该值当做1来处理),且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)
例如,当inSampleSize为2时,一个2000 * 1000的图片,将被缩小为1000 * 500,相应地,它的像素数和内存占用都被缩小为了原来的1/4
为了设置合适的inSampleSize
,我们就需要获取到原始Bitmap的宽高和实际所需宽高,实际所需的宽高我们根据实际业务场景很容易测量,那原始BItmap
的宽高如何进行测量呢?这里有一个比较关键的地方,因为我们并不想把这个Bitmap加载到内存中后再去测量,幸运的是,Android给我们提供了相关的解决方案
BitmapFactory
这个类提供了多个解析方法用于创建Bitmap
对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile
方法,网络上的图片可以使用decodeStream
方法,资源文件中的图片可以使用decodeResource
方法
Android为每一种解析方法都提供了一个可选的BitmapFactory.Options
参数,将这个参数的inJustDecodeBounds
属性设置为true
,就可以让解析方法禁止为Bitmap
分配内存,返回值也不再是一个Bitmap
对象,而是null
。但是BitmapFactory.Options
的outWidth
、outHeight
和outMimeType
属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
public static Bitmap decodeBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 还原inJustDecodeBounds为false
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int width = options.outWidth;
final int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
//计算图片高度和我们需要高度的最接近比例值
final int heightRatio = Math.round((float) height / (float) reqHeight);
//宽度比例值
final int widthRatio = Math.round((float) width / (float) reqWidth);
//取比例值中的较大值作为inSampleSize
inSampleSize = heightRatio > widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
不过要注意的是,上面这个inSampleSize
的算法也不是万能的,比如我们平时拍的照片都是长图,这种算法用在浏览相册图片的缩略图的时候就会导致压缩太过严重,导致图片不是很清楚。所以关于inSampleSize
的设置算法,还是需要根据自己的实际业务场景而定,设计一个合适的inSampleSize
- 使用LRU缓存技术
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生
LruCache 这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除
下面是一个使用 LruCache 来缓存图片的例子:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
// LruCache通过构造函数传入缓存值,以KB为单位。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用内存值的1/8作为缓存的大小。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重写此方法来衡量每张图片的大小,默认返回图片数量。
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(8004804)。因此,这个缓存大小可以存储2.5页的图片
当向 ImageView
中加载一张图片时,首先会在 LruCache
的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView
,否则开启一个后台线程来加载这张图片
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}
- 使用inBitmap复用Bitmap对象
利用options.inBitmap
的高级特性提高Android系统在Bitmap
分配与释放执行效率。使用inBitmap
属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap
会尝试去使用之前那张Bitmap
在Heap
中所占据的pixel data
内存区域,而不是去问内存重新申请一块区域来存放Bitmap
。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小
使用inBitmap需要注意几个限制条件:
在SDK 11 -> 18之间,重用的
Bitmap
大小必须是一致的。例如给inBitmap
赋值的图片大小为100 * 100,那么新申请的Bitmap必须也为100 * 100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap
大小新申请的
Bitmap
与旧的Bitmap
必须有相同的解码格式。例如大家都是ARGB_8888
的,如果前面的Bitmap
是ARGB_8888
,那么就不能支持ARGB_4444
与RGB_565
格式的Bitmap
了。我们可以创建一个包含多种典型可重用Bitmap
的对象池,这样后续的Bitmap
创建都能够找到合适的“模板”去进行重用
- 使用.9图
现在手机的分辨率越来越高,图片资源在被加载后所占用的内存也越来越大,所以要尽量避免使用大的PNG图,在产品设计的时候就要尽量避免用一张大图来进行展示,尽量多用NinePatch资源
NinePatch指的是一种拉伸后不会变形的特殊png图,NinePatch的拉伸区域可以自己定义。这种图的优点是体积小,拉伸不变形,可以适配多机型
Android SDK中有自带NinePatch资源制作工具,Android-Studio中在普通png图片点击右键可以将其转换为NinePatch资源,使用起来非常方便
- 主动释放资源
- 主动释放
Bitmap
资源
当你确定这个Bitmap
资源不会再被使用的时候(当然这个Bitmap
不释放可能会让程序下一次启动或者resume
快一些,但是其占用的内存资源太大,可能导致程序在后台的时候被杀掉,反而得不偿失),我们建议手动调用recycle()
方法,释放内存
private static void rceycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
- 主动释放
ImageView
的图片资源
由于我们在实际开发中,很多情况是在xml布局文件中设置ImageView
的src
或者在代码中调用ImageView.setImageResource
/setImageURI
/setImageDrawable
等方法设置图像,下面代码可以回收这个ImageView所对应的资源
private static void recycleImageViewBitMap(ImageView imageView) {
if (imageView != null) {
BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
rceycleBitmapDrawable(bd);
}
}
private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}
- 主动释放
ImageView
的背景资源
如果ImageView
是有Background
,那么下面的代码可以进行背景资源的释放:
public static void recycleBackgroundBitMap(ImageView view) {
if (view != null) {
BitmapDrawable bd = (BitmapDrawable) view.getBackground();
rceycleBitmapDrawable(bd);
}
}
private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}
总结
本文主要讲了一些Bitmap
的优化技巧,但是在实际生产环境中,建议使用Glide
等图片框架,因为这些框架内部往往已经帮我们做了很多的优化,例如缓存、Bitmap
复用、压缩等等操作,但是明白其中的原理对我们开发者也是很有意义,往往能帮我们解决一些框架没有覆盖到的疑难问题