主流第三方库
中级面试常问“X 库的原理“,尤其网络层和图片库。你做 SDK 偏底层,这些上层库要补实战理解。
一、OkHttp
Android 网络底座(Retrofit 默认基于它)。
- 核心:拦截器链(责任链模式)。请求依次经过拦截器:
- 应用拦截器(开发者添加,如加 header、log)。
- RetryAndFollowUp(重试、重定向)。
- Bridge(补全 header、gzip)。
- Cache(缓存)。
- Connect(建立连接)。
- 网络拦截器(开发者添加)。
- CallServer(真正发送收发)。
- 连接池(ConnectionPool):复用 TCP/HTTP 连接,常见默认配置是最多空闲连接约 5 个、保活约 5 分钟,但这是 OkHttp 版本/构造参数相关的可配置默认值。复用可减少 TCP/TLS 握手;HTTP/2 下同一连接还能多路复用多个 stream。
- 缓存:基于 HTTP 缓存语义(Cache-Control、ETag),磁盘 LRU。
- 分发器(Dispatcher):异步请求用线程池执行,控制最大并发和单 host 并发;常见默认值是
maxRequests=64、maxRequestsPerHost=5,可通过属性调整。WebSocket 等特殊连接是否计入限制要看版本文档。
OkHttp 请求链路与调优
| 机制 | 解决什么问题 | 面试追问 |
|---|---|---|
| Dispatcher | 控制异步 Call 何时进入执行队列,避免无限并发打爆线程/服务端 | 并发不是越大越好,要结合服务端限流、HTTP/2、业务优先级调整。 |
| ConnectionPool | 复用连接,减少 TCP/TLS 握手和慢启动 | 空闲连接会占 fd/内存;移动端网络切换时要能重建连接。 |
| HTTP/2 multiplexing | 一个 TCP 连接上并发多个 stream,减少同域名多连接开销 | 仍受服务端支持、流控、丢包影响;TCP 层队头阻塞没有完全消失。 |
| Cache | 利用 HTTP 语义减少网络请求 | 只对可缓存响应生效;动态接口要和服务端协商 Cache-Control/ETag。 |
val client = OkHttpClient.Builder()
.dispatcher(Dispatcher().apply {
maxRequests = 32
maxRequestsPerHost = 8
})
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
.build()
上面数字不是“最佳实践模板”,只是说明默认项可配置。真实项目要根据接口耗时、弱网、服务端限流、HTTP/2 支持和页面优先级做压测。
二、Retrofit
把 HTTP API 封装成 Kotlin/Java 接口。
- 核心:动态代理(Proxy.newProxyInstance)。
create()为接口生成代理对象,调用方法时被 InvocationHandler 拦截。 - 解析方法上的注解(@GET/@POST/@Query/@Body)生成 ServiceMethod/RequestFactory,构造 OkHttp 的 Request。
- CallAdapter:适配返回类型(Call、协程 suspend、Flow、RxJava)。协程版可理解为把 OkHttp Call 的异步回调桥接为挂起恢复,取消协程时也要取消底层 Call。
- Converter:序列化转换(Gson/Moshi/kotlinx.serialization),负责请求体/响应体和业务对象之间的转换。
- 协程用法:接口方法加
suspend直接返回data class,无需 Call.enqueue。
Retrofit 三层扩展点
| 扩展点 | 负责内容 | 常见坑 |
|---|---|---|
| 注解解析 | URL、HTTP method、Query/Header/Body 参数 | 动态 URL、编码、空参数要按接口契约处理。 |
| Converter | JSON/XML/Proto 与对象互转 | Kotlin 非空/默认值、混淆字段名、泛型类型擦除。 |
| CallAdapter | 把 Call<T> 适配成 suspend/RxJava/Result 包装 | 错误处理边界要统一:HTTP error、网络异常、解析异常不要混在一起。 |
面试回答结构:先说“接口不是实现类,Retrofit 用动态代理拦截方法调用”;再说“注解生成请求,Converter 负责数据转换,CallAdapter 负责返回类型”;最后补“底层网络仍交给 OkHttp”。
三、图片库(Glide / Coil)
- 三级缓存:内存(ActiveResources 弱引用 + LruCache)→ 磁盘(DiskLruCache)→ 网络。
- 生命周期绑定:Glide 官方 API 强调传入 Activity/Fragment 后会随宿主销毁自动清理请求。实现层面常见解释是注入无 UI 的
RequestManagerFragment/等价生命周期组件监听宿主生命周期;这是库内部实现,版本可能演进,面试要说“实现层面”。Coil 基于协程 + Lifecycle,更贴近 Kotlin 项目。 - Bitmap 复用(BitmapPool):复用可复用的 Bitmap 内存,减少频繁分配和 GC;硬件位图、尺寸/配置不匹配时不一定能复用。
- Coil:Kotlin 优先、协程实现、API 轻量,Compose 场景友好;Glide 生态成熟、缓存/变换/集成丰富,老项目更多。
- 优化点:指定尺寸(override)避免加载过大图、合适的缓存策略、占位图。
图片加载机制拆解
| 机制 | Glide 侧重点 | Coil 对照 | 注意点 |
|---|---|---|---|
| 生命周期 | Glide.with(activity/fragment) 绑定宿主,停止/销毁时暂停或清理请求 | ImageLoader + coroutine/lifecycle,Compose 集成自然 | 不要在长生命周期 context 上加载短生命周期 View。 |
| 缓存 key | 通常由 model、尺寸、变换、选项、签名等共同决定 | 同样需要区分 data/size/transformation | “同 URL”不代表同一缓存条目,尺寸和变换会影响命中。 |
| 内存缓存 | ActiveResources + MemoryCache | memoryCache | 内存命中快,但占用大;列表页要控制目标尺寸。 |
| 磁盘缓存 | 原始数据/转换后资源策略可配 | diskCache | 磁盘命中省网络但有 IO;策略按图片来源选择。 |
| BitmapPool | 复用 Bitmap 内存降低 GC | bitmap pool/解码组件实现不同 | 高版本硬件位图不可随意复用/修改。 |
RecyclerView 中的核心不是“手动取消所有请求”,而是每次绑定都给可复用 View 发起新请求或显式 clear。Glide 文档也提示,如果某个复用 View 不再加载图片,要 clear() 避免旧请求回调把旧图设置回来。
四、序列化库
- Gson:反射解析,易用但运行时反射有性能开销、不感知 Kotlin 默认值/空安全(可能给非空字段塞 null)。
- Moshi:Square 出品,支持 codegen(编译期生成 adapter,无反射),对 Kotlin 友好。
- kotlinx.serialization:Kotlin 官方,编译期插件生成,类型安全、多平台、无反射,新项目推荐。
@Serializable注解。
| 库 | 机制 | 适合场景 | 面试注意 |
|---|---|---|---|
| Gson | 运行时反射为主 | 老项目、简单 Java model | Kotlin 非空/默认值坑多,混淆要保留字段/注解。 |
| Moshi | 反射或 codegen adapter | Kotlin + Square 生态 | codegen 性能和类型安全更好,但要配置 KSP/KAPT。 |
| kotlinx.serialization | 编译期插件生成 serializer | Kotlin/多平台/Compose 新项目 | 需要 @Serializable,与 Retrofit 配合要加 converter。 |
五、其他常见库
- EventBus:发布订阅解耦组件通信。现代趋势用 Flow/SharedFlow 替代(类型安全、可控生命周期)。
- 依赖注入:Hilt(主流,见 07)、Koin(纯 Kotlin、运行时解析、轻量但无编译期校验)。DI 的核心价值不是“少写 new”,而是把对象创建、作用域和替换点显式化,方便测试和模块解耦。
- 协程之外的响应式:RxJava(老项目多,操作符丰富但学习曲线陡),新项目协程 + Flow 已基本替代。
| 模式/库 | 解决的问题 | 现代替代/边界 |
|---|---|---|
| EventBus | 跨组件发布订阅,调用方不直接依赖接收方 | 容易隐藏数据流、难追踪生命周期;页面内优先 LiveData/StateFlow/SharedFlow,跨模块事件要定义清晰契约。 |
| Hilt | 编译期 DI、作用域管理、Android 组件注入 | 适合中大型项目;需要理解 Component/Scope,不要滥用单例。 |
| Koin | DSL 声明依赖,上手快 | 运行时解析,错误可能到运行时才暴露;适合小项目或快速迭代。 |
| RxJava | 复杂异步流组合 | 老项目维护常见;新代码可优先协程 Flow,但迁移要逐步做。 |
高频面试题
Q1:OkHttp 的拦截器链是什么设计模式?顺序? 责任链模式。顺序:应用拦截器 → RetryAndFollowUp → Bridge → Cache → Connect → 网络拦截器 → CallServer。每个拦截器处理后调 chain.proceed 传递。
Q2:应用拦截器和网络拦截器区别? 应用拦截器在最外层,只调用一次,能看到原始请求(重定向前),不一定有网络;网络拦截器在 Connect 之后,可能多次调用(重定向/重试),能看到真实网络请求和中间响应。
Q3:Retrofit 的核心原理? 动态代理。create() 用 Proxy 为接口生成代理对象,调用方法时 InvocationHandler 拦截,解析注解构造 OkHttp Request,经 CallAdapter 适配返回类型(协程/Call/Flow),Converter 做序列化。
Q4:Retrofit 怎么支持协程的? 方法加 suspend 后,Retrofit 内部把 OkHttp Call 的异步回调适配成协程挂起恢复,常见实现会通过类似 suspendCancellableCoroutine 的方式处理取消,直接返回结果或 Response,无需手动 enqueue。
Q5:Glide 如何感知生命周期防止泄漏? Glide.with(activity/fragment) 返回与宿主生命周期绑定的 RequestManager,宿主停止/销毁时可暂停或清理请求。实现层面常见解释是注入无 UI 的 RequestManagerFragment/生命周期组件监听回调,但这是库内部实现细节,回答时不要说成永久 API 契约。
Q6:Glide 三级缓存? 内存(活动资源弱引用 + LruCache)、磁盘(DiskLruCache,可缓存原图或转换后)、网络。命中内存最快,逐级回退。
Q7:Gson 在 Kotlin 里有什么坑? Gson 用反射、不走构造函数,可能绕过 Kotlin 的非空检查和默认值,给非空字段赋 null 导致后续 NPE。建议用 Moshi 或 kotlinx.serialization。