Android图片处理与缓存

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,队列中的大部分请求都会被取消掉,之后执行时不会发起网络请求,并迅速让位于等待中的请求。也就是说,快速滚动过程的中间很多个列表项的请求都会被略过。这样的机制保证了不会过度消耗资源导致滑动卡顿与图片混乱问题!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,487评论 0 13
  • 【Android 库 Glide】 引用 Android图片加载框架最全解析(一),Glide的基本用法Andro...
    Rtia阅读 5,395评论 0 22
  • 前言 android中图片加载框架有很多,所有框架最终达到的目都是在Android平台上以极度简单的方式加载和展示...
    luoqiang108阅读 27,775评论 7 120
  • 一、简介 在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个名叫Glide的图片加载库,作者是bumptech。这...
    天天大保建阅读 7,451评论 2 28
  • 写在开头 由于杭州的房价实在太高,所以我可耻的跑路到了西安。几个月前在西安买了房,所以最近总结了一些还算全面的An...
    BlackFlag阅读 9,820评论 10 200