查看网站外链代码,云主机和云虚拟主机,wordpress插件 速度,用友财务软件官方网站前言
Glide可以说是最常用的图片加载框架了#xff0c;Glide链式调用使用方便#xff0c;性能上也可以满足大多数场景的使用#xff0c;Glide源码与原理也是面试中的常客。 但是Glide的源码内容比较多#xff0c;想要学习它的源码往往千头万绪#xff0c;一时抓不住重点.…前言
Glide可以说是最常用的图片加载框架了Glide链式调用使用方便性能上也可以满足大多数场景的使用Glide源码与原理也是面试中的常客。 但是Glide的源码内容比较多想要学习它的源码往往千头万绪一时抓不住重点. 本文以Glide做了哪些优化为切入点介绍与学习Glide的源码与原理如果对您有所帮助欢迎点赞. Glide做了哪些优化? 要想要回答这个问题我们可以先想一想如果我们自己要实现一个图片加载框架我们会思考什么问题 1.图片下载是个耗时过程我们首先需要考虑的就是图片缓存的问题 2.图片加载也是个耗内存的操作很多OOM都是图片加载导致的所以我们也要考虑内存优化问题 3.图片加载到一半页面关闭了图片加载也应该中止这又牵扯到了生命周期管理的问题 4.还有就是图片加载框架是否支持大图加载大图情况下会有什么问题
以上就是我们提出的有关于Glide的几个问题了这样我们可以轻松得出本文主要包括的内容 1.Glide图片加载的总体流程介绍 2.Glide缓存机制做了哪些优化 3.Glide做了哪些内存优化 4.Glide如何管理生命周期? 5.Glide怎么做大图加载?
下面就带着问题进入正文~
1.Glide图片加载总体流程介绍
在开始了解Glide具体做了哪些优化之前我们先对Glide图片加载的总体流程做一个简单的介绍让大家首先有个整体概念。 同时在后面对Glide做的优化具体发生在哪一步也可以方便的知道. 概括来说图片加载包含封装解析下载解码变换缓存显示等操作如下图所示
1.封装参数从指定来源到输出结果中间可能经历很多流程所以第一件事就是封装参数这些参数会贯穿整个过程2.解析路径图片的来源有多种格式也不尽相同需要规范化3.读取缓存为了减少计算通常都会做缓存同样的请求从缓存中取图片Bitmap即可4.查找文件/下载文件如果是本地的文件直接解码即可如果是网络图片需要先下载5.解码这一步是整个过程中最复杂的步骤之一有不少细节6.变换解码出Bitmap之后可能还需要做一些变换处理圆角滤镜等7.缓存得到最终bitmap之后可以缓存起来以便下次请求时直接取结果8.显示显示结果可能需要做些动画淡入动画crossFade等。
以上就是Glide图片加载的总体流程,这里只做了简单的介绍详情可见聊一聊关于Glide在面试中的那些事
2.Glide缓存机制做了哪些优化
我们知道下载图片是非常耗费资源的所以图片缓存机制是图片加载框架很重要的一部分,下面就以一张表格来说明下 Glide 缓存。
缓存类型缓存代表说明活动缓存ActiveResources如果当前对应的图片资源是从内存缓存中获取的那么会将这个图片存储到活动资源中。内存缓存LruResourceCache图片解析完成并最近被加载过则放入内存中磁盘缓存-资源类型DiskLruCacheWrapper被解码后的图片写入磁盘文件中磁盘缓存-原始数据DiskLruCacheWrapper网络请求成功后将原始数据在磁盘中缓存
在介绍具体缓存前先来看一张加载缓存执行顺序有个大概的印象 Glide的缓存机制主要分为2种缓存一种是内存缓存一种是磁盘缓存。 之所以使用内存缓存的原因是防止应用重复将图片读入到内存造成内存资源浪费。 之所以使用磁盘缓存的原因是防止应用重复的从网络或者其他地方下载和读取数据。 正式因为有着这两种缓存的结合才构成了Glide极佳的缓存效果。
2.1 内存缓存
Glide默认开启内存缓存我们也可以通过skipMemoryCache关闭 上面我们可以看到内存缓存其实分两个部分ActiveResource缓存与LRU缓存 ActiveResources 就是一个弱引用的 HashMap 用来缓存正在使用中的图片,使用 ActiveResources 来缓存正在使用中的图片可以保护这些图片不会被 LruCache 算法回收掉
内存缓存加载顺序如下 1.根据图片地址宽高变换签名等生成key 2.第一次加载没有获取到活动缓存。 3.接着加载内存资源缓存先清理掉内存缓存在添加进行活动缓存。 4.第二次加载活动缓存已经存在。 5.当前图片引用为 0 的时候清理活动资源并且添加进内存资源。 6.又回到了第一步然后就这样环环相扣。
总结为流程图如下 这里没有贴出源码如果想要看源码的同学可参考从源码的角度分析 Glide 缓存策略
我们上面总结了Glide内存缓存加载的流程,看到这里我们很容易有个疑问为什么Glide要设计两种内存缓存
2.1.1 为什么设计两种内存缓存 LruCache算法的实现你会发现它其实是用一个Set来缓存对象的每次内存超出缓存设定触发trim操作的时候其实是对这个Set进行遍历然后移除缓存。但是我们都知道Set是无序的因此遍历的时候有可能会把正在使用的缓存给误伤了我还在用着它呢就给移出去了。因此这个弱引用可能是对正在使用中的图片的一种保护使用的时候先从LruCache里面移出去用完了再把它重新加到缓存里面。 举个例子 比如我们 Lru 内存缓存 size 设置装 99 张图片在滑动 RecycleView 的时候如果刚刚滑动到 100 张那么就会回收掉我们已经加载出来的第一张这个时候如果返回滑动到第一张会重新判断是否有内存缓存如果没有就会重新开一个 Request 请求很明显这里如果清理掉了第一张图片并不是我们要的效果。所以在从内存缓存中拿到资源数据的时候就主动添加到活动资源中并且清理掉内存缓存中的资源。这么做很显然好处是 保护不想被回收掉的图片不被 LruCache 算法回收掉,充分利用了资源。
2.1.1 小结
本节主要总结了Glide内存缓存加载的流程 1.首先去获取活动缓存如果加载到则直接返回没有则进入下一步 2.接着去获取LRU缓存在获取时会将其从LRU中删除并添加到活动缓存中 3.下次加载就可以直接加载活动缓存了 4.当图片引用为0时会从活动缓存中清除并添加到LRU缓存中 5.之所以要设计两种内存缓存的原因是为了防止加载中的图片被LRU回收
2.2 磁盘缓存
首先了解一下磁盘缓存策略
DiskCacheStrategy.NONE 表示不缓存任何内容。DiskCacheStrategy.RESOURCE 在资源解码后将数据写入磁盘缓存即经过缩放等转换后的图片资源。DiskCacheStrategy.DATA 在资源解码前将原始数据写入磁盘缓存。DiskCacheStrategy.ALL 使用DATA和RESOURCE缓存远程数据仅使用RESOURCE来缓存本地数据。DiskCacheStrategy.AUTOMATIC它会尝试对本地和远程图片使用最佳的策略。当你加载远程数据时AUTOMATIC 策略仅会存储未被你的加载过程修改过的原始数据因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据AUTOMATIC 策略则会仅存储变换过的缩略图因为即使你需要再次生成另一个尺寸或类型的图片取回原始数据也很容易。默认使用这种缓存策略
在了解磁盘缓存时我们主要需要明确一个概念是当我们使用 Glide 去加载一张图片的时候Glide 默认并不会将原始图片展示出来而是会对图片进行压缩和转换总之就是经过种种一系列操作之后得到的图片就叫转换过后的图片。 我们既可以缓存变换之前的原始图片也可以缓存变换后的图片
2.2.1 为什么需要两种磁盘缓存
上文已经说了DiskCacheStrategy.RESOURCE缓存的是变换后的资源DiskCacheStrategy.DATA缓存的是变换前的资源 举个例子同一张图片我们先在100*100的View是展示再在200*200的View上展示 如果不缓存变换后的类型相当于每次都要进行一次变换操作如果不缓存原始数据则每次都要去重新下载数据 如下可以看出两种缓存的key不一样
DiskCacheStrategy.RESOURCE
currentKey new ResourceCacheKey(helper.getArrayPool(),sourceId,helper.getSignature(),helper.getWidth(),helper.getHeight(),transformation,resourceClass,helper.getOptions());DiskCacheStrategy.DATA
DataCacheKey newOriginalKey new DataCacheKey(loadData.sourceKey, helper.getSignature());2.2.2 小结
本节主要介绍了Glide磁盘缓存的几种策略并介绍了为什么需要两种磁盘缓存的原因 这里也没有贴什么源码如果想要看源码的同学可参考从源码的角度分析 Glide 缓存策略
3.Glide做了哪些内存优化
Glide的内存优化主要也是对Bitmap的优化在回答这个问题前我们可以想想有哪些常见的Bitmap优化手段 1.当图片大小与View大小不一致时可以用inSampleSize进行尺寸优化 2.图片所占内存即宽高每像素所占内存大小不同的模式每个像素所占的内存大小不同我们可以利用inpreferredconfig配置 3.Bitmpa所占内存比较大如果频繁创建回收Bitmap内存可能造成内存抖动我们可以利用inBitmap利用Bitmap内存 4.内存缓存上文我们已经介绍了Glide的弱引用缓存与LRU缓存
其实常见的Bitmap内存优化也就这么几种了不过我们在工作中比较少直接使用他们。 下面我们就介绍下Glide中具体是怎么使用他们的.
3.1 尺寸优化
当装载图片的容器例如ImageView只有100*100而图片的分辨率为800 * 800这个时候将图片直接放置在容器上很容易OOM同时也是对图片和内存资源的一种浪费。当容器的宽高都很小于图片的宽高其实就需要对图片进行尺寸上的压缩将图片的分辨率调整为ImageView宽高的大小一方面不会对图片的质量有影响同时也可以很大程度上减少内存的占用
我们通常使用inSampleSize对Bitmap进行尺寸缩放 如果inSampleSize 设置的值大于1则请求解码器对原始的bitmap进行子采样图像然后返回较小的图片来减少内存的占用例如inSampleSize 4则采样后的图像宽高为原图像的1/4而像素值为原图的1/16也就是说采样后的图像所占内存也为原图所占内存的1/16当inSampleSize 1时就当作1来处理也就是和原图一样大小。另外最后一句还注明inSampleSize的值一直为2的幂如1248。任何其他的值也都是四舍五入到最接近2的幂。 //1int widthScaleFactor orientedSourceWidth / outWidth;int heightScaleFactor orientedSourceHeight / outHeight;//2int scaleFactor rounding SampleSizeRounding.MEMORY? Math.max(widthScaleFactor, heightScaleFactor): Math.min(widthScaleFactor, heightScaleFactor);int powerOfTwoSampleSize;//3if (Build.VERSION.SDK_INT 23 NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {powerOfTwoSampleSize 1;} else {//4powerOfTwoSampleSize Math.max(1, Integer.highestOneBit(scaleFactor));//5if (rounding SampleSizeRounding.MEMORY// exactScaleFactor由各个裁剪策略如CenterCrop重写得到详情可见代码 powerOfTwoSampleSize (1.f / exactScaleFactor)) {powerOfTwoSampleSize powerOfTwoSampleSize 1;}}options.inSampleSize powerOfTwoSampleSize;如上就是Glide图片进行尺寸缩放相关的代码 1.首先计算出图片与View的宽高比 2.根据缩放策略是省内存还是高品质决定取宽高比的最大值还是最小值 3.当Build.VERSION.SDK_INT23时一些格式的图片不能缩放 4.highestOneBit的功能是把我们计算的比例四舍五入到最接近2的幂 5.如果缩放策略为省内存并且我们计算的SampleSizeexactScaleFactor,将inSampleSize*2
如上就是Glide图片加载时做尺寸优化的大概逻辑
3.2 图片格式优化
我们知道Bitmap所占内存大小由宽*高*每像素所占内存决定 上面的尺寸优化决定宽高图片格式优化决定每像素所占内存
在API29中将Bitmap分为ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六个等级。
ALPHA_8不存储颜色信息每个像素占1个字节RGB_565仅存储RGB通道每个像素占2个字节对Bitmap色彩没有高要求可以使用该模式ARGB_4444已弃用用ARGB_8888代替ARGB_8888每个像素占用4个字节保持高质量的色彩保真度默认使用该模式RGBA_F16每个像素占用8个字节适合宽色域和HDRHARDWARE一种特殊的配置减少了内存占用同时也加快了Bitmap的绘制。
每个等级每个像素所占用的字节也都不一样所存储的色彩信息也不同。同一张100像素的图片ARGB_8888就占了400字节RGB_565才占200字节RGB_565在内存上取得了优势但是Bitmap的色彩值以及清晰度却不如ARGB_8888模式下的Bitmap
值得注意的是在Glide4.0之前,Glide默认使用RGB565格式比较省内存 但是Glide4.0之后默认格式已经变成了ARGB_8888格式了,这一优势也就不存在了。 这本身也就是质量与内存之间的取舍如果应用所需图片的质量要求不高也可以修改默认格式
//默认格式修改为了ARGB_8888public static final OptionDecodeFormat DECODE_FORMAT Option.memory(com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat, DecodeFormat.DEFAULT);3.3 内存复用优化
Bitmap所占内存比较大如果我们频繁创建与回收Bitmap那么很容易造成内存抖动,所以我们应该尽量复用Bitmap内存 Glide主要使用了inBitmap与BitmapPool来实现内存的复用
3.3.1 inBitmap介绍
在 Android 3.0API 级别 11开始系统引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项那么采用 Options 对象的解码方法会在生成目标 Bitmap 时尝试复用 inBitmap这意味着 inBitmap 的内存得到了重复使用从而提高了性能同时移除了内存分配和取消分配。不过 inBitmap 的使用方式存在某些限制在 Android 4.4API 级别 19之前系统仅支持复用大小相同的位图4.4 之后只要 inBitmap 的大小比目标 Bitmap 大即可
3.3.2 BitmapPool介绍
通过上文我们知道了可以通过inBitmap复用内存但是还需要一个地方存储可复用的Bitmap,这就是BitmapPool JDK 中的 ThreadPoolExecutor 相信大多数开发者都很熟悉我们一般将之称为“线程池”。池化是一个很常见的概念其目的都是为了实现对象复用例如 ThreadPoolExecutor 就实现了线程的复用机制 BitmapPool即实现了Bitmap的池化
3.3.3 Glide的应用 private static void setInBitmap(BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {Nullable Bitmap.Config expectedConfig null;if (expectedConfig null) {expectedConfig options.inPreferredConfig;}// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.options.inBitmap bitmapPool.getDirty(width, height, expectedConfig);}如上即是Glide设置inBitmap的代码向BitmapPool中传入宽高与格式得到一个可复用的对象这样就实现了Bitmap的内存复用 由于篇幅原因详细的源码这里没有贴出来想要了解更多的读者可参考:Coil 和 Glide 的 Bitmap 缓存复用机制
4.Glide如何管理生命周期?
当我们在做一个网络请示时页面退出时应该中止请示不然容易造成内存泄漏 对于图片加载也是如此我们在页面退出时应该中止请示销毁资源。 但是我们使用Glide的时候却不需要在页面退出时做什么操作说明Glide可以做到在页面关闭时自动释放资源 下面我们一起看下Glide是如何实现的 主要是两步: 1.调用时通过Glide.with传入context,利用context构建一个Fragment 2.监听Fragment生命周期销毁时释放Glide资源
4.1 传入context构建Fragment
//通过Activity拿到RequestManager
public RequestManager get(NonNull Activity activity) {//拿到当前Activity的FragmentManagerandroid.app.FragmentManager fm activity.getFragmentManager();//生成一个Fragment去绑定一个请求管理RequestManagerreturn fragmentGet(activity, fm, /*parentHint*/ null, isActivityVisible(activity));}private RequestManager fragmentGet(NonNull Context context,NonNull android.app.FragmentManager fm,Nullable android.app.Fragment parentHint,boolean isParentVisible) {//①在当前Activity添加一个Fragment用于管理请求的生命周期RequestManagerFragment current getRequestManagerFragment(fm, parentHint, isParentVisible);//获取RequestManagerRequestManager requestManager current.getRequestManager();//如果不存在RequestManager则创建if (requestManager null) {Glide glide Glide.get(context);//②构建RequestManager //current.getGlideLifecycle()就是ActivityFragmentLifecycle也就是构建RequestManager时会传入fragment中的ActivityFragmentLifecyclerequestManager factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);//将构建出来的RequestManager绑定到fragment中current.setRequestManager(requestManager);}//返回当前请求的管理者return requestManager;} 如上所示 1.在当前Activity添加一个透明Fragment用于管理请示生命周期 2.构建RequestManager并传入Fragment生命周期
4.2 RequestManager监听生命周期
public class RequestManager implements LifecycleListener,ModelTypesRequestBuilderDrawable { RequestManager(Glide glide,Lifecycle lifecycle,RequestManagerTreeNode treeNode,RequestTracker requestTracker,ConnectivityMonitorFactory factory,Context context) {... //将当前对象注册到ActivityFragmentLifecyclelifecycle.addListener(this);}//...//RequestManager实现了fragment生命周期回调Overridepublic synchronized void onStart() {resumeRequests();targetTracker.onStart();}Overridepublic synchronized void onStop() {pauseRequests();targetTracker.onStop();}Overridepublic synchronized void onDestroy() {targetTracker.onDestroy();}}public class RequestManagerFragment extends Fragment {//生命周期的关键就在ActivityFragmentLifecycleprivate final ActivityFragmentLifecycle lifecycle;public RequestManagerFragment() {this(new ActivityFragmentLifecycle());}RequestManagerFragment(NonNull ActivityFragmentLifecycle lifecycle) {this.lifecycle lifecycle;}Overridepublic void onStart() {super.onStart();lifecycle.onStart();}Overridepublic void onStop() {super.onStop();lifecycle.onStop();}Overridepublic void onDestroy() {super.onDestroy();lifecycle.onDestroy();unregisterFragmentWithRoot();}//...
}逻辑很简单Fragment生命周期变化会回调RequestManager生命周期然后在进行相关的资源释放工作
4.3 小结 Glide.with(this)绑定了Activity的生命周期。在Activity内新建了一个无UI的Fragment这个Fragment持有一个Lifecycle通过Lifecycle在Fragment关键生命周期通知RequestManager进行相关从操作。在生命周期onStart时继续加载onStop时暂停加载onDestory时停止加载任务和清除操作。
5.Glide怎么做大图加载
对于图片加载还有种情况就是单个图片非常巨大并且还不允许压缩。比如显示世界地图、清明上河图、微博长图等 首先不压缩按照原图尺寸加载那么屏幕肯定是不够大的并且考虑到内存的情况不可能一次性整图加载到内存中 所以这种情况的优化思路一般是局部加载通过BitmapRegionDecoder来实现 这种情况下通常Glide只负责将图片下载下来,图片的加载由我们自定义的ImageView来实现
5.1 BitmapRegionDecoder介绍
BitmapRegionDecoder主要用于显示图片的某一块矩形区域如果你需要显示某个图片的指定区域那么这个类非常合适。 对于该类的用法非常简单既然是显示图片的某一块区域那么至少只需要一个方法去设置图片一个方法传入显示的区域即可 举个例子
//设置显示图片的中心区域
BitmapRegionDecoder bitmapRegionDecoder BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options new BitmapFactory.Options();
options.inPreferredConfig Bitmap.Config.RGB_565;
Bitmap bitmap bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 100, height / 2 100), options);
mImageView.setImageBitmap(bitmap);更详细的实现可见Android 高清加载巨图方案 拒绝压缩图片 不过这种方法虽然也能加载大图但做的还不够滑动时内存抖动卡顿现象比较明显不能用于线上
下面介绍一种可以用于线上的大图加载方案
5.2 可用于线上的大图加载方案
介绍一个开源库subsampling-scale-image-view SubsamplingScaleImageView将大图切片再判断是否可见如果可见则加入内存中否则回收减少了内存占用与抖动 同时根据不同的缩放比例选择合适的采样率进一步减少内存占用 同时在子线程进行decodeRegion操作解码成功后回调至主线程减少UI卡顿.
总结
本文主要以Glide做了哪些优化为切入点回答了如下几个问题 1.说一下Glide图片加载的总体流程 2.Glide缓存机制做了哪些优化 3.Glide做了哪些内存优化 4.Glide如何管理生命周期? 5.Glide怎么做大图加载?
下面我针对Android 中不同的技术模块整理了一些相关的 学习文档笔记 及相应的 面试习题含答案大家可以针对性的进行参考学习与复习。