个人语录:
时间不负有心人,星光不问赶路人,以顶级的态度写一篇博客
面试过程中,面试官最喜欢的问的就是一些常用的框架,但是框架的源码很多时候也很难看懂,这几天我自己也在学习Glide框架,写这篇博客的原因是总结一些面试官的常问的Glide的问题,源码枯燥乏味,本文不对源码深入剖析,只是把源码里面的一些框架比较好的特点总结出来,毕竟面试过程中,面试官也不会拿出源码来问你,一般会问你为什么这么设计,这么设计的好处。
拿出小本本来和我一起记
面试官:看你简历上说熟练使用Glide,能够说一说为什么项目上图片加载框架使用的是Glide而不是其它呢?
Glide的加载过程大致如下:
参考:Android 【手撕Glide】–Glide是如何关联生命周期的? - 简书 (jianshu.com)
深入理解Glide源码:三条主线分析 Glide 执行流程 - 掘金 (juejin.cn)
SupportRequestManagerFragment/RequestManagerFragment
,并绑定到当前Activity,这样Fragment就可以感知Activity的生命周期;Lifecycle
、LifecycleListener
,并且在生命周期的onStart() 、onStop()、 onDestroy()中调用相关方法;Lifecycle
对象,并且LifecycleListener实现了LifecycleListener
接口;传入Application Context或者在子线程使用:调用getApplicationManager(context);
这样Glide的生命周期就和应用程序一样了。用的是ApplicationLifecycle,由于没有创建Fragment,这里只会调用onStart()
,这种情况Glide生命周期就和Application一样长了。
以前老是听人说缓存是内存—>磁盘—>网络这样的方式去获取图片资源的,但这就是3级缓存吗?明显不是,这个只是2级缓存;Glide将它的缓存分为2个大的部分,一个是内存缓存,一个是硬盘缓存。其中内存缓存又分为2种,弱引用和Lrucache;磁盘缓存就是DiskLrucache,DiskLrucache算法和Lrucache差不多的,所以现在看起来Glide3级缓存的话应该是
WeakReference + Lrucache + DiskLrucache
。参考:Android 【手撕Glide】–Glide缓存机制 - 简书 (jianshu.com)
Glide缓存分为:内存缓存和磁盘缓存,其中内存缓存是由弱引用 + Lrucache
组成
取的顺序:
弱引用--->Lrucache--->DisLrucache--->网络
写的顺序
DiskLruCache --> 弱引用,当计数器为 0 的时候,再放到 LurCache
底层数据结构:HashMap维护,Key是缓存的key,这个key由图片url、width、height等10来个参数组成;value是图片资源对象的弱引用形式。
Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
底层数据结构:LinkedHashMap,LRU是Least Recently Used的缩写,即最近最少使用,一种常用的置换算法,选择最近最久未使用的对象予以淘汰。linkHashMap链表的特性,把最近使用过的文件插入到列表头部,没使用的图片放在尾部,LruCache是将linkHashMap按访问排序(accessOrder = true)
,最近访问的,是移动到链表的尾部。这里推荐一篇 图解LinkedHashMap原理,这是Glide自定义的LruCache:
#LruCache
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
取数据
在内存缓存中有一个概念叫图片引用计数器 ,具体来说是在EngineResource
中定义一个acquired
变量用来记录图片被引用的次数,调用acquire()
方法会让变量加1,调用release()
方法会让变量减1。
获取图片资源是先从弱引用取缓存,拿到的话,引用计数+1;没有的话从LruCache中拿缓存,拿到的话,引用计数也是+1,同时把图片从LruCache缓存转移到弱应用缓存池中;再没有的话就通过EngineJob
开启线程池去加载图片,拿到的话,引用计数也是+1,会把图片放到弱引用。
存数据
很明显,这是加载图片之后的事情。通过EngineJob
开启线程池去加载图片,取到数据之后,会回调到主线程,把图片存到弱引用。当图片不再使用的时候,比如说暂停请求或者加载完毕或者清除资源时,就会将其从弱引用中转移到LruCache
缓存池中。总结一下,就是正在使用中的图片使用弱引用
来进行缓存,暂时不用的图片使用LruCache
来进行缓存的功能;同一张图片只会出现在弱引用
和LruCache
中的一个。
为什么要引入弱引用?
1、分压策略,减少Lrucache 中trimToSize
的概率。如果正在remove的是张大图,lrucache正好处在临界点,此时remove操作,将延缓Lrucache的trimToSize
操作;
2 提高效率:弱引用用的是HashMap
,Lrucache用的是LinkedHashMap
,从访问效率而言,肯定是HashMap
更高。
其实面试过程中肯定避免不了,手写Lru
首选数据结构是
LinkedHashMap
LeetCode原题:146. LRU 缓存 - 力扣(LeetCode) (leetcode-cn.com)
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
借助这个结构,我们来逐一分析:
1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。
2、对于某一个 key,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 val。
3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key 快速映射到任意一个链表节点,然后进行插入和删除。
为什么要是双向链表
因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
Glide磁盘缓存策略(4.x)
DiskCacheStrategy.DATA
: 只缓存原始图片;DiskCacheStrategy.RESOURCE
:只缓存转换过后的图片;DiskCacheStrategy.ALL
:既缓存原始图片,也缓存转换过后的图片;对于远程图片,缓存 DATA和 RESOURCE;对于本地图片,只缓存 RESOURCE;DiskCacheStrategy.NONE
:不缓存任何内容;DiskCacheStrategy.AUTOMATIC
:默认策略,尝试对本地和远程图片使用最佳的策略。当下载网络图片时,使用DATA
(原因很简单,对本地图片的处理可比网络要容易得多);对于本地图片,使用RESOURCE
。如果在内存缓存中没获取到数据会通过EngineJob
开启线程池去加载图片,这里有2个关键类:DecodeJob
和EngineJob
。EngineJob
内部维护了线程池,用来管理资源加载,当资源加载完毕的时候通知回调; DecodeJob
是线程池中的一个任务。
磁盘缓存是通过DiskLruCache
来管理的,根据缓存策略,会有2种类型的图片,DATA
(原始图片)和 RESOURCE
(转换后的图片)。磁盘缓存依次通过ResourcesCacheGenerator
、SourceGenerator
、DataCacheGenerator
来获取缓存数据。ResourcesCacheGenerator
获取的是转换过的缓存数据;SourceGenerator
获取的是未经转换的原始的缓存数据;DataCacheGenerator
是通过网络获取图片数据再按照按照缓存策略的不同去缓存不同的图片到磁盘上。
Glide内存管理分为:
- OOM的防治
- 内存抖动
Glide通过:
- Glide图片采样
Glide针对较大的图片,会根据当前ui的显示大小与实际大小的比例,进行采样计算从而减小图片在内存中的占用。一般而言
图片的大小 = 图片宽 X 图片高 X 每个像素占用的字节数。
对于资源文件夹下的图片:
图片的高 = 原图高 X (设备的 dpi / 目录对应的 dpi )
图片的宽 = 原图宽 X (设备的 dpi / 目录对应的 dpi )
onlowMemory/onTrimMemory
- 当内存过低的时候会调用onlowMemory,在onlowMemory 中Glide会将一些缓存的内存进行清除,方便进行内存回收,
- 当onTrimMemory被调用的时候,如果level是系统资源紧张,Glide会将Lru缓存和BitMap重用池相关的内容进行回收。
- 其他的原因调用onTrimMemory,Glide会将缓存的内容减小到配置缓存最大内容的1/2。
- 弱引用
Glide通过RequestManager管理图片请求,而RequestManager内部是通过RequestTracker和TargetTracker来完成的。他们持有的方式都是弱引用。
- 生命周期绑定
减少加载到内存的图片大小,及时清楚不必要的对象引用,从而减少OOM的概率
在 Android 3.0(API 级别 11)开始,系统引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在生成目标 Bitmap 时尝试复用 inBitmap,这意味着 inBitmap 的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过 inBitmap 的使用方式存在某些限制,在 Android 4.4(API 级别 19)之前系统仅支持复用大小相同的位图,4.4 之后只要 inBitmap 的大小比目标 Bitmap 大即可
关于Glide中的线程线程池,准备说两个方面:
1.图片加载回调
2.Glide的线程池配置
线程作为cpu调度的最小单元,每一次的创建和回收都会有较大的消耗,通过使用线程池可以
1.降低资源消耗
:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度
:当任务到达时,可以不需要等待线程创建就能立即执行。
3.提高线程的可管理性
:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
4.有效的控制并发数
Glide中提供了四种线程池配置。
1.DiskCacheExecutor 该线程池只有一个核心线程,没有非核心线程,所有任务在线程池中串行执行。在Glide中常用与从文件中加载图片。
2.SourceExecutor 该线程也只有核心线程没有非核心线程,与DiskCacheExecutor 的不同之处在于核心线程的数量根据CPU的核数来决定。如果cpu核心数超过4则核心线程数为4 如果Cpu核心数小于4那么使用Cpu核心数作为核心线程数量。在Glide中长用来从网络中加载图片。
3.UnlimitedSourceExecutor 没有核心线程,非核心线程数量无限大。这种类型的线程池常用于执行量大而快速结束的任务。在所有任务结束。在所有任务结束后几乎不消耗资源。
4.AnimationExecutor 没有核心线程,非核心线程数量根据Cpu核心数来决定,当Cpu核心数大于等4时 非核心线程数为2,否则为1。
Glide通过RequestManager#as方法确定当前请求Target最终需要的资源类型。通过load方法确定需要加载的model资源类型,资源的加载过程经历ModelLoader的model加载匹配,解码器解码,转码器的转换,这几个过程构建成一个LoadPath 。而每一个LoadPath 又包含很多的DecodePath,DecodePath的主要作用是将ModelLoader加载出来的数据进行解码,转换。Glide会遍历所有可能解析出对应数据的LoadPath 直到数据正真解析成功。