背景
最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个系统的梳理,整理成一个系列:Android应用开发岗 面试汇总。本系列将分为以下几个大模块:
Java基础篇、Java进阶篇、常见设计模式
Android基础篇、Android进阶篇、性能优化
网络相关、数据结构与算法
常用开源库、Kotlin、Jetpack
注1:以上文章将陆续更新,直到我找到满意的工作为止,有跳转链接的表示已发表的文章。
注2:该系列属于个人的总结和网上东拼西凑的结果,每个知识点的内容并不一定完整,有不正确的地方欢迎批评指正。
注3:部分摘抄较多的段落或有注明出处。如有侵权,请联系本人进行删除。
在日常开发中,我们能够熟练的使用各种第三方库,且第三方库的源码和对源码的分析,网上已经有很多。这里不做过多分析,本篇内容主要概述第三方库的实现流程和原理,旨在通过概述的方式,让读者复习第三方库,以便在面试过程中有更好的表现。
1 OkHttp
1.1 Okhttp 基本实现原理
OkHttp 主要是通过 5 个拦截器和 3 个双端队列(2 个异步队列,1 个同步队列)工作。内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,实现了各层的解耦。
OkHttp 的底层是通过 Socket 发送 HTTP 请求与接受响应,但是 OkHttp 实现了连接池的概念,即对于同一主机的多个请求,可以公用一个 Socket 连接,而不是每次发送完 HTTP 请求就关闭底层的 Socket,这样就实现了连接池的概念。而 OkHttp 对 Socket 的读写操作使用的 OkIo 库进行了一层封装。
执行流程:
- 1.通过构建者构建出OkHttpClient对象,再通过newCall方法获得RealCall请求对象.
- 2.通过RealCall发起同步或异步请求,而决定是异步还是同步请求的是由线程分发器dispatcher来决定.
- 3.当发起同步请求时会将请求加入到同步队列中依次执行,所以会阻塞UI线程,需要开启子线程执行.
- 4.当发起异步请求时会创建一个线程池,并且判断请求队列是否大于最大请求队列64个,请求主机数是否大于5个,如果大于请求添加到异步等待队列中,否则添加到异步执行队列**,并执行任务.
内部的大致请求流程图如下所示:
1.2 Okhttp 网络缓存如何实现?
OKHttp 默认只支持 get 请求的缓存。
- 第一次拿到响应后根据头信息决定是否缓存。
- 下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
- 如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
1.3 Okhttp 网络连接怎么实现复用?
HttpEngine 在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,如果无法获取,就会调用createnextConnection()来新建一个Connection对象,这就是 Okhttp 多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象。
1.4 Dispatcher 的功能是什么?
Dispatcher中文是分发器的意思,和拦截器不同的是分发器不做事件处理,只做事件流向。他负责将每一次Requst进行分发,压栈到自己的线程池,并通过调用者自己不同的方式进行异步和同步处理。 通俗的讲就是主要维护任务队列的作用。
- 记录同步任务、异步任务及等待执行的异步任务。
- 调度线程池管理异步任务。
- 发起/取消网络请求 API:execute、enqueue、cancel。
Dispatcher 类,该类中维护了三个双端队列(Deque):
- readyAsyncCalls:准备运行的异步请求
- runningAsyncCalls:正在运行的异步请求
- runningSyncCalls:正在运行的同步请求
OkHttp 设置了默认的最大并发请求量 maxRequests = 64 和单个 Host 主机支持的最大并发量 maxRequestsPerHost = 5
1.5 addInterceptor 与 addNetworkInterceptor 的区别?
二者通常的叫法为应用拦截器和网络拦截器,从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。
1.首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
2.其次,如上文提到除了CallServerInterceptor,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
3.最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。
1.6 Okhttp 拦截器的作用是什么?
1、应用拦截器
拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
- RetryAndFollowUpInterceptor 处理错误重试和重定向
- BridgeInterceptor 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
- CacheInterceptor 缓存拦截器,如果命中缓存则不会发起网络请求。
- ConnectInterceptor 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
2、网络拦截器
用户自定义拦截器,通常用于监控网络层的数据传输。
- CallServerInterceptor 请求拦截器,在前置准备工作完成后,真正发起了网络请求。
1.7 Okhttp 有哪些优势?
- 支持 http2,对一台机器的所有请求共享同一个 Socket
- 内置连接池,支持连接复用,减少延迟
- 支持透明的 gzip 压缩响应体
- 响应缓存可以完全避免网络重复请求
- 请求失败时自动重试主机的其他 ip,自动重定向
- 丰富的 API,可扩展性好
1.8 Okhttp 运用了哪些设计模式?
Okhttp 运用了六种设计模式:
- 构造者模式(OkhttpClient,Request 等各种对象的创建)
- 工厂模式(在 Call 接口中,有一个内部工厂 Factory 接口。)
- 单例模式(Platform 类,已经使用 Okhttp 时使用单例)
- 策略模式(在 CacheInterceptor 中,在响应数据的选择中使用了策略模式,选择缓存数据还是选择网络访问。)
- 责任链模式(拦截器的链式调用)
- 享元模式(Dispatcher 的线程池中,不限量的线程池实现了对象复用)
链接
2 Glide
2.1 基本使用
通过Glide类进行一个链式调用
Glide.with(getApplicationContext()).load(imageurl).into(imageview);
with()
在使用过程中尽量要传入Applicaiton、Activity 、Fragment等类型的参数,因为glide加载图片的请求会与该参数的生命周期绑定在一起,如果onPaush时候,Glide就会暂停加载,重新onResume之后,又会继续加载。
load()
支持网络图片网址、二进制流、drawable资源、本地图片的传入。
crossFade
这是开启显示淡入淡出的动画
override
如果获取的网络图片过大,我们通过它进行一个大小的裁剪,传入width和height参数进行宽高裁剪。
diskCacheStrategy
磁盘缓存的设置,默认Glide会开启的。
DiskCacheStrategy.NONE 什么都不缓存
DiskCacheStrategy.SOURCE 只缓存全尺寸图
DiskCacheStrategy.RESULT 只缓存最终的加载图
DiskCacheStrategy.ALL 缓存所有版本图(默认行为)
Glide 不仅缓存了全尺寸的图,还会根据 ImageView 大小所生成的图也会缓存起来。比如,请求一个 800x600 的图加载到一个 400x300 的 ImageView 中,Glide默认会将这原图还有加载到 ImageView 中的 400x300 的图也会缓存起来。
error
这里的设置是当加载图片出现错误时,显示的图片。
placeholder
图片加载完成之前显示的占位图。
into()
一般传 ImageView 。
2.2 Glide原理
with是Glide类的一个静态方法,重载方法很多可以接收 Activity,Fragment,Context。
with方法里面,首先会调用RequestManagerRetriever的静态get方法得到RequestManagerRetriver对象。然后再调用该对象的get方法获取RequestManager对象。静态get方法中也有很多重载方法,主要分为传入Application参数和非Application参数,传入Application参数是最简单的情况,Glide只要持保和整个应用生命周期同步。
非Application参数不管是Activity,Fragment,最终都会向当前Activity传入一个隐藏的Fragment,因为Glide需要监控Activity的生命周期,Fragment依赖Activity生命周期并且是同步的,通过这个隐藏的Fragment就监听到Activity生命周期。
load方法,with方法返回的是一个RequestManager对象,所以load方法在RequestManager类中,load方法也有很多重载,支持本地图片,内存图片,网络图片,只看加载url的load方法。首先调用了fromString方法,再调用load方法,传入图片url,fromString方法里调用了loadGeneric方法,这个方法创建并返回了DrawableTypeRequest对象。
DrawableTypeRequest并没有load方法,load在DrawableTypeRequest的父类DrawableTypeRequestBuildle中。大部分操作都在这个类中,比如placeholder占位符,error,discacheStrategy等。
into方法是Glide图片加载流程中逻辑最为复杂的方法。
into方法在DrawableTypeRequestBuilder类中,里面调用了super.into方法,真正的实现在DrawableTypeRequestBuilder的父类GenericRequestBuilder中,这个类包括了网络请求,图片解析,图片解码,bitmap生成,缓存处理,图片压缩等大量逻辑操作,最后的最后才将图片展示出来。
总结:
Glide在加载绑定了Activity的生命周期。
- 在Activity内新建一个无UI的Fragment,这个特殊的Fragment持有一个Lifecycle。通过Lifecycle在Fragment关键生命周期通知RequestManger进行相关的操作。
- 在生命周期onStart时继续加载,onStop时暂停加载,onDestory是停止加载任务和清除操作。
2.3 Glide三级缓存
2.3.1 普通的三级缓存
- 内存缓存:优先加载,速度最快
- 本地缓存:次优先加载,速度快
- 网络缓存:最后加载,速度慢,浪费流量
2.3.2 Glide的三级缓存
- 内存缓存-弱引用缓存:弱引用缓存使用 WeakReference 修饰引用的图片,用于缓存正在使用中的图片
- 内存缓存:LruCache 就是常见的内存缓存,保存当前应用使用过但不是正在使用中的图片。
- 磁盘缓存:整个系统,只要不删除数据,就一直存在
2.3.3 缓存读取
Glide 获取一张图片时,首先会从弱引用缓存中获取,没有则从内存缓存 LruCache 中获取,再没有则从磁盘缓存中获取,再没有才通过网络获取。拿到图片后,通过 Handler 发送消息给主线程。将图片展示并缓存起来。
2.3.4
DiskLruCache 缓存原图 -> 弱引用缓存 -> LruCache -> DiskLruCache 缓存编码后的图片
注:弱引用缓存和 LruCache 之间存在缓存的转换关系,图片从正在使用状态转为不使用状态,Glide 将图片从弱引用缓存移除然后缓存到 LruCache 中,假如 LruCache 中的某张图片现在需要使用,则图片从 LruCache 中移除缓存到弱引用缓存中,弱引用缓存中保存的是正在使用的图片。
链接
问:为什么Glide磁盘缓存效率高?
答:磁盘缓存对图片文件进行了加密和压缩处理
意义:
1、三级缓存策略,最实在的意义就是减少不必要的流量消耗,增加加载速度。
2、从开发角度来说,Bitmap 的创建非常消耗时间和内存,可能导致频繁GC。而使用缓存策略,会更加高效地加载 Bitmap,减少卡顿,从而减少读取时间。
2.4 Glide优点
- 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
- 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
- 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
- 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)
链接
3 EventBus
EventBus是一个基于观察者模式的事件订阅/发布框架,利用 EventBus 可以在不同模块之间,实现低耦合的消息通信。
3.1 使用
使用 EventBus 注册消息的时候,可以通过 @Subscribe 注解来完成注册事件, @Subscribe 中可以通过参数 threadMode 来指定使用那个线程来接收消息。
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventTest(event:TestEvent){
// 处理事件
}
3.2 参数
threadMode 是一个 enum,有多种模式可供选择:
- POSTING,默认值,那个线程发就是那个线程收。
- MAIN,切换至主线程接收事件。
- MAIN_ORDERED,v3.1.1 中新增的属性,也是切换至主线程接收事件,但是和 MAIN 有些许区别,后面详细讲。
- BACKGROUND,确保在子线程中接收事件。细节就是,如果是主线程发送的消息,会切换到子线程接收,而如果事件本身就是由子线程发出,会直接使用发送事件消息的线程处理消息。
- ASYNC,确保在子线程中接收事件,但是和 BACKGROUND 的区别在于,它不会区分发送线程是否是子线程,而是每次都在不同的线程中接收事件。
3.3 线程切换源码
EventBus 的线程切换,主要涉及的方法就是 EventBus 的 postToSubscription() 方法。
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
从上面的代码可以看出,切换线程是通过MainThreadPoster、BackgroundPoster。BackgroundPoster通过线程池去执行事件。
3.4 总结
- EventBus是基于观察者模式的时间订阅/发布框架
- 通过 @Subscribe 注解来完成注册事件
- 通过EventBus.getDefault().post(new AAA());来发送事件
- 通过注解,来执行线程切换的方法postToSubscription()
链接
4 LeakCanary
利用弱引用特性,检测Activity 的内存泄漏。核心步骤如下:
- LeakCanary.install(application);此时使用application进行registerActivityLifecycleCallbacks,从而来监听Activity的何时被destroy。
- 在onActivityDestroyed(Activity activity)的回调中, 使用一个弱引用WeakReference指向这个activity,并且给这个弱引用指定一个引用队列queue,同时创建一个key来标识该activity。
- 然后将检测的方法ensureGone()投递到空闲消息队列。
- 当空闲消息执行的时候,去检测queue里面是否存在刚刚的弱引用,如果存在,则说明此activity已经被回收,就移除对应的key,没有内存泄漏发生。
- 如果queue里不存在刚刚的弱引用,则手动进行一次gc。
- gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,否则没有发生内存泄漏,流程结束。
- 之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析
- 最后通过DisplayLeakService进行内存泄漏的展示。
一句话总结:LeakCanary.install(application)在Application中通过registerActivityLifecycleCallbacks进行注册监听。当Activity onDestroy时,通过一个弱引用指向该Activity,把弱引用放入引用队列中。当发生GC后,判断队列中的是否存在该弱引用,如果存在,则说明发生了泄露,则去分析泄露原因,最后通知应用发生了泄露。
链接
5 ButterKnife
ButterKnife又名黄油刀,是一款知名的Andorid框架,通过注解绑定,省去初始化控件等重复工作,简化代码,极大提高工作效率。使用如下:
@BindView(R.id.tv)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.tv)
public void click(View view) {
Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
}
ButterKnife为什么执行效率为什么比其他注入框架高?
答:由于是在编译期生成的代码,并不是通过反射实现,所以性能优势是非常高的。
6 Retrofit
Retrofit 通过 Java 接口以及注解来描述网络请求,并用动态代理的方式生成网络请求的 request,然后通过 client 调用相应的网络框架(默认 okhttp)去发起网络请求,并将返回的 response 通过 converterFactorty 转换成相应的数据 model,最后通过 calladapter 转换成其他数据方式(如 rxjava Observable)
6.1 Retrofit 流程
- 通过动态代理生成网络请求对象,并且在InvocationHandler中统一处理请求方法
TranslateApi translateApi = retrofit.create(TranslateApi.class);
- 通过解析网络请求接口的注解,配置网络请求参数,封装成OkHttpCall
- 通过网络请求适配器将网络请求对象进行平台适配:CallAdpaterFactory
- 通过网络请求执行器发送网络请求
- 通过数据转换器解析服务器返回的数据,即ConverterFactory来处理数据格式的转换
- 通过回调执行器切换线程(子线程 ->>主线程)
- 用户在主线程处理返回结果
6.2 用到的设计模式和设计原则
设计模式
- 建造者模式:构造Retrofit对象时
- 动态代理:生成网络请求对象时
- 适配器模式:适配OkHttpCall时使用了Adapter模式
- 工厂方法模式:生成CallAdpater和Converter时
设计原则
- 单一职责:通过动态代理来处理接口,通过OkHttp来处理网络请求,通过CallAdapterFactory来适配OkHttpCall,通过ConverterFactory来处理数据格式的转换,符合单一职责原则
- 依赖倒置:Retrofit对CallAdpaterFactory和ConverterFactory的依赖都是依赖其接口的,这就让我们可以非常方便的扩展自己的CallAdpaterFactory和ConverterFactory,这符合依赖倒置原则
- 迪米特原则(最少知识原则):不管Retrofit内部的实现如何复杂,比如动态代理的实现、针对注解的处理以及寻找合适的适配器等,Retrofit对开发者隐藏了这些实现细节,只提供了简单的Api给开发者调用,开发者只需要关注通过的Api即可实现网络请求,这种对外隐藏具体的实现细节的思想符合迪米特原则。
链接