一个集成 IM 即时通讯 与 直播观看 的 Android 原生 App,聚焦高级岗位关注的工程与架构深度:消息可靠性、长连接生命周期管理、高帧率弹幕渲染、Compose + 多模块 Clean Architecture、Jetpack 全家桶。
项目定位:本仓库的目的是在代码层面向面试官完整展示我的能力——而不是做一款能跑的产品。代码中所有关键抽象都配有注释,解释"为什么这样写"而不只是"做了什么"。
| 维度 | 选型 |
|---|---|
| 语言 | Kotlin 2.0.20(Compose 编译器 Gradle 插件) |
| UI | 100% Jetpack Compose + Material 3 |
| 架构 | Clean Architecture(data / domain / ui 分层)+ MVI + StateFlow |
| 异步 | Coroutines + Flow |
| DI | Hilt(Convention Plugin 自动接线) |
| 网络 | OkHttp + Retrofit + kotlinx.serialization + WebSocket |
| 持久化 | Room + DataStore |
| 分页 | Paging 3 + Room Paging |
| 播放 | Media3 / ExoPlayer(HLS 拉流) |
| 动画 | Lottie + Animatable / withFrameMillis 自绘 |
| 构建 | Gradle 8.9 + AGP 8.5 + Version Catalogs + Convention Plugins |
| 质量 | Detekt + ktlint + Kotest + MockK + Turbine |
| 性能 | Baseline Profile + Macrobenchmark + LeakCanary |
| CI | GitHub Actions(assembleDebug + unit tests + detekt) |
LiveTalk/
├── build-logic/ Convention Plugins(消除 15 个模块的 build.gradle 重复)
├── app/ 壳工程 + Navigation + HiltApplication
├── core/
│ ├── common/ Dispatcher qualifier、Clock、AppResult、Logger
│ ├── designsystem/ Material3 主题、品牌色板、通用组件(Avatar 等)
│ ├── model/ 纯 Kotlin 数据模型(跨层契约,JVM 模块)
│ ├── network/ OkHttp/Retrofit 封装、AuthInterceptor、TokenAuthenticator、WebSocketFactory
│ ├── database/ Room Database、DAO、Converter、DataStore
│ └── ui/ 通用 UI 能力(弹幕引擎、礼物 Combo 横幅)
├── data/
│ ├── im/ IM 长连接、消息协议、Outbox/Ack、Inbound 路由、前台服务、Repository
│ ├── live/ 直播 API、Repository、Paging 3 PagingSource
│ └── user/ 用户登录、Session、TokenProvider 实现(OkHttp 刷新)
└── feature/
├── auth/ 登录页(MVI + Channel 单次事件)
├── home/ 直播列表(Paging + 网格卡片)
├── conversation/ 会话列表(实时未读数)
├── chat/ 聊天页(Paging 分页 + 气泡 + 发送状态)
└── liveroom/ 直播间(ExoPlayer + 弹幕 + 礼物)
依赖方向:app → feature → data → core,单向无环;core:model 是纯 JVM 模块,禁止任何 Android 依赖,保证领域层可在纯 JVM 测试中运行。
这是项目最重点的部分,对齐线上 IM 的生产实践。
① WebSocket 状态机 + 心跳 + 退避重连(ImConnectionManager.kt)
- 状态流:
Idle → Connecting → Authenticating → Connected → Backoff → ... - 双心跳:OkHttp 内置
pingInterval(25s)+ 应用层Ping/Pong。应用层心跳带 nonce 时间戳,并跟踪lastPongAtMs;超过pongTimeoutMs (45s)没收到 Pong 就主动断连——这是捕获 NAT 半开连接的关键(OkHttp 的 ping 在某些运营商 NAT 下会"看起来成功"但对端其实已经掉线)。 - 退避算法:Decorrelated Jitter(AWS 官方推荐),避免大量客户端在服务端恢复时同时重连产生雪崩(
BackoffPolicy.kt)。 - 单写线程:使用
Dispatchers.IO.limitedParallelism(1)作为 IM 网络分发器,保证出站帧的绝对顺序,并省去对 OkHttp WebSocket writer 的显式锁。
② 消息可靠性三板斧(OutboundMessageQueue.kt + AckReconciler.kt)
| 需求 | 机制 |
|---|---|
| 不丢(at-least-once) | 消息持久化到 outbox 表,进程被杀也能恢复;ACK 到才从 outbox 删除 |
| 不重(去重) | 客户端生成 clientMessageId 作为幂等键,服务端以此返回稳定的 serverMessageId |
| 有序(同会话顺序) | 单写线程 + 工作循环按 nextAttemptAtMillis 升序拉取;MessageDao 按 serverSequence 排序 |
| 重试(网络抖动) | 指数退避 baseRetryDelayMs * 2^(attempts-1),最高 60s;超过 6 次标记 FAILED,UI 出现重试按钮 |
用户体验:enqueue() 在一个 DB 事务里同时写 messages(PENDING) 和 outbox,UI 观察 Room Flow,在真正发送之前就渲染出消息气泡——这是消息类应用"发送即刻显示"观感的实现方式。
③ 入站消息与离线补拉(InboundMessageRouter.kt)
- 每个会话维护
ackedSequence,收到 Push 时比对serverSequence > ackedSequence + 1即检测到序列断层,立即发PullSince请求服务端把缺口补回来。 - Backfill 支持分页(
hasMore标志),大块离线消息不会一次性占爆内存。 - Push 写入使用
INSERT OR IGNORE,防止"已经在 outbox 里的消息"和"服务端 Push"双路并发时产生重复行。
④ 后台常驻(ImForegroundService.kt)
- Android 14+ 要求
foregroundServiceType="dataSync"才能合法常驻。 - Service 只负责"让连接活着",业务逻辑全部通过依赖注入的单例执行;符合 Android 官方后台限制演进。
① ExoPlayer 拉流(LivePlayer.kt)
- 采用 Media3 最新
HlsMediaSource,setAllowChunklessPreparation(true)降低首帧延迟。 - 生命周期感知:
ON_STOP暂停播放,ON_START恢复,避免用户切后台时持续消耗带宽。 - 连接/读取超时做得很小(5s/8s),直播场景宁愿快速失败切换协议也不要长时间卡白屏。
② 高性能弹幕(DanmakuEngine.kt + DanmakuHost.kt)
这是我想重点讲的 Compose 性能优化案例:
- Track-based 布局:所有弹幕共享同一个
withFrameMillis时间戳,X 坐标由(currentMs - startMs) * speedPxPerMs在drawWithContent里算出——零每帧分配。 - 对象跟踪而非复用:
mutableStateListOf<LivePlacement>,到期直接移除,不搞传统 RecyclerView 式的对象池(Compose 下复用带来的好处小于心智成本)。 - 权重分级:普通弹幕在所有赛道抢位,礼物弹幕优先占用上半部赛道,避免重要信息被淹没。
- 背压:
DanmakuDispatcher使用SharedFlow(extraBufferCapacity=256, DROP_OLDEST)——礼物风暴时扔掉旧的而不是积压。 - 为什么不用
AnimatedVisibility堆叠:100+ 条弹幕会产生 100+ 个独立动画 clock,每帧都触发大量重组;我们用单一 clock +drawWithContent保证 draw 成本O(active)、重组成本几乎为 0。
③ 礼物连击(GiftComboBanner.kt)
Animatablekeyed oncomboSeq—— 每次 combo 递增都触发一次 spring 弹跳,banner 本体不卸载不重建。- 数字使用
Modifier.scale(animated)在 GPU 层放大,不占 CPU 重组。
① Convention Plugins(build-logic/)
7 个自定义插件(livetalk.android.library、livetalk.android.feature 等),每个 feature 模块的 build.gradle.kts 只有 10-15 行。升级 Kotlin/AGP/SDK 只改一处。
② Version Catalog(gradle/libs.versions.toml)
所有版本号、依赖、插件 ID 集中声明。模块里写 libs.androidx.compose.ui 而不是字符串坐标,Android Studio 可补全、重构。
③ 依赖反转
core:network 定义 TokenProvider 接口;data:user 里 SessionTokenProvider 实现。避免 network 模块依赖 user 模块(否则形成环),让认证细节(刷新、Keystore 等)可以独立演进。
④ 分层可测性
Clock接口 +FakeClock:IM 层所有时间判断可在单元测试里确定性推进。Dispatcherqualifier:测试中替换为StandardTestDispatcher,协程调度可控。WebSocketFactoryseam:测试中注入假 factory,模拟 onMessage/onFailure。
⑤ Token 刷新并发控制(SessionTokenProvider.kt)
401 风暴场景下(很多请求同时失败),用 Mutex 串行刷新并在锁内做 double-check——第一个 caller 刷新、后续 caller 直接复用新 token。避免刷新 token 被并发请求连续消费导致失效。
⑥ 消息存储索引(MessageEntity.kt)
(conversationId, serverSequence)联合索引:聊天页分页查询的主路径。serverMessageId唯一索引:服务端推送去重。nextAttemptAtMillis单列索引:Outbox 重试调度器扫描热点。
- JDK 17+(JDK 21 也可用,已验证)
- Android SDK 34(platform-34、build-tools 34.0.0)
- macOS / Linux / Windows 均可
首次拉仓库后,根据你的 Android SDK 路径改 local.properties:
sdk.dir=/your/path/to/android-sdk# 编译 debug APK(产物:app/build/outputs/apk/debug/app-debug.apk)
./gradlew :app:assembleDebug
# 连上手机/模拟器后直接装
./gradlew :app:installDebug
# 或者手动推 apk:
adb install -r app/build/outputs/apk/debug/app-debug.apk首次构建会下载 Gradle 8.9 distribution(~120MB)和 AGP/Compose 等依赖(~500MB),需要 3-5 分钟;之后增量构建 < 30s。
Demo 模式是默认开启的(core/common/AppConfig.demoMode = true),没有后端也能跑完整流程:
- 登录页:随便输一个手机号,密码可空,点"登录"直接进主页。
- 直播 tab:看到 6 张假直播封面,点击任意一张进入直播间。
- 直播间:
- 视频源是 Apple 公开的 HLS 测试流(稳定不停),能看到真实的视频解码与播放
- 顶部是主播信息条,中间是自动滚动的弹幕(每 220ms 一条,高优先级会走上半部赛道)
- 底部"发弹幕"按钮点一下自己也能插入一条
- 消息 tab:5 个预置会话,其中 2 条带未读红点。
- 聊天页:点任一会话进入,底部输入任意文字发送——消息立刻显示为 SENT 状态;每 6-8 秒会有假 Push 从后端(DB 层注入)进来。
- 底部未读角标:随 Push 增长;点进会话后清零。
# 全部单元测试(BackoffPolicy / FrameCodec / DanmakuEngine)
./gradlew testDebugUnitTest
# 静态检查
./gradlew detekt
# 清缓存重来
./gradlew clean
# 查看完整任务列表
./gradlew tasks| 症状 | 解决 |
|---|---|
Directory does not exist 指向 sdk.dir |
改 local.properties 里的 sdk.dir= 为你本机路径 |
Unsupported Java version |
升级 JDK 到 17+;项目 CI 用的是 JDK 17 |
| 首次 sync 超慢 | 用代理或镜像(Gradle 和 Maven Central 在国内不稳) |
| 装机后看不到视频 | Demo HLS 在 devstreaming-cdn.apple.com,有时国内网络会超时;换 VPN 即可 |
| APK 24MB 偏大 | Debug 带 LeakCanary + Compose tooling;:app:assembleRelease 会用 minify + shrinkResources 瘦到 < 10MB |
把 core/common/src/main/kotlin/com/qwfy/livetalk/core/common/AppConfig.kt 里的 demoMode 改成 false。那时:
- 登录会真打 HTTP 请求(当前后端域名是占位,会 404)
- IM 会尝试连 WebSocket(同上)
- 直播列表会是空(API 返回 404)
这是预期行为 —— 这个仓库的目的是展示代码,不是跑真实服务。
支持 Android 7.0 / API 24 起步。
以下项被故意搁置,换取在关键路径上投入更多——每一项都会在面试时展开解释:
| 取舍 | 原因 | 生产做法 |
|---|---|---|
| 协议用 JSON 不是 Protobuf | 不想拉进 protoc 工具链,让仓库开箱即用 |
升级为 Protobuf(envelope 形状一致,已按此预留) |
| Token 存 DataStore 明文 | 展示 DataStore 用法 | Jetpack Security / Keystore 包一层 |
| 直播推流仅保留接口 | 推流涉及 CameraX + MediaCodec + OpenGL ES + RTMP 编码,单独一篇文章 | 独立仓库演示 |
| 服务端 Mock | 面试是聚焦客户端代码,真实后端不在本项目范围 | MockWebServer + JSON Fixture 已接入可自启 |
| UI 测试覆盖率低 | Compose 测试成本高,优先把单元测试写在 IM 核心逻辑上 | 补充 robolectric + compose test |
| WebRTC 连麦 | 和直播推流同一量级的系统工程 | 单独仓库 |
- 构建骨架:
settings.gradle.kts→gradle/libs.versions.toml→build-logic/convention/** - IM 核心(重点):
data/im/connection/ImConnectionManager.kt(状态机)data/im/connection/BackoffPolicy.kt(退避)data/im/outbound/OutboundMessageQueue.kt(发送+重试)data/im/outbound/AckReconciler.kt(ACK 对账)data/im/inbound/InboundMessageRouter.kt(入站+补拉)
- 网络层:
core/network/auth/*(AuthInterceptor、TokenAuthenticator、SessionTokenProvider) - 弹幕:
core/ui/danmaku/DanmakuEngine.kt+DanmakuHost.kt - 直播间:
feature/liveroom/player/LivePlayer.kt+LiveRoomScreen.kt
- Android 工程化:多模块划分、Convention Plugins、Version Catalog、依赖倒置
- Kotlin 原生:Coroutines/Flow 高级模式、状态机、Channel vs SharedFlow 选型
- Compose 深度:自定义 Layout、
withFrameMillis驱动动画、性能意识(Stable/Immutable、derivedStateOf、对象分配) - IM 系统设计:长连接、心跳、退避、ACK、幂等、离线补拉、持久化 outbox
- 音视频:ExoPlayer 配置、HLS 参数、生命周期与带宽管理
- 工程质量:可测性设计(Clock / Dispatcher / Factory 抽象)、单元测试、Detekt、CI
欢迎一起看代码、提问。