一些Okhttp,Glide,LiveData相关面试题
在前面的文章里面,我写了两篇面试相关的文章,分别针对八股文和手写题,但是在一个常规面试中,一个经验丰富的面试官是会根据你简历中的内容来问对应的问题,比如简历中提到了熟练某某三方库,精通某某三方库,那么面试官就会问一些跟这些三方库相关的问题,基本都是围绕着源码入手,检验你对这些三方库的理解有多深,但是这些库的源码都是非常庞大的,想都要看完着实需要一些时间,所以为了给大家节省一点时间,同时让自己对这方面也复习一下,这里整理了一些有关Okhttp,Glide和LiveData这三个常用库的一些面试题,有兴趣的朋友可以一起来看下
OKHttp篇
版本:4.12.0
1.OKHttp中是如何实现同步和异步请求的
在OKHttp中,我们如果要发送一个请求,肯定首先是需要创建一个OKHttpClient
对象,再创建一个这个请求对应的Request
对象,然后再调用newCall
函数将Request
对象绑定在OKHttpClient
对象上,对应代码如下

通过newCall
函数我们可以创建一个RealCall
实例,它是接口Call
的实现类,Call
里面有两个方法分别代表着同步请求与异步请求
execute
:是同步请求,不接收参数,当使用同步请求的时候,会立刻触发该请求,并且会阻塞其他请求直到获得请求结果或者请求出现错误enqueue
:是异步请求,接收一个callback
参数,当使用异步请求的时候,会通过一个分发器来决定该请求何时进行,多数情况下也是立刻执行,除非同时有其他请求也在进行,请求结果或者异常会通过callback
回调过来
2.分发器是如何工作的
上面的问题里面说到了分发器,那么什么是分发器,分发器到底是做了些什么事情呢?首先来看下同步请求

在同步请求中,分发器dispatcher
调用了executed
方法去执行当前RealCall
实例,随后就是调用方法getResponseWithInterceptorChain
,这个方法一看就知道是跟拦截器相关,先不看,我们看下executed
方法里面的代码

逻辑还是很简单的,直接将RealCall
实例加入到了一个runningSyncCalls
的同步队列中,并且如果当前没有正在被执行的同步或者异步任务,那么该同步任务就会执行,该逻辑存在于finished
和promoteAndExecute
函数中

而异步请求会先经过enqueue
函数,该函数中会先将异步请求加入到一个准备请求的队列中

然后再去判断该请求是不是webSocket
请求,如果不是的话就去正在执行和准备执行队列中查找是否存在同样的请求,如果存在就拿出来复用,最后同样也走到了promoteAndExecute
函数中,来看下这个函数的完整代码

这段代码的作用就是将准备执行队列中符合条件的请求挪入正在请求的队列中,并且将这些可执行请求添加到线程池里面去执行,所以总的来说,分发器就是维护在请求时的队列以及线程池
3.都有哪些拦截器,作用分别是什么?
上面讲到了无论是同步还是异步请求,都会经过getResponseWithInterceptorChain
这个函数,而这个函数内部就是开始做一些拦截器相关的工作,点进去可以看到有这么几个拦截器

interceptors
:自定义的普通拦截器,可以添加一些自定义的header
,通用参数等RetryAndFollowUpInterceptor
:用于请求失败的重试以及重定向的工作BridgeInterceptor
:应用层与网络层的桥接拦截器,用来添加cookie,添加一些固定的header,比如Host
,Content-length
,Content-Type
,User-Agent
,并且负责解压一些使用gzip压缩过的响应数据CacheInterceptor
:缓存拦截器,如果某些请求符合缓存的要求,那么就不会发起请求ConnectInterceptor
:连接拦截器,实现与服务器的真正连接networkInterceptor
:网络拦截器,自定义的与网络相关的拦截器,比如网络层的数据传输CallServerInterceptor
:请求拦截器,与服务器之间进行请求,数据通信
4.拦截器链是如何工作的
依然是在getResponseWithInterceptorChain
这个函数里面,当把所有拦截器都添加interceptors
这个数组里面后,就使用这个数组创建了一个RealInterceptorChain
的对象

这个对象就是带动整个拦截器链工作的驱动,它被创建后会直接调用内部的proceed
函数

可以看到proceed
函数内部是先用copy
函数将下一个拦截器节点取出,然后执行当前拦截器的intercept
函数,同时也会把下一个拦截器节点next
作为参数传到intercept
函数内,在里面也会调用proceed
函数执行下一个,当走到最后一个CallServerInterceptor
的时候,就不在进行proceed
操作了,而是获取与服务器的通讯的response,在一级一级往上返回,直到回到我们最初
getResponseWithInterceptorChain
这个地方,那么请求的响应结果就拿到了
5.如何复用TCP连接
创建一个TCP连接是很消耗资源的,如同线程一样,线程为了解决创建线程带来的资源消耗问题使用了线程池,那么OKHttp里面也有一个连接池专门来解决不用频繁创建TCP连接的问题,这部分是写在了ConnectInterceptor
拦截器里面,当我们点到ConnectInterceptor
这个类里面后,会看到只有这么几句代码

只有一句关键代码,就是call.initExchange
,这个函数是要去创建一个完整的拦截器链必不可少的Exchange
对象,所以真正逻辑要从initExchange
函数开始

在这段代码里面,我们有看到创建Exchange
对象的代码,其中第四个参数codec
是要去创建的,其他三个都是已知参数,还没有看到任何复用的逻辑,那么一定是还要再到exchanger.find
代码里面看下

find
里面也没有太多代码,但是有个findHealthyConnect
的函数,从这个函数名上就能知道离我们的答案不远了,继续点到这个函数里面

在这个函数里面,其实就是一个无限循环,循环做了什么事情呢,就是不断通过findConnection
函数获取RealConnection
对象,然后判断它是否能用,能的话直接返回,不能的话从连接池中删除,然后再去判断是否还有其他连接能用,能的话则循环不退出继续从findConnection
中获取下一个RealConnection
对象,那么我们继续跑到findConnection
函数中去看

在findConnection
函数中做了很多事情,也是复用TCP连接的关键代码所在,首先第一步会看下当前连接是否还能用,可以的话会直接返回,不可以的话会直接释放该连接

释放了当前连接后会去连接池里查看是否有符合条件的连接,有的话就直接拿出来用

如果连接池里面没有符合条件的连接的话,那么就要去新建一个了,首先就去查找下一个需要尝试的路由是哪一个,接着再去创建一个新的RealConnection
对象

新对象创建好之后,再将之前外面传进来的拦截器链的一些参数设置进去,然后把这个最新连接添加进连接池里面并返回,整个TCP的复用过程就结束了
6.OKHttp里面都用了哪些设计模式
构建者模式
这个应该是最明显,在创建OKHttpClient
和Request
的时候就用到了

责任链模式
这个也容易,在发起一个请求的时候,就会经过一串拦截器组成的拦截器链RealInterceptorChain
,这个就是责任链模式的具体表现

外观模式
外观模式的定义就是为子系统的一组接口提供一个高级接口或者高级类,使得子系统更容易使用,而在OKHttp中,OKHttpClient
就扮演着这个高级类的角色
享元模式
享元模式的定义就是通过共享对象来有效的支持大量细粒度的对象,从而实现节省内存,提高效率的目的,在OKHttp中连接池就用到了享元模式,复用TCP连接,使得不用频繁去创建TCP连接,节省了内存,也提高了连接效率
Glide篇
版本:4.16.0
1.Glide加载图片的时候是如何与生命周期绑定在一起
这个问题拿出来说是因为新老版本的Glide的处理方式不一样了,之前知道的是Glide通过添加一个叫做RequestManagerFragment
的无布局页面去感知生命周期,然后在对应的回调里面去调用lifeCycle相对于的方法,但是在4.16.0版本中,发现已经弃用RequestManagerFragment
这个类了

既然弃用了,那么又是如何感知生命周期的呢,我们还是用最直接的方法,从with
函数内一步一步点进去看,首先with
函数内基本保持不变

还是先去获取请求管理者检索器,再去调用get
获取RequestManager
,而在get
方法里面,就有所不一样了

我们看到创建RequestMaager
的逻辑是在getOrCreate
函数内进行,并且函数参数里面就有当前activity的lifeCycle
对象

在这个函数里面,我们看到了首先是根据传进来的lifeCycle创建了一个LifecycleLifecycle
对象,然后利用这个对象创建了一个RequestManager
,然后将当前页面的lifecycle
与这个RequestManager
保存在一个lifecycleToRequestManager
的Map
里面,这样就完成了每个RequestManager
与生命周期绑定在一起,随后在LifecycleLifecycle
上也添加了生命周期的监听回调,当页面处于销毁状态时候,就在Map
中移除该lifecycle
,这样就能做到在页面销毁时候停止图片加载,减少内存泄漏的风险
2.同一个图片地址分别加载在不同大小的ImageView上,会去读取同一个缓存吗
这个问题其实就是看你知不知道缓存key的生成逻辑,我们从源码上找下答案,这里既然说已经是加载到ImageView
上了,那自然得是从into
方法进入,into
方法先是设置了一下transform
参数,然后就是进入到into
的另一个重载方法里面

这个方法里面,我们去看buildRequest
方法,这个是建立请求的逻辑,内部的代码长这样

里面会去调用buildRequestRecursive
方法,我们看到这个方法里面的参数有两个是关于图片的宽高的,说明加载一张图片的时候,这个图片的宽高也会影响一些东西,接着往下看,我们一路从buildRequestRecursive
这个方法里面点进去,最终会走到SingleRequest
这个类里面

从这个类的注释我们可以知道,它就是负责将一个图片资源加载到组件里面去的,它里面有很多重写的方法,我们先看begin
方法,也就是开始请求部分,里面会经过这段代码

当传进来的宽高都是有效值的时候,就会走到onSizeReady
方法里面,这个方法里面会调用Engine
类的load
方法,我们的图片资源就从这个方法开始加载了,而在这个方法的开头,就是先去创建缓存的key
值

从代码中可以看到,一个图片的缓存key
有很多元素组成,除了它的宽高之外,还有签名,transform
等其他元素,任何一个元素改变了,Glide都会当做没有这个缓存而去重新加载图片资源,所以不同大小的ImageView
加载同一张图片,用到的缓存是不一样的
3.简单说一下Glide的缓存机制
Glide的缓存分为内存缓存和磁盘缓存,其中内存缓存由弱引用和LruCache
组成
弱引用
弱引用主要体现在ActiveResources
这个类中,可以从类里面的变量声明发现,它其实就是一个存储弱引用的HashMap
,它的key
就是我们上面说的用图片宽高,签名等一系列参数创建出来的,value
则是图片资源的弱引用,这些图片资源是刚好正在使用中的图片资源

LruCache
对于那些暂时不会使用的图片资源,或者已经从弱引用中被回收的资源,Glide会将它们转移至LruCache
里面,相关代码可以从ActiveResources
中的get
方法中看到

在第二个if中,当从弱引用中拿不到资源时候,那么就说明该资源已经被回收,我们就必须将这个资源挪入LruCache
里面,在cleanupActiveReference
方法中,最终会经过一个onResourcesReleased
的回调

在这个回调中,就会把资源添加到LruCache
中,这个部分逻辑在Engine
类中

LruCache
底层是一个LinkedHashMap
,把最近使用过的图片插入到头部,而没有使用过的则插入至尾部,当图片总数达到预先设置的阀值的时候,就会从尾部删除图片

判断是否达到阈值的逻辑在put
方法里面

onItemEvicted
方法的具体实现在LruCache
的子类LruResourceCache
里面,里面是调用了一个onResourceRemoved
回调方法

这个回调方法的实现是在Engine
里面

通过ResourceRecycler
将资源回收

磁盘缓存
应该不少人都会有疑问,为什么Glide有了内存缓存后,还要有磁盘缓存呢,其实这两种缓存的意义是不一样的
- 内存缓存:防止图片重复被写入内存,导致内存泄漏
- 磁盘缓存:防止反复从网络加载图片
我们有时候在使用Glide加载图片的时候,会去调用diskCacheStrategy
这个函数来对磁盘缓存进行一些配置,它有如下几个值,分别代表的意思是
- NONE:表示不缓存任何内容,也可以理解为禁用磁盘缓存
- DATA:表示缓存原始数据
- RESOURCE:表示缓存转换过的图片
- ALL:表示既缓存原始数据,也缓存转换过的数据
- AUTOMATIC:基于
DataFetcher
和EncodeStrategy
智能的选取一种策略
LiveData篇
版本:2.6.1
1.LiveData是如何感知生命周期的
当我们用LiveData去创建一个监听器的时候,通常会调用observe
方法,然后传入两个参数,第一个参数是当前所在的Activity
或者Fragment
,第二个参数是数据监听器

而这个第一个参数,它其实是一个LifecycleOwner
,翻译成中文就是生命周期持有者,它是一个接口,在ComponentActivity
中已经被实现了,使用它主要是做两件事情,获取当前生命周期以及注册生命周期事件观察者

那么什么是生命周期事件观察者呢,其实在ComponentActivity
中已经注册了几个观察者,分别处理了一些相应的事件

上面代码中是截取了其中两个观察者的监听事件,分别是在生命周期处于DESTROY
状态时候情况下清空ViewModelStore
以及在生命周期改变的时候移除生命周期监听器,对于注册事件观察者很简单,只需要在addObserver
方法中添加一个LifecycleEventObserver
的实例,就可以在onStateChanged
方法中做想要做的事情,而我们LiveData在监听数据变化的时候,也用到了LifecycleEventObserver
,看下observe
方法的源码

内部创建了一个叫LifecycleBoundObserver
的实例,传入的参数就是生命周期持有者以及数据观察者,而LifecycleBoundObserver
实现了我们LifecycleEventObserver
接口

说明它也是可以通过监听生命周期来做一些事情,而在onStateChanged
方法里面我们也能看到,在DESTROYED
状态的时候,移除了观察者,这样LiveData也就停止了对数据的监听
2.如何证明LiveData是粘性的
首先要理清楚一个概念,就是什么叫粘性,当一个新的观察者接收到老的数据的时候,我们就把这个现象叫做粘性,具体体现在了LiveData内部的版本号上,我们从setValue
方法开始看

当我们调用setValue
设置了一个新的数据的时候,它除了将数据赋值给本地mData
变量之外,还自增了一下它的版本号mVersion
,而这个mVersion
在每个观察者里面也维护着一个,初始化值为-1

知道了这些后我们看dispatchingValue
方法

由于参数initiator
是null
,所以我们看else
分支内的代码,它是对mObservers
的一个遍历,将每一个值也就是ObserverWrapper
作为参数给到considerNotify
方法,在considerNotify
方法内

内部会经过三层判断,观察者是否活跃,观察者绑定的生命周期持有者是否活跃以及传入的观察者的版本号是否大于当前版本号,这三个判断都通过了才会将数据传入onChanged
方法,而当有新的观察者注册进来的时候,由于它的版本号是-1,必定小于当前LiveData的版本号,那么这个观察者就会接收到之前已经发送过的所有数据,直到版本号不小于当前LiveData的版本号
3.observeForever是干什么用的
有时候在创建监听器的时候,在编辑器里面敲出observe,提示框里面除了observe
的函数,还会出现一个叫observerForever
的函数,那么这个函数是干什么用的呢?首先从字面上我们大概能判断出一些,永远监听,这个永远就体现在它不需要LifecycleOwner
参数,看下它的源码

跟observe
函数有点相似,但是它就没有LifecycleOwner的参数,说明它不会受生命周期变化而被影响,代码中,它这里是创建了一个叫AlwaysActiveObserver
的对象,它是ObserverWrapper
的子类,里面就重写了一个方法

这里将shouldBeActive
设置为true
,将它设置成true
的作用是啥呢?还记得之前说的setValue
时候需要判断的三层条件吗,它其中一个条件就是判断监听器的shouldBeActive
的值,只有为true
的时候数据才会被发送出来,所以当我们调用observerForever
这个方法的时候,只需要确保监听器活跃,监听器版本号小于当前版本号,就可以一直监听到数据
4.为什么有时候postValue会丢失数据
这种情况通常是发生在多线程情况下多次调用postValue
导致的,原因其实也很简单,我们可以看下postValue
里面的源码

这里有个暂存值mPendingData
,它可以存放具体值,也可以在没有值的时候设置成NOT_SET
状态,当第一次某个线程调用了postValue
去设置值,首先postTask
的值变成true
,mPendingData
变成具体想要设置的值,这个时候就会切换到主线程的任务mPostValueRunnable
里面,这个任务里面的代码如下

这个任务里面才是将暂存值设置到需要发送的newValue
变量里面去,然后mPendingData
的值才变成NOt_SET
,这个过程里面由于两边都加了同步锁,所以是同步进行的,在mPendingData
执行完成释放锁之前,postValue
内不会分发新的任务出来,所以才会造成多线程多次postValue
出现丢失数据的现象
总结
本来以为写不了多少,但还是啰里吧嗦写了一堆,不知道有没有人都看完的,以前我遇到这种问原理题的基本头都会很大,毕竟平时写业务都已经够忙的了,哪有时间去仔细的看这些源码,但是虽说看源码的过程是是辛苦的,但是一旦看明白了,其达到的效果不仅仅是帮助你在面试时候能够回答出面试官出的问题,同时也会帮助你在平时开发遇到问题后可以更快定位到问题,写出来的业务代码bug出现的概率也会相对降低,下篇见,拜拜~
转载自:https://juejin.cn/post/7394291501776470026