阅读导航(建议先看)

如果你是第一次接触这套 SDK,建议按下面顺序读:

  1. 先看 2 章:理解 commonMain 与双端 actual 的职责边界;
  2. 再看 5~6 章:把“主流程 + 双端播放器实现”串起来;
  3. 最后看 10 章:按需查 Debug 手册,不必一次读完。

如果你只想快速建立整体认知,优先看 0 + 5 + 9 + 12 四节即可。

0. 这篇写什么

这篇不再重复 KMM 概念,而是聚焦一件事:AIRead 这套 KMM SDK 在工程上到底怎么跑起来

会重点回答四个问题:

  1. 工程目录怎么分层;
  2. 核心 API 给了什么能力;
  3. start(contentId, sentenceId) 到播放/高亮/回调/重连,链路如何闭环;
  4. 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/androidMainExoPlayer 与 Android WebView 侧落地Android 播放行为、线程调度、WebView 注入联动
shared/src/iosMainAVPlayer/KVO 与 iOS WebView 侧落地iOS 播放队列、状态映射、WKWebView 联动
androidApp / iosAppSDK 示例接入与验证验证 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.ktplayer/PlayState.kt

2.1.1 commonMain 子目录职责速查

子目录/文件角色关键输出
AIReadManager.kt总调度器(状态机入口)播放控制、状态回调、句子同步
websocket/流式音频数据入口AudioInfo 数据流、重连行为
player/跨端播放器抽象层PlayerController 接口与 PlayState 语义
webview/播放状态到页面桥接高亮事件与滚动通知
data/协议与策略模型AudioInfoParagraphInfoRetryConfig
platform/Platform.ktexpect 声明点把播放器与平台实现解耦

这层可以理解成“可跨端复用的业务控制面”,不直接依赖 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 + AIReadKVOHelper pod

见: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 关键步骤对应代码

  1. start() 入口:AIReadManager.ktstart
  2. 重置旧任务:AIReadManager.ktstopCurrentTask
  3. 建立 WS 连接:AIReadManager.ktconnectWebSocket)→ WebSocketClient.ktconnect/receivedData
  4. 收到 JSON:AIReadManager.kthandleWebSocketMessage
  5. 加入播放队列:AIReadManager.ktaddItem
  6. sentenceId/startPosition 定位播放点:AIReadManager.ktplay
  7. 句子切换检测:AIReadManager.ktcheckSentenceChange/findTargetSentenceId
  8. 进度汇总(多段音频累计):AIReadManager.ktcheckProcessChange
  9. WebView 高亮同步:WebViewNotifier.ktnotify
  10. 异常重连:WebSocketClient.ktreconnectWithException

5.2 文件级调用链(按职责看)

如果你在排查“播放没动 / 高亮没同步 / 状态没回调”,可以按这条文件链路快速定位:

  1. 入口层AIReadManager.start() 收到业务调用;
  2. 数据层WebSocketClient 持续推送 AudioInfo
  3. 编排层AIReadManager 决定何时 addItem/play/seek
  4. 执行层
    • Android:AndroidPlayerController 实际驱动 ExoPlayer;
    • iOS:IOSPlayerController / QueuePlayer 实际驱动 AVPlayer;
  5. 联动层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 有两种前端通知模式:

  1. window.aiReadHighlight(...)
  2. window.dispatchEvent(new CustomEvent("airead.syncSentenceInfo", ...))

开关来自 AIReadConfig.isNeedTriggerScroll()

6.4 平台官方文档速查

为了方便读者延伸阅读,下面整理了文中提到的关键平台能力官方文档:

Android

iOS


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

建议重点看这些方法:

  1. start(...):统一入口,串起 stopCurrentTask -> initializePlayback -> initializePlayer -> connectWebSocket
  2. handleWebSocketMessage(...):把 WS 文本反序列化为 AudioInfo,去重后入队并触发播放。
  3. play(audioInfo):处理三种起播路径(从头播 / sentenceId 定位 / startPosition 定位)。
  4. checkSentenceChange(...) + findTargetSentenceId(...):根据播放进度推导当前句子并回调。
  5. checkProcessChange(...):按多段音频累计总进度,回调 onProgressChange
  6. resume():恢复播放时补做 WS 重连(连接已断且数据未收完时)。

读完这个文件,你会拿到完整“控制平面”。

8.2 WebSocketClient.kt:数据平面(流式接收与重连)

文件:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/websocket/WebSocketClient.kt

建议重点看:

  1. connect(contentId, isReconnect):重连入口。
  2. receivedData(...):真正消费 WS incoming frame,分发消息给 AIReadManager
  3. reconnectWithException(...):失败后的重试分支。
  4. RetryConfig.getTotalDelayTime(...)(在 RetryConfig.kt):控制固定/线性递增延迟。

实现里有一个关键协议约定:{"isEnd":true} 表示流式数据接收完成。

8.3 AndroidPlayerController.kt + AndroidPlayerControllerProxy.kt:Android 播放执行器

文件:

  • shared/src/androidMain/kotlin/com/tencent/qqsports/airead/platform/AndroidPlayerController.kt
  • shared/src/androidMain/kotlin/com/tencent/qqsports/airead/platform/AndroidPlayerControllerProxy.kt

建议重点看:

  1. setPlayListener(...):把 ExoPlayer 状态映射到统一 PlayState
  2. addItem(...):动态加 MediaItem 并在 IDLE/ENDED 时 prepare。
  3. play(mediaItem, position):通过 mediaId 查索引并 seek。
  4. updateProgressState() + Handler:每秒进度轮询。
  5. Proxy 的 runOnUiThread(...):保证控制调用在主线程执行。

8.4 QueuePlayer.kt + IOSPlayerController.kt:iOS 播放执行器

文件:

  • shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt
  • shared/src/iosMain/kotlin/com/tencent/qqsports/airead/platform/IOSPlayerController.kt

建议重点看:

  1. QueuePlayer.addItem(...):链表化队列组织。
  2. queuePlayerPlay(...):按节点播放、seek、错误恢复。
  3. didPlayToEnd(...):切到 nextNode 自动续播。
  4. didReceiveObjChange(...):通过 KVO 捕捉失败态并上抛 ERROR。
  5. IOSPlayerController:把 QueuePlayerState 统一映射成 PlayState

8.5 WebViewNotifier.kt:句子高亮桥接层

文件:shared/src/commonMain/kotlin/com/tencent/qqsports/airead/webview/WebViewNotifier.kt

建议重点看:

  1. notify(...):把 ParagraphInfoDetail 序列化为 JSON 注入 JS。
  2. 两种前端协议:window.aiReadHighlight(...)CustomEvent("airead.syncSentenceInfo")
  3. evaluateJavaScript 回调 position 后的 scrollToPosition(...) 调用链。

8.6 推荐阅读顺序(30 分钟)

  1. 先读 AIReadManager.kt(抓主流程)。
  2. 再读 WebSocketClient.kt(抓数据流与重连)。
  3. 再读 Android 或 iOS 播放器其一(抓平台执行细节)。
  4. 最后看 WebViewNotifier.kt(抓 UI 联动闭环)。

9. 当前实现最关键的三个设计点

  1. 状态机收敛在 commonMain:业务语义只维护一套,避免双端状态漂移。
  2. 播放器能力下沉到平台层:Android 用 ExoPlayer、iOS 用 AVPlayer,但对上统一为 PlayerController
  3. 播放与页面联动做成闭环:不仅有音频播放,还有句子高亮与状态回传,保证“听到哪、看到哪”。

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.sh
  • xc_kt_install.py
  • konan_lldb.py
  • deleteKotlinVersionFromMaven.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

脚本实际做了三件事:

  1. 检查并安装 brew xcode-kotlin
  2. 执行 xcode-kotlin install 安装 XC 插件;
  3. 扫描 /Applications/Xcode* 并逐个执行 xcode-kotlin sync <XcodeAppPath>

对应实现可看:

  • content/posts/KMM/kotlin_debug/xc_kt_install.sh:7
  • content/posts/KMM/kotlin_debug/xc_kt_install.sh:27
  • content/posts/KMM/kotlin_debug/xc_kt_install.sh:62
  • content/posts/KMM/kotlin_debug/xc_kt_install.py:18
  • content/posts/KMM/kotlin_debug/xc_kt_install.py:54
  • content/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 *"
  • 启用 Kotlin type category;
  • 增加自定义命令:type_nametype_by_addresssymbol_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 侧最有价值):

  1. AIReadManager.starthandleWebSocketMessageplay 打断点;
  2. QueuePlayer.queuePlayerPlaydidPlayToEnddidReceiveObjChange 打断点;
  3. 命中断点后,用 LLDB 查看 Kotlin 对象摘要(脚本已接管 ObjHeader * 显示);
  4. symbol_by_name 查 Kotlin 符号,用 type_name 看地址对应类型。

建议优先断点位置:

  • shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:61
  • shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:273
  • shared/src/commonMain/kotlin/com/tencent/qqsports/airead/AIReadManager.kt:299
  • shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:141
  • shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:83
  • shared/src/iosMain/kotlin/com/tencent/qqsports/airead/queuePlayer/QueuePlayer.kt:279

10.4 常见故障与排查

  1. 插件安装总失败

    • 先确认 Xcode 关闭;
    • 再单独跑 xcode-kotlin info 看是否仍是 Plugin not installed
  2. 对象仍显示指针,没摘要

    • 确认 LLDB 已执行 command script import .../konan_lldb.py
    • 确认当前对象类型是 ObjHeader *(脚本主要接管这个类型)。
  3. 脚本跑了但结果不稳定

    • konan_lldb.py 里有缓存字典;可重新 import 脚本并重启调试会话;
    • ~/konan_lldb_log.txtKonan_Debug... 调用链是否异常。
  4. 脚本执行方式问题

    • 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:58
  • content/posts/KMM/kotlin_debug/deleteKotlinVersionFromMaven.py.py:75
  • content/posts/KMM/kotlin_debug/deleteKotlinVersionFromMaven.py.py:104

这一步只建议给维护私有 Maven 仓库的人使用,不建议作为业务排障常规动作。

10.6 Debug 原理补充(从主工程视角)

SDK 集成到 iOS 主工程后,断点能不能“进 Kotlin”,核心取决于三件事:

  1. 有没有 Kotlin 符号与调试信息

    • 集成方式如果是源码 Pod(pod 'AIRead', :git => ...:path),通常更容易拿到可追踪符号;
    • 如果是纯二进制分发,常见情况是只能做符号级/汇编级排查,源码断点能力受限。
  2. LLDB 是否加载了 Kotlin 对象可视化脚本

    • konan_lldb.py 负责把 ObjHeader * 从“裸地址”转换成可读对象摘要;
    • 没加载脚本时,经常看到的是指针地址,不是字段内容。
  3. 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:7
  • content/posts/KMM/kotlin_debug/xc_kt_install.sh:27
  • content/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 断点时的排查顺序

  1. 确认不是 Release 配置在跑(先用 Debug);
  2. 确认插件已安装且 sync 过当前 Xcode;
  3. 确认 LLDB 已 import konan_lldb.py
  4. 确认当前集成方式不是“无源码的纯二进制”;
  5. 退化方案:先在 Swift 调用点断,再用 symbol_by_name + 地址级别排查。

10.8 推荐的主工程调试基线

  • 每次 Xcode 升级后重新执行一次 xcode-kotlin sync
  • 团队统一一份 .lldbinit(自动 import konan_lldb.py);
  • 关键链路固定断点模板:start -> handleWebSocketMessage -> queuePlayerPlay
  • 出现“对象只显示指针”时,第一优先检查 LLDB 脚本是否成功加载。

11. 当前可见的改进点(按优先级)

  1. 先补 WebView 滚动落地AndroidWebView.scrollToPosition()IOSWebView.scrollToPosition() 仍为空,导致高亮回调有数据但无真实滚动。
  2. 再修示例状态比较:Android 示例中的 AudioPlayerUiState.equals() 固定返回 false,会干扰状态驱动 UI 的判断(示例层问题)。
  3. 最后增强协议语义WebSocketClient 目前依赖 {"isEnd":true} 判定流结束,建议补充版本/消息类型字段,降低协议歧义。

12. 结论

如果从工程视角看,AIRead 的价值不在“用了 KMM”,而在“把跨端音频播报链路真正闭环”:

  • 数据侧:WebSocket 流式接收与重连;
  • 播放侧:Android ExoPlayer / iOS AVPlayer 各自落地但接口统一;
  • 业务侧:PlayState + Progress + SentenceId 回调语义一致;
  • 展示侧:WebView 句子高亮与滚动联动。

一句话总结:这是一套可接入业务、可排障、可持续演进的 KMM 语音播报 SDK,而不是演示型 Demo。