1.Android Glide源码教学十年老师傅带你飞
面试官:为什么用Glide,而不选择其它图片加载框架?
链式调用,生命周期,解耦
面试官:有看过它的源码吗?跟其它图片框架相比有哪些优势?
同上,fresc要写在xml中,而且宽和高要确定或者按比例
正式开始:
面试官:如何实现一个图片加载框架?
概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,压缩,显示等操作。
封装参数: 从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;
解析路径: 图片的来源有多种,格式也不尽相同,需要规范化;
读取缓存: 为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;
查找文件/下载文件: 如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;
解码: 这一步是整个过程中最复杂的步骤之一
变换: 解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等);
缓存: 得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;
显示: 显示结果,可能需要做些动画(淡入动画,crossFade等)
流程图“”: https://juejin.cn/post/6844904002551808013
面试官:那么具体的,Glide比较关注的有哪些?如何加载图片的?
异步加载:线程池
切换线程:Handler,没有争议吧
缓存:LruCache、DiskLruCache
防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
内存泄露:注意ImageView的正确引用,生命周期管理
列表滑动加载的问题:加载错乱、队满任务过多问题
面试官:有看过它的源码吗?怎么看的?
Glide最基本的用法就是三步走,先with(),再load(),最后into()
Glide.with(this).load(url).into(imageView);前面2步是最简单的,后面最复杂
比较:picasso,发现:先with(),再load(),最后into()
面试官:with()里面做了什么事情?
绑定生命周期,glide最大的优势,glide最大的优点就是对bitmap的管理是跟随生命周期去发生改变的。其它的框架,当Activity销毁的时候,是不会释放之前加载图片占用的所有内存。
glide的优势就是当Activity销毁的时候,之前加载的所有图片的内存都释放了
Glide.with(Activity) 主要做了 线程池 + 缓存 + 请求管理与生命周期绑定+其它配置初始化的构建
面试官:生命周期具体是怎么绑定的?如果感知生命周期的? lifecycyle?如何避免内存泄露的?
源码分析:
@NonNull
public RequestManagerget(@NonNull View view) {
if (Util.isOnBackgroundThread()) {
return get(view.getContext().getApplicationContext());
}
Preconditions.checkNotNull(view);
Preconditions.checkNotNull(
view.getContext(), "Unable to obtain a request manager for a view without a Context");
Activity activity =findActivity(view.getContext());
// The view might be somewhere else, like a service.
if (activity ==null) {
return get(view.getContext().getApplicationContext());
}
// Support Fragments.
// Although the user might have non-support Fragments attached to FragmentActivity, searching
// for non-support Fragments is so expensive pre O and that should be rare enough that we
// prefer to just fall back to the Activity directly.
if (activityinstanceof FragmentActivity) {
Fragment fragment = findSupportFragment(view, (FragmentActivity) activity);
return fragment !=null ? get(fragment) : get((FragmentActivity) activity);
}
// Standard Fragments.
android.app.Fragment fragment = findFragment(view, activity);
if (fragment ==null) {
return get(activity);
}
return get(fragment);
}
实际上只有两种情况而已,即传入Application类型的参数,和传入非Application类型的参数
@NonNull
public RequestManagerget(@NonNull Fragment fragment) {
Preconditions.checkNotNull(
fragment.getContext(),
"You cannot start a load on a fragment before it is attached or after it is destroyed");
if (Util.isOnBackgroundThread()) {
return get(fragment.getContext().getApplicationContext());
}else {
FragmentManager fm = fragment.getChildFragmentManager();
return supportFragmentGet(fragment.getContext(), fm, fragment, fragment.isVisible());
}
}
如果有fragment,是否还会创建空白fragment
public class SupportRequestManagerFragmentextends Fragment {
@Override
public void onDetach() {
super.onDetach();
parentFragmentHint =null;
unregisterFragmentWithRoot();
}
@Override
public void onStart() {
super.onStart();
lifecycle.onStart();
}
@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}
SupportRequestManagerFragment 里面通过监听,把方法回调出去了。
class ActivityFragmentLifecycleimplements Lifecycle {
private final SetlifecycleListeners =
Collections.newSetFromMap(new WeakHashMap());
private boolean isStarted;
private boolean isDestroyed;
@Override
public void addListener(@NonNull LifecycleListener listener) {
lifecycleListeners.add(listener);
if (isDestroyed) {
listener.onDestroy();
}else if (isStarted) {
listener.onStart();
}else {
listener.onStop();
}
}
@Override
public void removeListener(@NonNull LifecycleListener listener) {
lifecycleListeners.remove(listener);
}
void onStart() {
isStarted =true;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStart();
}
}
void onStop() {
isStarted =false;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStop();
}
}
然后构建RequestManager并传入Fragment生命周期
public RequestManager(
@NonNull Glide glide,
@NonNull Lifecycle lifecycle,
@NonNull RequestManagerTreeNode treeNode,
@NonNull Context context) {
this(
glide,
lifecycle,
treeNode,
new RequestTracker(),
glide.getConnectivityMonitorFactory(),
context);
}
*/
@Override
public synchronized void onStart() {
resumeRequests();
targetTracker.onStart();
}
/**
* Lifecycle callback that unregisters for connectivity events (if the
* android.permission.ACCESS_NETWORK_STATE permission is present) and pauses in progress loads.
*/
@Override
public synchronized void onStop() {
pauseRequests();
targetTracker.onStop();
}
/**
* Lifecycle callback that cancels all in progress requests and clears and recycles resources for
* all completed requests.
*/
@Override
public synchronized void onDestroy() {
RequestManager里面进行请求的暂停,取消,重新开始等等!
逻辑很简单:
Fragment生命周期变化会回调RequestManager生命周期。
public RequestManager(
@NonNull Glide glide,
@NonNull Lifecycle lifecycle,
@NonNull RequestManagerTreeNode treeNode,
@NonNull Context context) {
因为RequestManager里面有LifecycleListener接口,所有可以把生命周期回调出去。
三者的关系是这样:fragmnet里面通过LifecycleListener回调出去,RequestManager注册监听。fragmnet把自己的注册器给了RequestManager.
流程图:
demo:
总结:
Glide.with(this)绑定了Activity的生命周期。在Activity内新建了一个无UI的Fragment,这个Fragment持有一个Lifecycle,通过Lifecycle在Fragment关键生命周期通知RequestManager进行相关从操作。在生命周期onStart时继续加载,onStop时暂停加载,onDestory时停止加载任务和清除操作。
面试官:glide调用在子线程会怎么样?
在UI线程中调用Glide,在子线程使用的话会和使用ApplicationContext一样;
可以理解为不对请求的生命周期进行管理
1、传入Application Context或者在子线程使用:调用getApplicationManager(context);这样Glide的生命周期就和应用程序一样了。可以理解为不对请求的生命周期进行管理
@NonNullprivate RequestManagergetApplicationManager(@NonNullContext context){// Either an application context or we're on a background thread.if(applicationManager==null){synchronized(this){if(applicationManager==null){// Normally pause/resume is taken care of by the fragment we add to the fragment or// activity. However, in this case since the manager attached to the application will not// receive lifecycle events, we must force the manager to start resumed using// ApplicationLifecycle.// TODO(b/27524013): Factor out this Glide.get() call.Glide glide=Glide.get(context.getApplicationContext());applicationManager=factory.build(glide,newApplicationLifecycle(),newEmptyRequestManagerTreeNode(),context.getApplicationContext());}}}returnapplicationManager;}
classApplicationLifecycleimplementsLifecycle{@OverridepublicvoidaddListener(@NonNullLifecycleListenerlistener){listener.onStart();}@OverridepublicvoidremoveListener(@NonNullLifecycleListenerlistener){// Do nothing.}}
面试官:load()里面做了什么事情?
load() 调用的是 RequestManager.load()
返回一个请求:request=load(String string)
public DrawableTypeRequest<String> load(String string) {
return (DrawableTypeRequest<String>) fromString().load(string);
}
总结;最终load()方法返回的其实就是一个DrawableTypeRequest对象
Glide的缓存功能,大部分都是在load()方法中进行的
面试官:into()里面做了什么事情?
2个队列,一个用set方法,一个用list方法。为什么?
请求:和okhttp一样,有2个队列,运行和准备队列。 在不同生命周期会调用队列的一些处理方法。
private final Setrequests = Collections.newSetFromMap(new WeakHashMap());
// A set of requests that have not completed and are queued to be run again. We use this list to maintain hard
// references to these requests to ensure that they are not garbage collected before they start running or
// while they are paused. See #346.
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final ListpendingRequests =new ArrayList();
总结:请求服务器,变换,显示都是这里处理的。
面试官:为什么要用缓存?
1、减少流量消耗,加快响应速度;
2、Bitmap 的创建/销毁比较耗内存,可能会导致频繁GC;使用缓存可以更加高效地加载 Bitmap,减少卡顿。
为什么用内存缓存?:增加图片读取的速度,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,
为什么用硬盘缓存? 而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。
面试官:Glide缓存机制是怎样的?
磁盘缓存+二级内存缓存
面试官:为什么内存缓存用2层?为什么不能只使用软引用?
弱引用它是一个随时可能被回收的资源,只使用软引用作为图片缓存的手段效率是比较低的,因为不能控制软引用的图片什么时候被系统回收。而需要被频繁使用的图片也可能被回收,导致要重新从网络上下载。因此,不推荐单独使用软引用作为App中缓存图片的唯一形式。
为什么有Lrucache,还需要弱引用
之所以需要 activeResources,它是一个随时可能被回收的资源,memory 的强引用频繁读写 可能造成内存激增频繁 GC,而造成内存抖动。资源在使用过程中保存在 activeResources 中, 而 activeResources 是弱引用,随时被系统回收,不会造成内存过多使用和泄漏
之所以要设计两种内存缓存的原因是为了防止加载中的图片被LRU回收。ActiveResources 就是一个弱引用的 HashMap ,用来缓存正在使用中的图片,使用 ActiveResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉
面试官:存是怎么存的?,取是怎么取的?
(不同的版本不一样?)比如郭霖大神那个版本就不是
glide是如何取图片的?(取图片)
@Nullable
private EngineResourceloadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
EngineResource active = loadFromActiveResources(key);
if (active !=null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
EngineResource cached = loadFromCache(key);
if (cached !=null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
源码中我没有看到去硬盘中取,因为磁盘不是内存缓存!!!
WeakReference---LruCache-----------DiskCache
4.8版本大致总结一下: 首先从弱引用读取缓存,没有的话通过Lru读取,有则取,并且加到弱引用中,如果没有会开启EngineJob进行后面的图片加载逻辑。
图片是如何存放的?(存图片)
LruCache------- -WeakReference---------DiskCache(不准确
结构图:
面试官:Lrucache是什么样的数据结构?linkHashmap的算法结构,LRUcache的实现和时间复杂度?
LruCache算法的实现,你会发现它其实是用一个LinkedHashMap来缓存对象的,每次内存超出缓存设定的时候,就会把最近最少使用的缓存去掉,因此有可能会把正在使用的缓存给误伤了,我还在用着它呢就给移出去了。因此这个弱引用可能是对正在使用中的图片的一种保护,使用的时候先从LruCache里面移出去,用完了再把它重新加到缓存里面。
LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,(LruCache本身是强引用,里面包的对象是软应用)
privatestaticLruCache<String,SoftReference<Bitmap>>mLruCache=newLruCache<String,SoftReference<Bitmap>>(10*1024){@OverrideprotectedintsizeOf(Stringkey,SoftReference<Bitmap>value){//默认返回1,这里应该返回Bitmap占用的内存大小,单位:K//Bitmap被回收了,大小是0if(value.get()==null){return0;}returnvalue.get().getByteCount()/1024;}};
LruCache算法,Least Recently Used,又称为近期最少使用算法。主要算法原理就是把最近所使用的对象的强引用存储在LinkedHashMap上,并且,把最近最少使用的对象在缓存池达到预设值之前从内存中移除。
面试官:Glide加载一个一兆的图片(100*100),是否会压缩后再加载,放到一个200*200的view上会怎样,1000*1000呢,图片会很模糊,怎么处理?
根本原因:保存的时候,URl+宽高保存的。
当我们调整imageview的大小时,Picasso会不管imageview大小是什么,总是直接缓存整张图片,而Glide就不一样了,它会为每个不同尺寸的Imageview缓存一张图片,也就是说不管你的这张图片有没有加载过,只要imageview的尺寸不一样,那么Glide就会重新加载一次,这时候,它会在加载的imageview之前从网络上重新下载,然后再缓存。
缓存的key:
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,resourceClass, transcodeClass, options);
缓存一般通过键值对的形式,Glide以一个EngineKey对象当作key,缓存的键包括图片的宽、高、signature等参数。而OriginalKey对象其实就存了EngineKey的真实key,并重写其equals方法,所以这个对象就可以作为source资源的key,从source磁盘缓存读取时就以这个为key而写入磁盘缓存时,会将无论是EngineKey还是OriginalKey的字段通过SHA-256加密并转成16进制字符串当作名字存入本地
面试官:gilde是如何加载git图片的?会出现什么问题
Glide加载gif的卡顿优化思路分析
至此我们就知道了Glide加载Gif图片的原理了,就是将gif根据每一帧解析成很张图片,然后在依次设置给ImageView。
面试官:
当图片没变,但是 图片的链接一直在变的时候,怎么缓存?
比如使用七牛云的时候,会在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:
http://url.com/image.jpg?token=d9caa6e02c990b0a
而使用Glide加载这张图片的话,也就会使用这个url地址来组成缓存Key。
但是接下来问题就来了,token作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果token变了,那么图片的url也就跟着变了,图片url变了,缓存Key也就跟着变了。结果就造成了,明明是同一张图片,就因为token不断在改变,导致Glide的缓存功能完全失效了。
解决办法就是重写 GlideUrl 的 getCacheKey() 方法,把会变的一部分的值给干掉,就可以解决问题。
public classMyGlideUrlextendsGlideUrl{private String mUrl;publicMyGlideUrl(String url){super(url);mUrl=url;}@Override public StringgetCacheKey(){returnmUrl.replace(findTokenParam(),"");}private StringfindTokenParam(){String tokenParam="";int tokenKeyIndex=mUrl.indexOf("?token=")>=0?mUrl.indexOf("?token="):mUrl.indexOf("&token=");if(tokenKeyIndex!=-1){int nextAndIndex=mUrl.indexOf("&",tokenKeyIndex+1);if(nextAndIndex!=-1){tokenParam=mUrl.substring(tokenKeyIndex+1,nextAndIndex+1);}else{tokenParam=mUrl.substring(tokenKeyIndex);}}returntokenParam;}}//使用Glide.with(this).load(newMyGlideUrl(url)).into(imageView);
我们需要在load()方法中传入这个自定义的MyGlideUrl对象,而不能再像之前那样直接传入url字符串了。不然的话Glide在内部还是会使用原始的GlideUrl类,而不是我们自定义的MyGlideUrl类。
面试官:glide如何防止内存泄漏的?
简单说一下内存泄漏的场景,如果在一个页面中使用Glide加载了一张图片,图片正在获取中,
如果突然关闭页面,这个页面会造成内存泄漏吗?
ImageView 内存泄露
曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,原因是SDK中有个加载网络头像的方法,持有ImageView引用导致的。
当然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。
事实上,这种方式虽然解决了内存泄露问题,但是并不完美,例如在界面退出的时候,我们除了希望ImageView被回收,同时希望加载图片的任务可以取消,队未执行的任务可以移除。
Glide的做法是监听生命周期回调,看 RequestManager 这个类
publicvoidonDestroy(){targetTracker.onDestroy();for(Target<?>target:targetTracker.getAll()){//清理任务clear(target);}targetTracker.clear();requestTracker.clearRequests();lifecycle.removeListener(this);lifecycle.removeListener(connectivityMonitor);mainHandler.removeCallbacks(addSelfToLifecycle);glide.unregisterRequestManager(this);}
面试官:glide如何防止OOM?Glide做了哪些内存优化?
方法1:LruCache缓存大小设置,
方法2:onLowMemory
当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。
图片格式优化:
Picasso的默认质量是 ARGB_8888 ,Glide的默认质量则为 RGB_565
值得注意的是在Glide4.0之前,Glide默认使用RGB565格式,比较省内存但是Glide4.0之后,默认格式已经变成了ARGB_8888格式了,这一优势也就不存在了
面试官:glide第一次运行的时候,加载splashActiivty很慢,如何优化?
会初始化所有的配置module,requestManager(网络层,工作线程,option)
面试官:Glide的线程池 是怎样的?异步加载怎么样的?
线程池,多少个?
缓存一般有三级,内存缓存、硬盘、网络。
由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。
读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。
Glide 必然也需要多个线程池,看下源码是不是这样
publicfinalclassGlideBuilder{...privateGlideExecutorsourceExecutor;//加载源文件的线程池,包括网络加载privateGlideExecutordiskCacheExecutor;//加载硬盘缓存的线程池...privateGlideExecutoranimationExecutor;//动画线程池
Glide使用了三个线程池,不考虑动画的话就是两个。
我看了下是2个
private ExecutorServicesourceService;
private ExecutorServicediskCacheService;
面试官:如何实现 圆角?
通过变换,transform
面试官:自适应图片宽高可以做到吗?如何根据图片的url获取图片的宽和高。瀑布流这种形式!
比如:CenterCrop类源码分析
图片变换:比别的框架牛
Glide.with(this) .load(url) .transform(...) .into(imageView);
transform()方法
变化库依赖:glide-transformations的项目主页地址是 https://github.com/wasabeef/glide-transformations 。
面试官:有用过Glide的什么深入的API,自定义model是在Glide的什么阶段
Glide Modules
Glide modules是一个全局改变Glide行为的抽象的方式。你需要创建Glide的实例,来访问GlideBuilder
Glide的自定义模块功能
public class MyGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}
举例1:比如图片是8888,我要变成565
public class MyGlideModule implements GlideModule {
public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}
举例2:把网络请求HttpURLConnection换成okhttp
Glide Module 案例: 通过加载自定义大小图片优化
面试官:如何实现先加载静态图片。让它loadding,然后加载完成加载gif图!
就是上面说的自定义module,然后HttpURLConnection换成okhttp。然后okhttp拦截器监听!!
面试官:如何切换线程的?
图片异步加载成功,需要在主线程去更新ImageView,
无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。
看下Glide 相关源码:
classEngineJob<R>implementsDecodeJob.Callback<R>,Poolable{privatestaticfinalEngineResourceFactoryDEFAULT_FACTORY=newEngineResourceFactory();//创建HandlerprivatestaticfinalHandlerMAIN_THREAD_HANDLER=newHandler(Looper.getMainLooper(),newMainThreadCallback());
public >Y into(Y target) {
Util.assertMainThread();
if (target ==null) {
throw new IllegalArgumentException("You must pass in a non null Target");
}
if (!isModelSet) {
throw new IllegalArgumentException("You must first set a model (try #load())");
}
Request previous = target.getRequest();
if (previous !=null) {
previous.clear();
requestTracker.removeRequest(previous);
previous.recycle();
}
Request request = buildRequest(target);
target.setRequest(request);
lifecycle.addListener(target);
requestTracker.runRequest(request);
return target;
}
面试官:列表加载错乱怎么解决的?
由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。
常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。
当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。
图片和url错位的问题:
工具类中有一个地方是显示UI之前,拿到最新的url地址recentlyUrl和url进行比对,一致的情况下才将图片设置到当前的控件。
其实这里最主要涉及到像Listview和Recyclerview控件的复用原理,Listview和Recyclerview之所以能滑动无限的数量最主要原因就是在于他们巧妙的复用性。
比如在recyclerview中大量的图片加载到图片控件上,ImageView控件的个数其实就比一屏能显示的图片数量稍微多一点而已,移出屏幕的ImageView控件会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取ImageView控件。
当一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。
被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而网络请求是耗时操作,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,
因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。
解决办法:
1.使用setTag()方式。但是,Glide图片加载也是使用这个方法,所以需要使用setTag(key,value)方式进行设置,这种方式是不错的一种解决方式,注意取值的时候应该是getTag(key)这个方法,当异步请求回来的时候对比下tag是否一样,再判断是否显示图片,我使用的是position设置tag.
面试官:Coil和Glide比较?
Coil:kotlin,体积小
面试官:Glide怎么做大图加载
对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等
首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中
所以这种情况的优化思路一般是局部加载,通过
BitmapRegionDecoder来实现
这种情况下通常
Glide只负责将图片下载下来,图片的加载由我们自定义的ImageView来实现