Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

主流第三方库

中级面试常问 “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 与包体积评估.

  • 核心: 拦截器链 (责任链模式). 请求依次经过拦截器:

    1. 应用拦截器 (开发者添加, 如加 header, log).
    2. RetryAndFollowUp (重试, 重定向).
    3. Bridge (补全 header, gzip).
    4. Cache (缓存).
    5. Connect (建立连接).
    6. 网络拦截器 (开发者添加).
    7. 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 原生支持 Kotlin suspend 接口, 但不内置 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, 编码, 空参数要按接口契约处理.
ConverterJSON/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 + MemoryCachememoryCache内存命中快, 但占用大; 列表页要控制目标尺寸.
磁盘缓存原始数据 / 转换后资源策略可配diskCache磁盘命中省网络但有 IO; 策略按图片来源选择.
BitmapPool复用 Bitmap 内存降低 GCbitmap pool/解码组件实现不同高版本硬件位图不可随意复用/修改.

RecyclerView 中的核心不是 “手动取消所有请求”, 而是每次绑定都给可复用 View 发起新请求或显式 clear. Glide 文档也提示, 如果某个复用 View 不再加载图片, 要 clear() 避免旧请求回调把旧图设置回来.

四, 序列化库

  • Gson: 反射解析, 易用但运行时反射有性能开销, 不感知 Kotlin 默认值 / 空安全 (可能给非空字段塞 null).
  • Moshi: Square 出品, 支持 codegen (编译期生成 adapter, 无反射), 对 Kotlin 友好.
  • kotlinx.serialization: Kotlin 官方, 编译期插件生成, 类型安全, 多平台, 无反射, 新项目推荐. @Serializable 注解.
库机制适合场景面试注意
Gson运行时反射为主老项目, 简单 Java modelKotlin 非空/默认值坑多, 混淆要保留字段/注解.
Moshi反射或 codegen adapterKotlin + Square 生态codegen 性能和类型安全更好, 但要配置 KSP/KAPT.
kotlinx.serialization编译期插件生成 serializerKotlin/多平台/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, 不要滥用单例.
KoinDSL 声明依赖, 上手快运行时解析, 错误可能到运行时才暴露; 适合小项目或快速迭代.
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 和有上限的重发队列; 重连使用带抖动的指数退避, 并明确账号和进程生命周期下的单一连接所有者.