1. Retrofit 的工作原理?如何通过动态代理将接口注解转换为网络请求?
Retrofit 是基于 OkHttp 封装的 RESTful 网络请求框架,本身不做实际的网络请求,只负责创建动态代理、接口解析、参数封装、构建请求对象、解析响应数据,最终交给 OkHttp 发送请求。
原理:
- 先定义网路请求接口,用注解(
@GET、@POST、@Path、@Query等)标记请求方式、路径参数等; - 然后调用
retrofit.create(Servcie::class.java),通过动态代理(Proxy.newProxyInstance)的方式生成接口的实现类; - 调用接口方法时,代理拦截该方法,解析注解、参数、返回类型等(
InvocationHandler.invoke),封装成 ServiceMethod 对象;ServiceMethod 对象会根据实际传入的参数,生成OkHttp 的 Request 对象,由OkHttpCall执行网络请求; - 最后通过 Converter(如 Gson)将响应数据解析为方法声明的返回值类型;
记忆:动态代理拦方法,注解转 Request,OkHttp 发请求,Converter 解响应。
2. 为什么 Retrofit 接口方法不能有重载?
- 原因:Retrofit 通过方法名和注解来唯一确定一个网络请求。重载方法同名但参数不同,动态代理在解析时无法区分,也无法根据参数类型推断注解含义,会导致歧义和实现复杂。
- 替代:使用不同方法名,或将参数封装成对象。
记忆:重载同名难区分,注解解析易混淆,换不同名或对象。
3. Retrofit 是如何支持协程挂起函数的?
Continuation:是 Kotlin 协程里的“续体对象”,本质是一个回调接口,协程挂起时保存状态,恢复时从 Continuation 继续执行(唤醒协程 + 把结果/异常丢出去 + 让代码继续往下执行);
suspend函数如何变成 Continuation?- Kotlin 编译器遇到
suspend函数,会自动做CPS(Continuation Passing Style 续体传递风格)转换。给函数自动加一个Continuation 参数; - 函数不再直接返回结果,而是把结果交给
continuation.resumeWith();
- Kotlin 编译器遇到
Retrofit 依赖 Kotlin 编译器,通过 Continuation 把 OkHttp 异步回调结果交给
continuation.resumeWith();- 内部自动调度线程:IO 子线程请求,主线程恢复协程更新 UI;
4. 多个 BaseUrl 如何动态切换?
- 注解 @Url:接口方法不传相对路径,直接传完整全路径 URL,灵活切换;
- 多个 Retrofit 实例:不同 BaseUrl 单独创建 Retrofit + Service,隔离使用;
- 全局配置转换器:实现
BaseUrl替换工厂,统一规则动态适配域名; - 拦截器动态替换:自定义 OkHttp 拦截器,拦截请求,动态修改 Request 的 host 和 path;
5. 如何实现请求缓存(无网络时返回缓存数据)?
- 方案:结合 OkHttp 的缓存拦截器和网络拦截器,配合
CacheControl策略。 - 步骤:
- 创建 OkHttp
Cache对象,设置缓存目录和大小。 - 添加自定义拦截器:若无网络,强制使用缓存(
CacheControl.FORCE_CACHE);若有网络,根据缓存策略决定是否读缓存。 - 服务端需支持
Cache-Control响应头。
- 创建 OkHttp
- Retrofit:通过 OkHttp 的
Cache和拦截器实现,对业务层透明。
记忆:OkHttp 缓存 + 拦截器,无网强制缓存,有网按策略走。
6. OkHttp 的拦截器机制(Interceptor Chain)?责任链模式处理请求和响应?
- 机制:OkHttp 采用责任链模式,多个拦截器形成链式调用,通过
Chain.proceed把请求交给下一个拦截器,每个拦截器都可对请求进行处理; - 自定义拦截器:可添加应用拦截器(最早执行)或网络拦截器(在连接后执行,监控网络层数据传输);
- 常见内置拦截器:
- retryAndFollowUpInterceptor:失败和重定向拦截器。当请求内部抛出异常时,判定是否需要重试,当响应结果是3xx重定向时,构建新的请求并发送请求;
- BridgeInterceptor:应用层和网络层的桥接拦截器。主要工作是请求添加cookie,添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要解压;
- CacheInterceptor:缓存拦截器。如果命中缓存,则不发起网络请求;
- ConnectInterceptor:连接拦截器。内部会维持一个连接池,主要负责TCP连接,包括连接复用、创建连接(三次握手)、释放连接以及创建连接上的socket流;
- CallServerInterceptor:请求拦截器。在前置准备工作完成后,真正发起网络请求;
记忆:拦截器连成链,proceed 递进,各司其职;应用优先,网络后。
7. 谈谈网络请求中的拦截器(应用拦截器 vs 网络拦截器区别)
| 类型 | 特点 |
|---|---|
应用拦截器(addInterceptor) | 最先执行,只执行一次,不关心重定向、重试多次请求; |
网络拦截器(addNetworkInterceptor) | 每次重试、重定向都会重复执行,监控网络层数据传输、连接建立的过程 |
记忆:应用拦截器最早,一次;网络拦截器在连接后,可多次。
8. OkHttp 如何支持 HTTPS?
- 基本握手与验证
- 基于SSL/TLS协议建立加密连接(SSL Secure Sockets Layer 安全套接层,TLS Transport Layer Security 传输层安全);
- 默认信任系统内置的合法 CA 根证书,完成证书链校验;
- 进行域名校验,防止中间人攻击;
- 握手完成后使用对称加密传输数据,保证传输安全;
- 自定义证书配置(如自签名证书)
- 可自定义
SSLSocketFactory和HostnameVerifier,以信任本地自定义证书。 - 仅测试环境下可忽略证书校验(不推荐生产环境使用)。
- 可自定义
- 证书锁定(Certificate Pinning)
- 通过
CertificatePinner限制只信任特定公钥或证书,进一步防止中间人攻击。
- 通过
记忆:基于 SSL/TLS 握手加密,默认校验 CA 合法证书链与域名,支持自定义证书、双向认证,加密传输防窃听和中间人攻击。
9. 什么是 HTTP/2 的多路复用?OkHttp 如何利用它?
多路复用:
- 同一个 TCP 连接,可以同时跑多个 HTTP 请求,不用排队,不会阻塞;
- 请求按帧拆分,交错传输,互不阻塞,解决 HTTP1.1 队头阻塞;
OkHttp:OkHttp 默认支持 HTTP/2(如果服务器支持)。它会自动为同一主机使用一个连接进行多路复用,减少连接数,提升性能。
记忆:单连接并发行请求;OkHttp 自动升级 HTTP/2,复用连接。
10. 发生 SocketTimeoutException 时的重试机制?
超时原因:读写超时、网络波动、服务器响应慢;
OkHttp:自带默认重试机制,默认自动重试少量次数;
自定义重试:
- 自定义拦截器捕获 SocketTimeout 异常;
- 通过循环 + 延迟,手动发起重试;
- 设置最大重试次数、重试间隔;
只对幂等 GET 请求重试,POST 谨慎避免重复提交;
记忆:Socket 读写超时触发该异常,OkHttp 内置简单重试;可自定义拦截器捕获异常,限制最大次数和间隔,对幂等 GET 做自动重试,POST 慎用。
11. 如何自定义 DNS(Domain Name System 域名系统)?
- 实现 OkHttp 的
Dns接口,重写lookup()方法; - 自定义域名解析逻辑:本地静态映射、阿里 / 腾讯公共 DNS、IP 直连;
- 构建 OkHttpClient 时,通过
dns()注入自定义 DNS; - 作用:绕过系统 DNS、防 DNS 劫持、域名 IP 直连、智能调度切换 IP。
记忆:实现 OkHttp Dns 接口重写 lookup 方法,自定义域名解析规则,构建 OkHttp 注入,实现 IP 直连、防劫持、自定义公共 DNS。
12. 说说 ViewBinding 和 DataBinding 的区别?DataBinding 是如果实现数据和 UI 的双向绑定?
核心区别:
- ViewBinding:
- 只做视图绑定,自动生成布局对应绑定类;
- 直接引用 XML 控件 ID,无数据绑定、无 MVVM;
- 轻量、编译快、无注解、仅替代 findViewById;
- 不支持表达式、不支持双向绑定。
- DataBinding
- 兼顾视图绑定 + 数据绑定 + MVVM;
- 布局内嵌
<layout>、<data>绑定实体类; - 支持表达式、单向 / 双向绑定、事件绑定;
- 编译慢、复杂度高,适合正式 MVVM 项目;
DataBinding 双向绑定实现原理:
- 布局中使用
@={}语法声明双向绑定; - 编译器编译时会为每个布局生成绑定辅助类 XXLayoutBinding;
- 数据实体继承BaseObservable,字段变化时主动通知 UI 刷新;
- UI 控件编辑变化自动反向同步到数据模型;
- 底层借助监听器 + 反射 / 注解生成代码,实现 UI ↔ 数据 自动同步。
ViewBinding 可视为 DataBinding 的子集,如果只需视图绑定功能,建议优先使用 ViewBinding。
13. Coil 图片加载库的特点与协程实现
Coil 全称Coroutine Image Loader,是 Kotlin-first 的图片加载库,纯 Kotlin 编写,基于 Kotlin 协程构建。
Coil 核心特点(对比 Glide)
轻量级:API 更简洁优雅,适配 Kotlin 项目,包体积增量远小于 Glide(无多余依赖),但比 Glide 加载慢些;
挂起函数:异步加载通过挂起函数实现,每个图片请求通过
suspend fun execute()返回 ImageResult,无需回调。替代 Glide 的线程池 + Handler,更轻量;- 协程是 Kotlin 提供的轻量级并发模型,挂起函数是实现协程挂起与恢复的核心机制,二者配合实现非阻塞式异步编程。
生命周期感知:利用
CoroutineScope绑定 Activity/Fragment 销毁自动取消请求,避免内存泄漏和无效网络;缓存机制:与 Glide 一致的双层 LRU(内存缓存存 Bitmap 对象,磁盘缓存存原始数据),但 API 更简化;
Jetpack Compose 支持:声明式 API,原生适配 Compose 项目;
线程调度:通过
setDecodingDispatcher、setFetcherDispatcher分别控制解码和网络/磁盘 IO 的调度器,无需手动切换线程。流水线处理:缓存查询、网络下载、解码全部在协程中串行排队,线程开销低于传统线程池。
自动暂停/取消:滑动列表时自动暂停未显示的请求,节省网络和 CPU。
内存安全:自动采样策略、Bitmap 复用,降低 OOM 风险。
对比 Glide:若项目大量使用 Kotlin 协程和 OkHttp,Coil 更轻量、更顺手;Glide 加载速度略快,但 Coil 的协程集成和 Compose 支持更好。
记忆口诀:Coil 全称协程加载器,Kotlin-first 轻量级;双层 LRU 内存磁盘,协程 suspend 自动取消;采样降 OOM,Bitmap 复用同 Glide;项目用协程选 Coil,Compose 支持更无敌。
14. Glide 的完整加载流程与缓存机制
三级缓存:Glide 使用ActiveResources(活动资源弱引用)+ MemoryCache(内存缓存 LruCache)+ DiskCache(磁盘缓存 LruCache)。
- ActiveResources:存储当前正在使用的图片(弱引用),界面不展示后移到 MemoryCache。读取优先级最高,活动缓存解决 Lru 缓存时存在的弊端问题。
- MemoryCache:存储最近释放的图片(LruCache),图片离开界面但还在内存缓存中,下次加载直接复用。
- DiskCache:分为Resource Cache(形变后缓存,如缩放宽高)和Data Cache(源数据原始缓存),以文件形式存储在应用的私有目录下,采用 Lru 算法。
LRU 全称:最近最少使用算法,优先淘汰长时间未使用的数据
- 底层:基于 LinkedHashMap,开启访问有序模式。
- 核心机制:
- 每次 get/put 访问元素,会将当前元素移至队列头部;
- 长期不访问的元素会逐步挤到队列尾部;
- 缓存达到设定阈值时,自动移除尾部最少使用元素;
加载流程(Engine.load 中的缓存读取顺序):内存 → 磁盘 → 网络
- 从 ActiveResources 取:弱引用存储正在使用的图片,命中直接返回。
- 从 MemoryCache 取:LruCache,命中后移动到 ActiveResources。
- 从 DiskCache 取:先查 Resource Cache(已处理图),再查 Data Cache(原始数据)。
- 从网络/源头加载:以上都没有,开启 EngineJob 进行网络下载,解码后存入各级缓存[reference:6]。
缓存 Key 生成机制:根据图片 URL、宽高、变换参数等信息唯一标识,确保不同参数的同一图片单独缓存。
记忆口诀:活动引用正在用,Lru 内存暂存放,两级磁盘分形变与原始,BitmapPool 复用降 GC,缓存读取顺序:活动→内存→磁盘→网络。
15. EventBus 的原理、注解反射与线程调度
EventBus 通过发布/订阅事件总线实现组件间通信,广泛用于 Android 模块解耦。
订阅、发布实现:
- 订阅:
@Subscribe注解标记订阅方法,方法必须有且仅有一个参数(事件类型)。EventBus 3.x 优先通过APT(Annotation Processing Tool 注解处理器)编译生成订阅者索引,避免反射;未配置或无法索引时回退到反射生成;@Subscribe标记订阅方法,指定线程模式、是否粘性、优先级;
- 发布事件:调用
post()发送事件,根据事件类型从映射表找到所有订阅该事件的方法; - 遍历回调所有订阅方法,完成事件分发;
事件发布与线程调度:
post发布事件:根据事件类型在subscriptionsByEventType中找到所有订阅者,遍历调用postToSubscription。- 线程调度:
@Subscribe(threadMode = ...)支持 4 种模式:POSTING:当前线程(发布线程)直接回调;MAIN:切到主线程执行(通过MainHandler发送消息)。BACKGROUND:切到后台线程(单线程池)。ASYNC:异步线程池独立执行。
- 粘性事件:发送后缓存事件,后续注册的订阅者能收到之前已发送的粘性事件。
postSticky先缓存到stickyEventsMap,后续注册粘性订阅者时立即收到。
记忆口诀:EventBus 发布/订阅模式,@Subscribe 标记订阅。APT 索引编译时生成,反射提速性能优。四种 ThreadMode 调度灵活,post 事件遍历分发,Sticky 存 Map 后注册也能接。
16. LeakCanary 的工作原理与内存泄漏检测
LeakCanary 内存泄漏检测通过弱引用 + 引用队列 + 堆快照分析实现,自动检测并报告内存泄漏。
详细工作原理:
- 监听组件生命周期:自动监听Activity、Fragment、View、ViewModel销毁时机(
Application.registerActivityLifecycleCallbacks()); - 标记待回收对象:页面销毁后,将本该被 GC 回收的对象加入弱引用队列,关联唯一 Key;
- 主动触发 GC:延迟一定时间后,手动触发 GC,等待虚拟机回收无用对象;
- 判断是否泄漏:检查弱引用队列,对象未被回收,说明存在强引用持有,判定内存泄漏;
- 堆快照分析:生成 hprof 堆文件,解析引用链,找出是谁持有导致对象无法回收;
- 展示泄漏:在桌面弹窗展示泄漏位置、引用链路,方便定位修复;
检测原理:当 JVM 进行垃圾回收时,如果对象只有弱引用存在,该对象会被回收,弱引用进入引用队列;反之若引用队列中没有该弱引用,说明对象仍有其他强引用持有,判定为内存泄漏。
常见配置:DebuggerControl(调试模式禁用检测)、watchDurationMillis(5s 等待 GC 窗口)、ReferenceMatcher用于忽略系统已知的泄漏(如某些 Android 版本 View 的 mContext 泄露)。
记忆口诀:弱引用绑定观察对象,GC 后查引用队列判泄漏。堆转储分析引用链,报告分类应用或库。ContentProvider 自动初始化,Activity 泄漏自动检测。