阅读导航(建议先看)
如果你是第一次接触这套 SDK,建议按下面顺序读:
- 先看 2 章:理解 commonMain 与双端 actual 的职责边界;
- 再看 5~6 章:把“主流程 + 双端播放器实现”串起来;
- 最后看 10 章:按需查 Debug 手册,不必一次读完。
如果你只想快速建立整体认知,优先看 0 + 5 + 9 + 12 四节即可。
0. 这篇写什么
这篇不再重复 KMM 概念,而是聚焦一件事:AIRead 这套 KMM SDK 在工程上到底怎么跑起来。
会重点回答四个问题:
- 工程目录怎么分层;
- 核心 API 给了什么能力;
- 从
start(contentId, sentenceId)到播放/高亮/回调/重连,链路如何闭环; - Android(ExoPlayer)和 iOS(AVPlayer)分别承担了什么。
KMM 基础与平台适配背景放在上一篇:KMM 基础篇。
1. 仓库目录总览
AIRead 仓库根目录:
AIRead/
├── androidApp/ # Android 示例应用(Compose + WebView)
├── iosApp/ # iOS 示例应用(SwiftUI + WKWebView)
├── shared/ # KMM SDK 主模块
├── gradle/ # 版本目录、发布脚本、wrapper
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
└── README.md
先按根目录分工看一眼:
androidApp/iosApp:示例宿主,用来验证 SDK 接入;shared:SDK 主模块(本文重点);gradle+ 根构建文件:版本管理、构建与发布配置。
shared/src 是核心,下面这棵目录可以理解为“统一编排 + 平台执行”:
shared/src/
├── commonMain/kotlin/com/tencent/qqsports/airead/
│ ├── AIReadConfig.kt
│ ├── AIReadManager.kt
│ ├── data/
│ │ ├── AudioInfo.kt
│ │ ├── ParagraphInfo.kt
│ │ └── RetryConfig.kt
│ ├── player/
│ │ ├── PlayCallback.kt
│ │ ├── PlayMediaItem.kt
│ │ ├── PlayerController.kt
│ │ ├── PlayerListener.kt
│ │ └── PlayState.kt
│ ├── websocket/
│ │ └── WebSocketClient.kt
│ ├── webview/
│ │ ├── PlatformWebView.kt
│ │ ├── WebPosition.kt
│ │ └── WebViewNotifier.kt
│ ├── log/
│ │ ├── Logger.kt
│ │ └── LogUtil.kt
│ └── platform/
│ └── Platform.kt
├── androidMain/kotlin/com/tencent/qqsports/airead/
│ ├── AIReadInitConfig.kt
│ └── platform/
│ ├── Platform.android.kt
│ ├── AndroidPlayerController.kt
│ ├── AndroidPlayerControllerProxy.kt
│ ├── AndroidWebView.kt
│ └── AndroidLogger.kt
└── iosMain/kotlin/com/tencent/qqsports/airead/
├── AIReadInitConfig.kt
├── queuePlayer/
│ ├── QueuePlayer.kt
│ ├── QueuePlayerListener.kt
│ ├── QueuePlayerNode.kt
│ └── QueuePlayerState.kt
└── platform/
├── Platform.ios.kt
├── IOSPlayerController.kt
├── IOSWebView.kt
└── IOSLogger.kt
看完目录后,可以先建立一个“目录→职责”的映射:
| 目录 | 主要职责 | 你通常在什么场景修改它 |
|---|---|---|
shared/src/commonMain | 业务状态机、播放编排、跨端统一回调语义 | 调整播放流程、回调规则、重试策略 |
shared/src/androidMain | ExoPlayer 与 Android WebView 侧落地 | Android 播放行为、线程调度、WebView 注入联动 |
shared/src/iosMain | AVPlayer/KVO 与 iOS WebView 侧落地 | iOS 播放队列、状态映射、WKWebView 联动 |
androidApp / iosApp | SDK 示例接入与验证 | 验证 API 用法、回归联调、演示场景复现 |
一句话:**SDK 的“决策层”在 commonMain,“执行层”在 androidMain/iosMain。**下面直接拆这条边界。
2. 模块结构:KMM 是怎么拆层的
先说结论:commonMain 负责“业务状态机与流程控制”,androidMain/iosMain 负责“把流程落到各自播放器与系统能力”。
2.1 commonMain:业务主脑
commonMain 放统一逻辑:
- 会话状态与控制入口:
AIReadManager.kt - 流式数据接收:
websocket/WebSocketClient.kt - 状态/进度/句子回调分发:
AIReadManager.kt - WebView 高亮通知:
webview/WebViewNotifier.kt - 重试策略与播放状态定义:
data/RetryConfig.kt、player/PlayState.kt
2.1.1 commonMain 子目录职责速查
| 子目录/文件 | 角色 | 关键输出 |
|---|---|---|
AIReadManager.kt | 总调度器(状态机入口) | 播放控制、状态回调、句子同步 |
websocket/ | 流式音频数据入口 | AudioInfo 数据流、重连行为 |
player/ | 跨端播放器抽象层 | PlayerController 接口与 PlayState 语义 |
webview/ | 播放状态到页面桥接 | 高亮事件与滚动通知 |
data/ | 协议与策略模型 | AudioInfo、ParagraphInfo、RetryConfig |
platform/Platform.kt | expect 声明点 | 把播放器与平台实现解耦 |
这层可以理解成“可跨端复用的业务控制面”,不直接依赖 ExoPlayer/AVPlayer 细节。
2.2 androidMain / iosMain:平台实现
commonMain 通过 expect/actual 取平台播放器:
- expect 定义:
shared/src/commonMain/.../platform/Platform.kt - Android actual:
shared/src/androidMain/.../platform/Platform.android.kt - iOS actual:
shared/src/iosMain/.../platform/Platform.ios.kt
Android 用 ExoPlayer:
- 核心实现:
AndroidPlayerController.kt - UI 线程代理:
AndroidPlayerControllerProxy.kt
iOS 用 AVPlayer(队列封装):
- 核心实现:
queuePlayer/QueuePlayer.kt - 控制器封装:
platform/IOSPlayerController.kt
3. 构建与发布配置
这一节不展开构建原理,只回答“它是怎么被打包和交付的”。从 shared/build.gradle.kts 看,这是一个标准 KMP 双端产物工程:
- 目标:
android + iosX64 + iosArm64 + iosSimulatorArm64 - Android 发布 artifact:
airead - iOS Cocoapods:
name = "AIRead",baseName = "AIRead" - 依赖:
- common:Ktor client/websocket/logging + kotlinx serialization
- Android:
androidx.media3:media3-exoplayer - iOS:Ktor Darwin +
AIReadKVOHelperpod
见:shared/build.gradle.kts(AIRead 仓库内)
另外:
- iOS Pod 入口:
shared/AIRead.podspec - iOS 示例 Podfile:
iosApp/Podfile - 版本目录:
gradle/libs.versions.toml
到这里可以把它当成一个“可发布 SDK”。下一步看它暴露给业务方的 API 面。
4. 对外 API(代码级)
4.1 初始化
Android:
AIReadInitConfig.init(
context,
wsUrl,
params,
retryConfig,
needTriggerScroll
)
定义位置:shared/src/androidMain/kotlin/com/tencent/qqsports/airead/AIReadInitConfig.kt
iOS:
AIReadInitConfig.init(wsUrl, params, retryConfig, needTriggerScroll)
定义位置:shared/src/iosMain/kotlin/com/tencent/qqsports/airead/AIReadInitConfig.kt
4.2 核心控制
AIReadManager.start(contentId, sentenceId, startPosition, callback)
AIReadManager.pause()
AIReadManager.resume()
AIReadManager.seekTo(positionMillis)
AIReadManager.seekTo(sentenceId)
AIReadManager.attachWebView(webView)
AIReadManager.detachWebView()
AIReadManager.release()
定义位置:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt
4.3 状态与重试
播放状态:IDLE / BUFFER / PLAYING / STOP / PAUSE / ERROR
定义位置:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/player/PlayState.kt
重试策略:
- 次数:
times(默认 3) - 延迟模式:
FIXED / MULTIPLICATION - 延迟时长:
delayTimeMillis(默认 5000ms) - 连接超时:
timeout(默认 60000ms)
定义位置:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/data/RetryConfig.kt
5. “怎么做的”:端到端执行链路
这是全文最核心的一节。先看一句话主线:AIReadManager 负责编排,WebSocketClient 负责喂数据,PlayerController 负责执行播放,WebViewNotifier 负责把播放状态同步回页面。
下面是代码里的真实执行顺序(以 Android 路径举例,iOS 同构):
sequenceDiagram
participant App as App层
participant M as AIReadManager
participant W as WebSocketClient
participant P as PlayerController
participant V as WebViewNotifier
App->>M: start(contentId, sentenceId, startPosition, callback)
M->>M: stopCurrentTask + initializePlayback
M->>P: setPlayListener(createPlayerListener)
M->>W: connect(contentId, false)
W-->>M: onMessageReceived(json)
M->>M: handleWebSocketMessage -> AudioInfo反序列化
M->>P: addItem(PlayMediaItem)
M->>P: play(mediaItem, position)
P-->>M: onPlayStateChange(PLAYING/PAUSE/STOP/ERROR)
M-->>App: onAudioStateChange(state)
P-->>M: onPlayProgressChange(progress, mediaIndex)
M-->>App: onProgressChange(totalProgress, duration)
M-->>App: onPlaySentenceChange(sentenceId)
M->>V: notify(contentId, paragraph, state)
V->>WebView: evaluateJavaScript(aiReadHighlight / CustomEvent)
5.1 关键步骤对应代码
start()入口:AIReadManager.kt(start)- 重置旧任务:
AIReadManager.kt(stopCurrentTask) - 建立 WS 连接:
AIReadManager.kt(connectWebSocket)→WebSocketClient.kt(connect/receivedData) - 收到 JSON:
AIReadManager.kt(handleWebSocketMessage) - 加入播放队列:
AIReadManager.kt(addItem) - 按
sentenceId/startPosition定位播放点:AIReadManager.kt(play) - 句子切换检测:
AIReadManager.kt(checkSentenceChange/findTargetSentenceId) - 进度汇总(多段音频累计):
AIReadManager.kt(checkProcessChange) - WebView 高亮同步:
WebViewNotifier.kt(notify) - 异常重连:
WebSocketClient.kt(reconnectWithException)
5.2 文件级调用链(按职责看)
如果你在排查“播放没动 / 高亮没同步 / 状态没回调”,可以按这条文件链路快速定位:
- 入口层:
AIReadManager.start()收到业务调用; - 数据层:
WebSocketClient持续推送AudioInfo; - 编排层:
AIReadManager决定何时addItem/play/seek; - 执行层:
- Android:
AndroidPlayerController实际驱动 ExoPlayer; - iOS:
IOSPlayerController/QueuePlayer实际驱动 AVPlayer;
- Android:
- 联动层:
WebViewNotifier把 sentence/state 同步到前端页面。
这个顺序基本就是 AIRead 的“问题定位顺序”:先看有没有数据,再看有没有编排,再看平台播放器是否执行。
5.3 WebSocket 完结信号
WebSocketClient 里把 {"isEnd":true} 作为“数据接收完成”标志;如果未完成且异常,则走重连。
定义位置:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/websocket/WebSocketClient.kt
6. 平台实现细节
同样一条业务链路,在双端的“落地方式”并不相同:Android 偏 ExoPlayer 事件驱动,iOS 偏 AVPlayer/KVO 组合。下面分开看。
6.1 Android:ExoPlayer + 主线程进度轮询
AndroidPlayerController.kt 关键点:
- 用
ExoPlayer.Builder(context).setLooper(Looper.getMainLooper())初始化; onIsPlayingChanged映射到PlayState.PLAYING/PAUSE;Handler(Looper.getMainLooper())每秒上报一次进度;play(mediaItem, position)通过mediaId找索引并seekTo(index, position)。
再由 AndroidPlayerControllerProxy.kt 保证调用回到 UI 线程。
6.2 iOS:AVPlayer 队列 + KVO
QueuePlayer.kt 关键点:
- 用链表维护播放队列:
headNode/tailNode/currentNode; addPeriodicTimeObserverForInterval做进度回调;- 监听
AVPlayerItemDidPlayToEndTimeNotification做自动切段; - 通过
AIReadKVOHelper监听currentItem.status,失败态映射到ERROR。
IOSPlayerController.kt 负责把 QueuePlayerState 映射成统一 PlayState。
6.3 WebView 联动
WebViewNotifier.kt 有两种前端通知模式:
window.aiReadHighlight(...)window.dispatchEvent(new CustomEvent("airead.syncSentenceInfo", ...))
开关来自 AIReadConfig.isNeedTriggerScroll()。
6.4 平台官方文档速查
为了方便读者延伸阅读,下面整理了文中提到的关键平台能力官方文档:
Android
- ExoPlayer(Media3)总览:https://developer.android.com/media/media3/exoplayer
- Player 事件监听:https://developer.android.com/media/media3/exoplayer/listening-to-player-events
Handler:https://developer.android.com/reference/android/os/HandlerLooper:https://developer.android.com/reference/android/os/LooperWebView:https://developer.android.com/reference/android/webkit/WebView
iOS
AVPlayer:https://developer.apple.com/documentation/avfoundation/avplayeraddPeriodicTimeObserver(forInterval:queue:using:):https://developer.apple.com/documentation/avfoundation/avplayer/addperiodictimeobserver(forinterval:queue:using:)AVPlayerItemDidPlayToEndTimeNotification:https://developer.apple.com/documentation/avfoundation/avplayeritem/didplaytoendtimenotification- KVO(Key-Value Observing):https://developer.apple.com/documentation/swift/using-key-value-observing-in-swift
WKWebView:https://developer.apple.com/documentation/webkit/wkwebview
7. 示例工程怎么接入
7.1 Android 示例(androidApp)
- 初始化(
AudioApp.kt):
AIReadInitConfig.init(this, "wss://<your-domain>/content/voice", null)
- 播放(
AudioViewModel.kt):
AIReadManager.start(contentId = contentId, sentenceId = "2", callback = object : PlayCallback { ... })
- WebView 绑定(
AudioWebView.kt):
AIReadManager.attachWebView(AndroidWebView(webView))
7.2 iOS 示例(iosApp)
- 初始化(
AudioViewModel.swift):
AIReadInitConfig.shared.doInit(wsUrl: "wss://<your-domain>/content/voice")
- 播放:
AIReadManager.shared.start(contentId: contentID, sentenceId: nil, startPosition: nil, callback: self)
- WebView 绑定:
AIReadManager.shared.attachWebView(webView: IOSWebView(webView: webView))
8. 关键文件拆读(按阅读顺序)
如果你准备真正下手排查或改代码,这节可以当“最短阅读路径”。目标不是面面俱到,而是让你快速建立可操作的源码地图。
8.1 AIReadManager.kt:总调度 + 业务状态机
文件:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt
建议重点看这些方法:
start(...):统一入口,串起stopCurrentTask -> initializePlayback -> initializePlayer -> connectWebSocket。handleWebSocketMessage(...):把 WS 文本反序列化为AudioInfo,去重后入队并触发播放。play(audioInfo):处理三种起播路径(从头播 / sentenceId 定位 / startPosition 定位)。checkSentenceChange(...)+findTargetSentenceId(...):根据播放进度推导当前句子并回调。checkProcessChange(...):按多段音频累计总进度,回调onProgressChange。resume():恢复播放时补做 WS 重连(连接已断且数据未收完时)。
读完这个文件,你会拿到完整“控制平面”。
8.2 WebSocketClient.kt:数据平面(流式接收与重连)
文件:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/websocket/WebSocketClient.kt
建议重点看:
connect(contentId, isReconnect):重连入口。receivedData(...):真正消费 WS incoming frame,分发消息给AIReadManager。reconnectWithException(...):失败后的重试分支。RetryConfig.getTotalDelayTime(...)(在RetryConfig.kt):控制固定/线性递增延迟。
实现里有一个关键协议约定:{"isEnd":true} 表示流式数据接收完成。
8.3 AndroidPlayerController.kt + AndroidPlayerControllerProxy.kt:Android 播放执行器
文件:
shared/src/androidMain/kotlin/com/tencent/qqsports/airead/platform/AndroidPlayerController.ktshared/src/androidMain/kotlin/com/tencent/qqsports/airead/platform/AndroidPlayerControllerProxy.kt
建议重点看:
setPlayListener(...):把 ExoPlayer 状态映射到统一PlayState。addItem(...):动态加MediaItem并在IDLE/ENDED时 prepare。play(mediaItem, position):通过mediaId查索引并 seek。updateProgressState()+Handler:每秒进度轮询。- Proxy 的
runOnUiThread(...):保证控制调用在主线程执行。
8.4 QueuePlayer.kt + IOSPlayerController.kt:iOS 播放执行器
文件:
shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.ktshared/src/iosMain/kotlin/com/tencent/qqsports/airead/platform/IOSPlayerController.kt
建议重点看:
QueuePlayer.addItem(...):链表化队列组织。queuePlayerPlay(...):按节点播放、seek、错误恢复。didPlayToEnd(...):切到 nextNode 自动续播。didReceiveObjChange(...):通过 KVO 捕捉失败态并上抛 ERROR。IOSPlayerController:把QueuePlayerState统一映射成PlayState。
8.5 WebViewNotifier.kt:句子高亮桥接层
文件:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/webview/WebViewNotifier.kt
建议重点看:
notify(...):把ParagraphInfoDetail序列化为 JSON 注入 JS。- 两种前端协议:
window.aiReadHighlight(...)与CustomEvent("airead.syncSentenceInfo")。 evaluateJavaScript回调position后的scrollToPosition(...)调用链。
8.6 推荐阅读顺序(30 分钟)
- 先读
AIReadManager.kt(抓主流程)。 - 再读
WebSocketClient.kt(抓数据流与重连)。 - 再读 Android 或 iOS 播放器其一(抓平台执行细节)。
- 最后看
WebViewNotifier.kt(抓 UI 联动闭环)。
9. 当前实现最关键的三个设计点
- 状态机收敛在 commonMain:业务语义只维护一套,避免双端状态漂移。
- 播放器能力下沉到平台层:Android 用 ExoPlayer、iOS 用 AVPlayer,但对上统一为
PlayerController。 - 播放与页面联动做成闭环:不仅有音频播放,还有句子高亮与状态回传,保证“听到哪、看到哪”。
10. Debug 手册(基于 kotlin_debug 工具)
这部分偏工具化,建议按需查阅:
- 想快速开始:优先看 10.1 + 10.2 + 10.3;
- 想做主工程稳定联调:直接看 10.7 + 10.8;
- 遇到异常再回查 10.4。
下面给出可直接落地的 Kotlin/Native 调试流程,工具目录在:
content/posts/KMM/kotlin_debug
该目录包含 4 个脚本:
xc_kt_install.shxc_kt_install.pykonan_lldb.pydeleteKotlinVersionFromMaven.py.py
10.1 先装调试插件(xcode-kotlin)
目的:让 Xcode + LLDB 能更好识别 Kotlin/Native 对象。
可选两种方式:
# 方式1:shell
bash content/posts/KMM/kotlin_debug/xc_kt_install.sh
# 方式2:python
python3 content/posts/KMM/kotlin_debug/xc_kt_install.py
脚本实际做了三件事:
- 检查并安装
brew xcode-kotlin; - 执行
xcode-kotlin install安装 XC 插件; - 扫描
/Applications/Xcode*并逐个执行xcode-kotlin sync <XcodeAppPath>。
对应实现可看:
content/posts/KMM/kotlin_debug/xc_kt_install.sh:7content/posts/KMM/kotlin_debug/xc_kt_install.sh:27content/posts/KMM/kotlin_debug/xc_kt_install.sh:62content/posts/KMM/kotlin_debug/xc_kt_install.py:18content/posts/KMM/kotlin_debug/xc_kt_install.py:54content/posts/KMM/kotlin_debug/xc_kt_install.py:98
注意:脚本检测到
Xcode is running会直接失败,先关 Xcode 再执行。
10.2 在 LLDB 导入 Kotlin synthetic provider
核心脚本是 konan_lldb.py,它会给 ObjHeader * 注册 summary/synthetic provider,让 Kotlin 对象不再只显示裸指针。
在 LLDB 里执行:
command script import content/posts/KMM/kotlin_debug/konan_lldb.py
脚本初始化时会做这些事:
- 注册
type summary add ... konan_lldb.kotlin_object_type_summary "ObjHeader *"; - 注册
type synthetic add ... konan_lldb.KonanProxyTypeProvider "ObjHeader *"; - 启用
Kotlintype category; - 增加自定义命令:
type_name、type_by_address、symbol_by_name。
对应实现:content/posts/KMM/kotlin_debug/konan_lldb.py:1067。
此外脚本会写调试日志到:
~/konan_lldb_log.txt
见:content/posts/KMM/kotlin_debug/konan_lldb.py:1069。
10.3 结合 AIRead 的实战调试路径
建议按下面顺序调试(iOS 侧最有价值):
- 在
AIReadManager.start、handleWebSocketMessage、play打断点; - 在
QueuePlayer.queuePlayerPlay、didPlayToEnd、didReceiveObjChange打断点; - 命中断点后,用 LLDB 查看 Kotlin 对象摘要(脚本已接管
ObjHeader *显示); - 用
symbol_by_name查 Kotlin 符号,用type_name看地址对应类型。
建议优先断点位置:
shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:61shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:273shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:299shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:141shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:83shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:279
10.4 常见故障与排查
插件安装总失败
- 先确认 Xcode 关闭;
- 再单独跑
xcode-kotlin info看是否仍是Plugin not installed。
对象仍显示指针,没摘要
- 确认 LLDB 已执行
command script import .../konan_lldb.py; - 确认当前对象类型是
ObjHeader *(脚本主要接管这个类型)。
- 确认 LLDB 已执行
脚本跑了但结果不稳定
konan_lldb.py里有缓存字典;可重新 import 脚本并重启调试会话;- 看
~/konan_lldb_log.txt里Konan_Debug...调用链是否异常。
脚本执行方式问题
xc_kt_install.sh首行是全角感叹号(#!/bin/bash),直接可执行可能失败;- 建议显式
bash xc_kt_install.sh。
10.5 deleteKotlinVersionFromMaven.py.py 的用途
这个脚本不是运行时调试工具,而是“仓库清理工具”:
- 通过 mirrors API 枚举
tmm-snapshot/org/jetbrains/kotlin工件; - 按
versions = [...]删除指定 Kotlin 版本节点。
对应实现:
content/posts/KMM/kotlin_debug/deleteKotlinVersionFromMaven.py.py:58content/posts/KMM/kotlin_debug/deleteKotlinVersionFromMaven.py.py:75content/posts/KMM/kotlin_debug/deleteKotlinVersionFromMaven.py.py:104
这一步只建议给维护私有 Maven 仓库的人使用,不建议作为业务排障常规动作。
10.6 Debug 原理补充(从主工程视角)
SDK 集成到 iOS 主工程后,断点能不能“进 Kotlin”,核心取决于三件事:
有没有 Kotlin 符号与调试信息
- 集成方式如果是源码 Pod(
pod 'AIRead', :git => ...或:path),通常更容易拿到可追踪符号; - 如果是纯二进制分发,常见情况是只能做符号级/汇编级排查,源码断点能力受限。
- 集成方式如果是源码 Pod(
LLDB 是否加载了 Kotlin 对象可视化脚本
konan_lldb.py负责把ObjHeader *从“裸地址”转换成可读对象摘要;- 没加载脚本时,经常看到的是指针地址,不是字段内容。
Xcode-kotlin 插件是否与本机 Xcode 版本同步
- 插件安装后还需要
xcode-kotlin sync /Applications/Xcode*.app; - 换 Xcode 版本后如果没同步,调试体验会明显退化。
- 插件安装后还需要
10.7 SDK 集成到 iOS 主工程后的断点调试步骤
下面按“可直接执行”的顺序给步骤。
Step 1:主工程 Pod 集成(确认是可调试集成)
pod 'AIRead', :git => '<AIRead-repo-url>', :tag => '0.1.2-SNAPSHOT'
示例来源:AIRead 仓库 README 的 Pod 集成片段
若使用本地联调,也可
:path指向本地 SDK 目录,便于直接改 SDK 源码并断点。
Step 2:安装并同步 xcode-kotlin(一次性 + 版本变更后重做)
bash content/posts/KMM/kotlin_debug/xc_kt_install.sh
# 或
python3 content/posts/KMM/kotlin_debug/xc_kt_install.py
脚本会检测并安装 brew 包、安装插件、同步全部 Xcode。见:
content/posts/KMM/kotlin_debug/xc_kt_install.sh:7content/posts/KMM/kotlin_debug/xc_kt_install.sh:27content/posts/KMM/kotlin_debug/xc_kt_install.sh:62
Step 3:在主工程调试会话里加载 LLDB 脚本
在 Xcode 启动调试后(LLDB 控制台)执行:
command script import content/posts/KMM/kotlin_debug/konan_lldb.py
可选:把这条放进 ~/.lldbinit,避免每次手工输入。
Step 4:在“调用点 + SDK 核心链路”双点位下断
主工程调用点(Swift):
AIReadManager.shared.start(...)AIReadManager.shared.pause()/resume()/release()
SDK 核心链路(Kotlin,优先这几处):
shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:61(start)shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:273(WS 消息处理)shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:299(起播定位)shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:141(queuePlayerPlay)shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:83(didPlayToEnd)
这样能同时看到:业务触发 -> SDK 状态机 -> iOS 播放器执行。
Step 5:断点命中后如何看对象
- 直接
po/expr看 Swift 入参; - 对 Kotlin 对象,用脚本提供的 summary/synthetic 展示;
- 辅助命令:
symbol_by_name kfun:.*AIReadManager.*
type_name <address>
命令注册位置:content/posts/KMM/kotlin_debug/konan_lldb.py:1099。
Step 6:无法命中 Kotlin 断点时的排查顺序
- 确认不是 Release 配置在跑(先用 Debug);
- 确认插件已安装且 sync 过当前 Xcode;
- 确认 LLDB 已 import
konan_lldb.py; - 确认当前集成方式不是“无源码的纯二进制”;
- 退化方案:先在 Swift 调用点断,再用
symbol_by_name+ 地址级别排查。
10.8 推荐的主工程调试基线
- 每次 Xcode 升级后重新执行一次
xcode-kotlin sync; - 团队统一一份
.lldbinit(自动 importkonan_lldb.py); - 关键链路固定断点模板:
start -> handleWebSocketMessage -> queuePlayerPlay; - 出现“对象只显示指针”时,第一优先检查 LLDB 脚本是否成功加载。
11. 当前可见的改进点(按优先级)
- 先补 WebView 滚动落地:
AndroidWebView.scrollToPosition()与IOSWebView.scrollToPosition()仍为空,导致高亮回调有数据但无真实滚动。 - 再修示例状态比较:Android 示例中的
AudioPlayerUiState.equals()固定返回false,会干扰状态驱动 UI 的判断(示例层问题)。 - 最后增强协议语义:
WebSocketClient目前依赖{"isEnd":true}判定流结束,建议补充版本/消息类型字段,降低协议歧义。
12. 结论
如果从工程视角看,AIRead 的价值不在“用了 KMM”,而在“把跨端音频播报链路真正闭环”:
- 数据侧:WebSocket 流式接收与重连;
- 播放侧:Android ExoPlayer / iOS AVPlayer 各自落地但接口统一;
- 业务侧:
PlayState + Progress + SentenceId回调语义一致; - 展示侧:WebView 句子高亮与滚动联动。
一句话总结:这是一套可接入业务、可排障、可持续演进的 KMM 语音播报 SDK,而不是演示型 Demo。