为了账号安全,请及时绑定邮箱和手机立即绑定

Android 图片加载之Glide缓存策略

标签:
Android

    由于图片加载是应用开发中非常常见,但是有非常容易消耗资源甚至出现问题的场景,因此出现了很多第三方图片加载框架,从最早的ImageLoader(Universal ImageLoader),再到Facebook推出的Fresco,还有新兴的Picasso和Glide。当然,这些框架的使用场景一般都有很大的重合,所以一般我们只选择一个进行使用,而本人使用最多的是Google所推荐的Glide,它使用简单,支持GIF等,而且缓存效果得到很多人的肯定,也因此特意去了解了一下Glide的缓存机制。至于他们的优劣点,也有很多人进行比较,但是这不是这篇文章的重点。

    (这里说明一下,目前针对Glide 4.4版本进行了解)

    1.Glide 的缓存机制简介

    2.Glide 的请求流程

    3.常见的压缩方式

    4.动态URL与Glide缓存机制冲突问题

    通过这4个问题,我们进行简单的介绍,最基础的使用就不进行说明了,而且内容多数来自参考博客的大佬们分析,个人进行自我理解式的整理简化,所以就尽量不贴源码了,涉及源码不清楚了可以进入相关博客进行详细理解或者自己下载源码进行阅读。Glide最常用就是加载图片,下文以加载图片为例进行说明。

1.Glide 的缓存机制简介

    Glide采取的多级缓存机制,能够较为友好地实现图片、动图的加载。其主要有 内存缓存+硬盘缓存  ,当然他们的作用也有不同,其中内存缓存主要用于防止将重复的图读入内存中,硬盘缓存则用于防止从网络或者其他地方将重复下载和数据读取。由此可见,以APP应用范围来看,内存缓存主要针对内在处理,硬盘缓存主要针对对外管理,也由二者结合才构成Glide的主要缓存机制基础。默认情况下,内存缓存和硬件缓存,Glide都是开启的,当然官方也提供了二者的开关设置:skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能;调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了(这里并不是boolean值类型,因为Glide提供了四种枚举类型,在1.3硬盘缓存中会进行说明)。

    1.1 Key的生成

    上文说了,缓存机制中最主要的问题之一就是避免重复加载,那要作为避免重复就需要有依据,这个依据就是 缓存Key,它是每个图的多项信息经过Glide内部算法生成的(其中一个重要参数信息就是图片的URL,这也造成动态URL的时候会发生重复加载的问题,这我们会在第4部分进行说明)。

    在源码里查看可以知道,每一次的load方法内部,都会有一个fetcher.getId()方法获得了一个id字符串,这个字符串也就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个id就是这张图片的url地址。然后,这个id会结合signature、width、height等等10个参数一起传入到EngineKeyFactory的buildKey()方法当中,从而构建出了一个EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。

    因此,如果你图片的width或者height发生改变,也会生成一个完全不同的缓存Key。

(这里补充一下:4.4以前是Bitmap复用必须长宽相等才可以复用,而4.4及以后是Size>=所需就可以复用,只不过需要调用reconfigure来调整尺寸

    1.2 内存缓存

    我们知道,当Glide加载完某个图片后,就会将它放入内存缓存中,在它被内存缓存回收之前,再调用就可以直接从内存缓存里直接获取。当然,这是最基本的运用场景,其内存缓存机制里所依据的算法是 LRUCache算法(Last Recently Used:近期最少使用),Google有提供对应的算法工具类DiskLruCache,但是Glide是使用的自己编写的DiskLruCache工具类,不过可以知道的是二者算法原理是一致的。而且,为了达到更好的效果,还采取了弱引用机制,结合LRUCache,二者奠定了内存缓存的总体基础。

    如上所述,Glide将内存缓存中划分成两个区域:LruResourceCache(某些人称为 图片池,就是Glide实现内存缓存所使用的LruCache对象了)+activeResources(某些人称为对象池,即正在使用的图片管理处)。由此可知,前者使用的是LRU算法进行管理的缓存工具,而activeResources就是使用了弱引用机制(采取了HashMap进行弱引用进行存储)。

    在这里说明一下,图片请求开启,Glide检查是否开启内存缓存后,如果开启了,则先去LruResourceCache中查找是否符合的图片,如果找到了,我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后将这个缓存图片存储到activeResources当中,以保护这些图片不会被LruCache算法回收掉。如果在LruResourceCache中没有找到,再去activeResources中进行查找。如果二者均找不到符合资源,再开启子线程进行加载图片资源。由此可知,在Glide的内存缓存机制中,LruResourceCache的优先级在activeResources之前。

    另外,在Glide内存缓存中,通过EngineResource作为图片的管理对象,里面有一个参数变量acquired用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1。当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中。而经过release()之后,如果acquired变量等于0了,说明图片已经不再被使用了,此时会进行内部回收。这里的内部回收过程是这样的:首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。

    1.3 硬盘缓存

    调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:

    DiskCacheStrategy.NONE: 表示不缓存任何内容。

    DiskCacheStrategy.SOURCE: 表示只缓存原始图片。

    DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。

    DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

    这边提到了原始图片和缓存图片的概念,这里我们需要知道,大多数情况下,Glide会对请求的原始图片进行压缩和转换,再进行展示。其中Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。这样的机制能够比较有效地规避解决 大图和超大图带来的OOM问题+大图小框的资源浪费问题。至于压缩和转换,Glide主要采用尺寸压缩算法,更多的内容我们会在下文第3部分进行说明。

    我们先说说硬盘缓存的资源管理机制,其实Glide在这里也是采用LRU算法进行管理,这里就不对Glide的DiskLRUCache工具类进行解析了,有兴趣的可以自行搜索。上头说到,Glide在内存缓存中没有找到对应的图片资源后,便会开启新的子线程进行图片加载,此时会执行EngineRunnable的run()方法,run()方法中又会调用一个decode()方法:

webp

decode源码图

    可知,Glide会先在硬盘缓存中进行搜索,当硬盘缓存存在后直接读取缓存的图片(decodeFromCache方法),当硬盘缓存中不存在符合资源时候,再调用decodeFromSource()来读取原始图片。而在硬盘缓存读取中(decodeFromCache方法)先去调用DecodeJob的decodeResultFromCache()方法来获取缓存,如果获取不到,会再调用decodeSourceFromCache()方法获取缓存,这两个方法的区别其实就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE这两个参数的区别,而这两个指的就是硬盘缓存是采取“转换后的图片缓存”、“原始图片缓存”的选择。

    那什么时候写入硬盘缓存?简单说一下,默认下是在网络请求图片,进行转化后,写入硬盘缓存的。详细的就不说了,感兴趣可以去看参考博客或者源码。

2.Glide 的请求流程

    这里,我们从最直接的使用角度进行解释:

    1. Glide.with(context)创建RequestManager 

    RequestManager负责管理当前context的所有Request

    Context可以传Fragment、Activity或者其他Context,当传Fragment、Activity时,当前页面对应的Activity的生命周期可以被RequestManager监控到,从而可以控制Request的pause、resume、clear。这其中采用的监控方法就是在当前activity中添加一个没有view的fragment,这样在activity发生onStart onStop onDestroy的时候,会触发此fragment的onStart onStop onDestroy

    RequestManager用来跟踪众多当前页面的Request的是RequestTracker类,用弱引用来保存运行中的Request用强引用来保存暂停需要恢复的Request

    2. Glide.with(context).load(url)创建需要的Request 

    通常是DrawableTypeRequest,后面可以添加transform、fitCenter、animate、placeholder、error、override、skipMemoryCache、signature等等

    如果需要进行Resource的转化比如转化为Byte数组等需要,可以加asBitmap来更改为BitmapTypeRequest

    Request是Glide加载图片的执行单位

    3. Glide.with(context).load(url).into(imageview) 

    在Request的into方法中会调用Request的begin方法开始执行

    在正式生成EngineJob放入Engine中执行之前,如果并没有事先调用override(width, height)来指定所需要宽高,Glide则会尝试去获取imageview的宽和高,如果当前imageview并没有初始化完毕取不到高宽,Glide会通过view的ViewTreeObserver来等View初始化完毕之后再获取宽高再进行下一步

    这里我们再着重讲一下,资源加载的相关知识:

    GlideBuilder在初始化Glide时,会生成一个执行机Engine

    Engine中包含LruCache缓存及一个当前正在使用的active资源Cache(弱引用)

    activeCache辅助LruCache,当Resource从LruCache中取出使用时,会从LruCache中remove并进入acticeCache当中

    Cache优先级LruCache>activeCache

    Engine在初始化时要传入两个ExecutorService,即会有两个线程池,一个用来从DiskCache获取resource,另一个用来从Source中获取(通常是下载)

    线程的封装单位是EngineJob,有两个顺序状态,先是CacheState,在此状态先进入DiskCacheService中执行获取,如果没找到则进入SourceState,进到SourceService中执行下载

 3.常见的压缩方式

    Android中图片是以bitmap形式存在的,那么bitmap所占内存,直接影响到了应用所占内存大小,首先要知道计算方式:

        bitmap所占内存大小 = 图片长度 * 图片宽度 * 一个像素点占用的字节数(单位尺寸的像素密度一般是手机分辨率所决定的)

    因此我们常见的压缩方式就是两种方式:1.将图片的w、h进行压缩;2.降低像素点占用的字节数。而我们最常见的是尺寸压缩,质量压缩和格式压缩,这里我们简单解释一下它们。

    3.1 尺寸压缩&采样率压缩

    说实话,个人感觉这两个压缩概念上虽然有差异,但是实际使用中原理是相通的,或者说二者几乎都是一起使用的。

    尺寸压缩就是将图片的大小(w、h进行缩小),从而保持单位尺寸上的像素密度不变,由此像素点的减少,使得图片大小得到减少;

    采样率压缩通过设置采样率,单位尺寸上减少像素密度(比如读取图片时候,并不读取所有的像素点,只读取部分像素点),造成单位尺寸上的像素密度降低, 达到对内存中的Bitmap进行压缩,或者说就是按照一定的倍数对图片减少单位尺寸的像素值。其原理为:通过减少单位尺寸的像素值,真正意义上的降低像素值。但是一般为了保持图片不失真,会结合尺寸的压缩。

    常见的使用场景:缓存缩略图 (头像的处理)。

    通常通过BitmapFactory中的decodeFile方法对图片进行尺寸压缩,其中一个参数opts 就是所谓的采样率,它里边有很多属性可以设置,我们通过设置属性来达到根据自己的需要,压缩出指定的图片。

    为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。

    现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:

    1. 预估一下加载整张图片所需占用的内存。

    2. 为了加载这一张图片你所愿意提供多少内存。

    3. 用于展示这张图片的控件的实际大小。

    4. 当前设备的屏幕尺寸和分辨率。

    比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。

    那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值

    3.2 质量压缩    

    顾名思义就是降低图片质量(大小)。原理 :通过算法扣掉(同化)了 图片中的一些某个点附近相近的像素,达到降低质量减少文件大小的目的。

    注意 : 通过上述我们知道,它并没有改变w、h,也没有单个像素占据的字节数。所以只能实现对 file 的影响,对加载这个图片出来的bitmap 内存是无法节省的,因为质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的。

    使用场景 :将图片保存到本地 ,或者将图片上传 到服务器 ,根据实际需求来 。

    质量压缩主要借助Bitmap中的compress方法实现:

    public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

    这个方法用来将特定格式的压缩图片写入输出流(OutputStream)中,当然例如输出流与文件联系在一起,压缩后的图片也就是一个文件。如果压缩成功则返回true,其中有三个参数:

    format是压缩后的图片的格式,可取值:Bitmap.CompressFormat .JPEG、~.PNG、~.WEBP。

    quality的取值范围为[0,100],值越小,经过压缩后图片失真越严重,当然图片文件也会越小。(PNG格式的图片会忽略这个值的设定)

    stream指定压缩的图片输出的地方,比如某文件。

    上述方法还有一个值得注意的地方是:当用BitmapFactory decode文件时可能返回一个跟原图片不同位深的图片,或者丢失了每个像素的透明值(alpha),比如说,JPEG格式的图片仅仅支持不透明的像素。



作者:双木火羽白
链接:https://www.jianshu.com/p/78ad4ce1694e


点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消