主流第三方库
中级面试常问 “X 库的原理”, 尤其网络层和图片库. 你做 SDK 偏底层, 这些上层库要补实战理解.
一, OkHttp
Android 网络底座 (Retrofit 默认基于它).
-
版本现状 (核验 2026-08): 4.12.0 是 4.x 最后一版; 5.0.0 已于 2025-07 发布稳定版, 当前 5.x 为主流稳定线. 4.x→5.x 主要变化: JVM/Android 分构件, 原生 Happy Eyeballs (RFC 8305), Kotlin API 更简洁, 支持 GraalVM; 升级需结合 targetSdk 与包体积评估.
-
核心: 拦截器链 (责任链模式). 请求依次经过拦截器:
- 应用拦截器 (开发者添加, 如加 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 支持和页面优先级做压测.
取消, 超时与重试
Dispatcher 管理异步 Call 的 ready queue 和 running queue. 请求未达到全局及单 host 并发限制时进入执行, 否则继续排队; 同步 execute() 由调用线程执行, 但仍会被 Dispatcher 统计. 调大并发只能减少排队, 不能消除服务端限流, 带宽竞争和线程调度成本.
Retrofit 的 suspend 调用被取消时, 取消应传播到底层 OkHttp Call.cancel(). 但取消只能终止客户端仍可控制的等待或 IO, 服务端可能已经收到并执行请求. 因此扣款, 下单和提交表单不能把 “客户端取消” 当作 “服务端回滚”.
| 超时 | 控制范围 | 常见误区 |
|---|---|---|
| connect timeout | 建立 TCP 或代理连接 | 不包含整个请求生命周期 |
| read timeout | 一次读取等待 | 不是响应总耗时上限 |
| write timeout | 一次写入等待 | 大文件上传还要考虑整体 deadline |
| call timeout | 从 Call 开始到结束的总体预算 | 包含 DNS, 排队, 重试, 请求和响应等阶段 |
重试前先判断三件事: 操作是否幂等, 请求体能否重新发送, 错误是否瞬时可恢复. GET 等安全读取通常更适合有限重试; POST 下单, 支付和上传必须使用幂等键或服务端去重. 重试要设置次数上限, 指数退避和抖动, 避免弱网时形成重试风暴.
缓存与网络切换
HTTP 缓存由 Cache-Control, ETag, Last-Modified 等协议语义决定, 解决可缓存响应的复用和重新验证. 业务缓存保存经过建模的数据, 还要处理登录身份, 数据版本, 离线编辑, 数据库事务和失效策略. OkHttp Cache 不能直接替代 Room 或 Repository 的离线数据源.
Wi-Fi 和蜂窝网络切换时, 旧连接可能仍在连接池中但已经不可达. 新请求通常会在连接失败后重新选路和建连; 长请求或长连接则要监听业务层失败并恢复. 不要在每次网络回调时无条件清空连接池和重放所有请求, 这会丢失复用收益并可能造成重复写入.
WebSocket 可靠性
- ping/pong 用于检测连接活性, 不是业务消息确认. 心跳间隔要结合代理空闲超时, 电量和服务端容量.
- 断线后使用带抖动的指数退避重连, 在网络恢复或 App 回到前台时允许适度加速, 但要设置上限并避免所有客户端同时重连.
- 重连成功不代表消息连续. 需要业务序列号, ack, 游标或会话恢复协议判断哪些消息已送达, 哪些需要补拉或重发.
- 待发送队列必须有容量, 过期和持久化策略. 对不可重复操作使用幂等 ID, 不能简单重放所有未确认帧.
- 页面退后台, 账号切换和进程重建时要明确连接所有者, 避免多个 WebSocket 并存或旧账号消息串入新会话.
二, Retrofit
把 HTTP API 封装成 Kotlin/Java 接口.
-
版本现状 (核验 2026-08): 2.x 最新为 2.12.0; 3.0.0 已于 2025-05 发布, 将 OkHttp 升到 4.12 (引入 Kotlin 传递依赖), 与 2.x 保持前向二进制兼容, 新项目以 3.x 为准; 具体以官方仓库当前版本为准.
-
核心: 动态代理 (Proxy.newProxyInstance).
create()为接口生成代理对象, 调用方法时被 InvocationHandler 拦截. -
解析方法上的注解 (@GET/@POST/@Query/@Body) 生成 ServiceMethod/RequestFactory, 构造 OkHttp 的 Request.
-
CallAdapter: 适配
Call, RxJava 或自定义返回包装. Retrofit 原生支持 Kotlinsuspend接口, 但不内置 Kotlin Flow call adapter; 返回 Flow 需要第三方 / 自定义 adapter, 或在 Repository 中用flow { emit(service.load()) }等方式建模并处理重复订阅语义. -
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”.
可操作排障片段: 拦截器, 代理与 Compose 图片
// 上下文片段:仅记录脱敏的诊断字段,不能读取或打印 Authorization/body.
class RequestIdInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("X-Request-Id", UUID.randomUUID().toString())
.build()
return chain.proceed(request)
}
}
interface UserApi {
@GET("users/{id}") suspend fun load(@Path("id") id: String): UserDto
}
// Retrofit.create(UserApi::class.java) 返回动态代理;真正请求在调用 load 时按注解构造.
// 上下文片段:需已引入 Coil Compose;model 和 size 共同影响缓存与解码成本.
// 160 x 160 为像素尺寸;若 UI 以 dp 约束,应由布局约束/密度换算得到实际 px.
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.size(160, 160) // px
.crossfade(false)
.build(),
contentDescription = null
)
列表 OOM 或图片排队时, 先取证而非盲目调大缓存: 确认 ImageView/Compose 目标实际尺寸, 请求数量, 解码尺寸, 内存/磁盘命中, 队列等待时间与取消次数. 常见修复是约束目标尺寸, 让可见项请求随绑定变化取消, 避免把原图或 Context 强引用放入业务缓存, 并按低优预取限额. 调大 Dispatcher 或内存缓存只能改变排队 / 淘汰, 可能恶化峰值内存和网络竞争, 须依据上述指标验证.
Glide 的统一模型是 Active Resources -> Memory Cache -> Resource Disk Cache -> Data Disk Cache -> 数据源获取; 数据源可以是网络, 文件或 ContentProvider, 网络不是 “第三级缓存”.
学完能做什么与练习
你应能按请求链和图片数据路径定位问题. 练习: 让列表复用同一 View 并分别请求同 URL 的两种像素尺寸; 预期证据是缓存 key / 解码尺寸不同, 不出现旧图回填, 且 OOM 排查记录包含尺寸, 命中和队列等待证据.
三, 图片库 (Glide / Coil)
- Glide 查找链: active resources → memory cache → resource disk cache → data disk cache → source. 网络只是可能的数据源之一, 不是缓存层; 具体内部类和默认策略需绑定 Glide 主版本说明.
- 生命周期绑定: 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
历史原理: EventBus 通过注册订阅者与事件类型建立发布订阅关系. 运行时可用反射发现带注解的订阅方法, 也可使用订阅者索引减少反射查找; ThreadMode 决定回调在发布线程, 主线程或后台线程等上下文执行.
当前状态与限制: 它能降低调用方对接收方的直接依赖, 但也让事件生产者, 消费者和时序分散在全局通道中. 未在合适生命周期取消注册会泄漏 Activity, Fragment 或 View; 错误的线程模式会使 UI 更新越过主线程边界. Event 名称和载荷缺少显式调用图, 也会增加追踪和测试成本. 新项目不应将它作为默认组件通信机制.
现代替代: 页面内状态优先由 ViewModel 持有 StateFlow/LiveData, 一次性事件可在明确所有者和收集生命周期下使用 SharedFlow 或 Channel. Fragment 间返回结果优先使用 Fragment Result API, 导航返回值可由 SavedStateHandle 建模. 跨模块通知应定义显式接口或领域事件契约, 并明确发布者, 消费者, 线程和生命周期.
ButterKnife
历史原理: ButterKnife 在 Java Android 项目中通过注解处理器扫描 @BindView 等注解, 于编译期生成绑定类, 用生成代码代替运行时反射查找. 绑定对象持有 View 引用, 因而 Fragment 的 View 生命周期通常需要在 onDestroyView 等时机调用 unbind().
当前状态与限制: ButterKnife 10.2.3 是最终发布版本, 项目已声明不再积极维护. Kotlin Android Extensions 的 synthetic views 在 Kotlin 1.4.20 标记废弃, 并在 Kotlin 1.8 移除; kotlin-parcelize 是独立插件, 不能把仍可使用 Parcelize 误解为 synthetic views 仍受支持. 两者都不适合作为新项目依赖; 继续使用可能带来构建兼容, 维护和 View 生命周期误用风险.
迁移选择:
-
Java/XML 或 Kotlin/XML 的普通 View 访问: 使用 View Binding; Fragment 在
onDestroyView清空可空 binding 引用. -
Java/XML 或 Kotlin/XML 确有 XML 表达式, Observable 字段或双向绑定需求: 评估 Data Binding; 不因简单视图访问引入其额外复杂度.
-
Compose 工具链: 直接由可组合函数和状态建模 UI, 不使用 View Binding, Data Binding 或 synthetic views; 仅在互操作的 XML View 边界按需保留相应绑定方案.
-
依赖注入:Hilt (主流, 见 Jetpack 架构组件的 Hilt 章节), Koin (纯 Kotlin, 运行时解析, 轻量但无编译期校验). DI 的核心价值不是 “少写 new”, 而是把对象创建, 作用域和替换点显式化, 方便测试和模块解耦.
-
协程之外的响应式: RxJava (老项目多, 操作符丰富但学习曲线陡), 新项目协程 + Flow 已基本替代.
| 模式/库 | 解决的问题 | 现代替代/边界 |
|---|---|---|
| EventBus | 跨组件发布订阅, 调用方不直接依赖接收方 | 反射 / 索引发现订阅者与线程模式是其历史机制; 容易隐藏数据流, 难追踪生命周期; 新项目优先显式状态流或通信契约. |
| 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, 经原生 suspend 支持或 CallAdapter 适配为 Call/RxJava/自定义类型, Converter 做序列化. Retrofit 不内置 Flow adapter.
Q4: Retrofit 怎么支持协程的? 方法加 suspend 后, Retrofit 内部把 OkHttp Call 的异步回调适配成协程挂起恢复, 常见实现会通过类似 suspendCancellableCoroutine 的方式处理取消, 直接返回结果或 Response, 无需手动 enqueue.
Q5: Glide 如何感知生命周期防止泄漏? Glide.with(activity/fragment) 返回与宿主生命周期绑定的 RequestManager, 宿主停止/销毁时可暂停或清理请求. 实现层面常见解释是注入无 UI 的 RequestManagerFragment/生命周期组件监听回调, 但这是库内部实现细节, 回答时不要说成永久 API 契约.
Q6: Glide 的缓存与数据源链路?
按 Active Resources -> Memory Cache -> Resource Disk Cache -> Data Disk Cache -> 数据源获取 理解; 数据源可以是网络, 文件或 ContentProvider, 不属于缓存层. 尺寸, 变换和请求选项会影响缓存 key.
Q7: Gson 在 Kotlin 里有什么坑? Gson 用反射, 不走构造函数, 可能绕过 Kotlin 的非空检查和默认值, 给非空字段赋 null 导致后续 NPE. 建议用 Moshi 或 kotlinx.serialization.
Q8: OkHttp 的四类超时有什么区别? connect, read 和 write timeout 分别约束建连, 单次读取等待和单次写入等待; call timeout 约束整个 Call 的总体时间, 会覆盖排队, DNS, 建连, 重试和收发等阶段. 具体值应按接口类型和弱网数据确定, 不能所有请求共用一个拍脑袋数字.
Q9: 协程取消后, 服务端操作一定取消吗? 不一定. Retrofit 可以把协程取消传播到 OkHttp Call, 从而终止客户端等待或 IO, 但请求可能已到达服务端. 写操作仍要依赖服务端幂等键, 状态查询或补偿机制确认最终结果.
Q10: HTTP 缓存和业务缓存有什么区别? HTTP 缓存按响应头和验证器复用原始响应; 业务缓存保存领域数据和同步状态, 负责身份隔离, 数据版本, 离线能力和冲突处理. 前者不能替代 Room/Repository, 两者可以分层配合.
Q11: WebSocket 重连后如何避免丢消息和重复消息? 心跳只判断连接是否活跃. 可靠恢复需要业务序列号或游标, ack, 补拉接口, 幂等消息 ID 和有上限的重发队列; 重连使用带抖动的指数退避, 并明确账号和进程生命周期下的单一连接所有者.