1、高效加载大图
设备给每个应用程序分配的内存大小是有限的,如果加载一张大图到内存中可能会导致OOM。我们通过下面的方法就可以看到每个应用程序最高可用内存是多少。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
因此在展示高分辨率图片时需要对图片进行压缩,压缩后图片的大小应该和展示控件的大小接近。试想一下,如果一个特别小的控件展示一个非常大的图片,这无疑是一种内存的浪费,而且在性能上会带来很大的影像。下面看下如果对图片进行压缩。
BitmapFactory提供了很多解析方法:
- BitmapFactory.decodeResource:解析资源文件中的图片
- BitmapFactory.decodeFile():解析磁盘中的图片
- BitmapFactory.decodeStream():解析网络上的图片
每个方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的options.inJustDecodeBounds设置为true,就可以让解析方法禁止为Bitmap分配内存,返回值不再是一个Bitmap,而是null。但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这样我们就可以根据outWidth、outHeight和显示控件的大小计算出一个合适的值赋值给BitmapFactory.Options的inSampleSize,这样就完成了图片的压缩。下面看下常规的压缩代码。
首先根据outWidth、outHeight和显示控件的大小计算出缩小的比例。
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int inSampleSize = 1;
int bitmapWidth = options.outWidth;
int bitmapHeight = options.outHeight;
if (bitmapWidth > reqWidth || bitmapHeight > reqHeight) {
int widthRatio = Math.round(bitmapWidth * 1.0f / reqWidth);
int heightRatio = Math.round(bitmapHeight * 1.0f / reqHeight);
//选出宽高比中较小的那个,保证压缩后图片的宽高大于等于显示控件的宽高
//当bitmapWidth < reqWidth || bitmapHeight < reqHeight为true时此时inSampleSize的值可能等于0
//而当options.inSampleSize在小于1时,会当做1来处理。所以当Bitmap的宽或高小于控件的宽高时图片不会被压缩
//这样的话依然可能会导致oom
inSampleSize = Math.min(widthRatio, heightRatio);
}
return inSampleSize;
}
上面代码很简单,但需要注意的是:当bitmapWidth < reqWidth || bitmapHeight < reqHeight为true时此时inSampleSize的值可能等于0
,而当options.inSampleSize在小于1时,会当做1来处理。所以当Bitmap的宽或高小于控件的宽高时图片不会被压缩这样的话依然可能会导致oom。
下面看下图片压缩的代码
BitmapFactory.Options options = new BitmapFactory.Options();
//只解析图片宽高属性不加载到内存中
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.mipmap.origin, options);
options.inSampleSize = calculateInSampleSize(options, iv_origin.getWidth(), iv_origin.getHeight());
//设置为false,再次解析
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.origin, options);
iv_origin.setImageBitmap(bitmap);
首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
2、使用图片缓存技术
当使用RecyclerView控件展示图片,随着不断的滑动,加载的图片也就会越多,对内存的消耗也就会越大,当快速滑动列表,还可能会导致OOM。
有的人可能会问,在滑动过程中将不可见的图片回收不就行了。是的这样可能不会带来OOM,但是当你需要再次展示之前的图片时就需要重新将图片加载到内存中,然后才能显示,如果网络比较差时,体验就很差。
那该怎么办呢?其实比较好的解决方案当然是使用缓存了,它可以让组件快速地重复加载图片。下面就看下如何使用内存缓存技术来对图片进行缓存,从而让你的程序在能加载很多图片的时候提高响应的速度和流畅性。
内存缓存技术最核心的类就是LruCache,它的算法原理就是将最近使用的对象存储在LinkedHashMap中,将最近最少使用的对象在缓存值达到预定值之前从内存中删除。
下面看下LruCache的使用
private LruCache<String, Bitmap> 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<String, Bitmap>(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);
}
当向 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);
}
}
BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// 在后台加载图片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}