Android中常常会加载很多图片,但是手机分配给每个应用的内存大小都是有限的,如果图片资源过大会导致OOM现象,所以android的图片处理成了一个非常重要能有效减轻app负担的途径。下面我们分析一下常用的图片处理方式,有错误的地方希望可以指出,共同学习进步。
首先可以通过代码检查一下应用的内存分配是多少,让我们在做图片处理的时候心里有一个标准。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() /1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法,每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的Width、Height和MimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。
获取原始图片的宽高与类型可以用下面的方法,注意:前期必须是injustDecodeBounds为true防止分配内存:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage,options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
图片压缩主要是通过设置BitmapFactory.Options中inSampleSize的值就可以实现。inSampleSize值是计算图片大小的非常关键的一个变量,数字越大分辨率越小,比如我们有一张20481536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源图片的高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
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大小,当然这个大小不是越大越好,这里是根据你的imageView来进行相对比例的放缩。
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true解析一次图片防止分配内存。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
public static Bitmap decodeSampledBitmapFromResource(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);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
imageview的调用方式就很简单了:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
缓存技术
设想一下,如果在一个recyclerView中,每个item中都有大量的图片,滑动的时候还会加载更多,这样的现象会在一些电商类的app中经常见到,如果我们只是单纯的做了图片的压缩可能还是会导致OOM的出现,因为会往内存中写入大量的图片数据,导致内存泄漏,这样的话也不能达到我们预期的效果,这个时候混存技术就显的格外的重要了。
缓存技术用到的类是LruCache (此类在android-support-v4的包中提供),这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除,这样的话我们就可以针对不同的场景进行缓存了。当然也有人说缓存一般使用的是软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠,随时都有被回收的风险,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);
}
}
网络加载完毕后还要使用addBitmapToMemoryCache这个方法将新加入内存的图片的key进行缓存,当然我们拿到的八分之一的内存进行图片缓存key的时候,至于内存不够的时候会自动摒弃一些不常用的key来维持这个缓存的平衡,这个是由LruCache自动进行的GC,我们不用纠结。
Glide的缓存机制
既然是缓存功能,就必然会有用于进行缓存的Key。那么Glide的缓存Key是怎么生成的呢?我不得不说,Glide的缓存Key生成规则非常繁琐,决定缓存Key的参数竟然有10个之多
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
...
}
...
}
可见,决定缓存Key的条件非常多,即使你用override()方法改变了一下图片的width或者height,也会生成一个完全不同的缓存Key,主要就是重写了equals()和hashCode()方法,保证只有传入EngineKey的所有参数都相同的情况下才认为是同一个EngineKey对象,这样就保证了所有的图片是唯一的。
当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而提升了用户体验。可以调用skipMemoryCache()方法并传入true,禁用掉Glide的内存缓存功能
Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能,glide使用了两种缓存方式,内存缓存与磁盘缓存。
RecyclerView滑动时Glide加载图片的原理:
Glide使用的是一个全局单例的模式来保证了线程的安全性与原子性,内部构建是使用了线程池的方式来加载图片,这样线程池可以并发的发起多个请求并且互不干扰,但是这个并发数与线程池的核心数密切相关,Glide将加载请求和目标ImageView关联(关键),开始某个ImageView的加载请求前会先将该ImageView关联的请求清除。此时在线程池中的关联的DecodeJob,正在进行的网络请求不会被中断,在等待队列里的也不会被直接从线程池移除,而是移除回调并设置取消标志位,让未开始的后续加载步骤的逻辑不会被执行。
当列表(ListView/RecyclerView)快速滚动时,线程池中同时执行的网络请求数量不会超过设备可用核心数,其余请求会放到队列中等待执行。虽然队列长度可能会一下增加到几十,但随着列表复用View,队列中的大部分请求都会被取消掉,之后执行时不会发起网络请求,并迅速让位于等待中的请求。也就是说,快速滚动过程的中间很多个列表项的请求都会被略过。这样的机制保证了不会过度消耗资源导致滑动卡顿与图片混乱问题!