BiliPai 架构全景:构建基线、Navigation3 导航内核、播放主链路与 BAS 高级弹幕引擎解析
BiliPai 架构全景:构建基线、Navigation3 导航内核、播放主链路与 BAS 高级弹幕引擎解析
BiliPai 是一个以「原生、纯净、可扩展」为目标的 B 站客户端开源实现,本文以仓库中的 docs/wiki/ARCHITECTURE.md 为骨架,结合当前工作区与构建配置逐层拆解其架构设计:从 Gradle 模块边界、App 源码分层,到基于官方 Navigation3 的导航与整卡过渡、播放主链路,再到独立实现的 BAS 高级弹幕解析引擎。读完本文,你将掌握 BiliPai 的模块划分原则、导航状态机的工作方式、播放链路的数据流向,以及 Mode 7/Mode 9 弹幕在解析、时间线编译、Range 索引与窗口化加载上的完整实现方案。
构建与运行基线
BiliPai 的架构说明以「当前工作区与构建配置校对」为基准,最新校对日期为 2026-08-15。基线数据既是架构讨论的前提,也是所有后续模块说明的版本锚点:
| 项目 | 当前值 |
|---|---|
| 应用版本 | 当前构建与最近完整发布记录 v0.2.3-beta.7 / versionCode 296 |
| Android | minSdk 26、targetSdk 35、compileSdk 37、arm64-v8a |
| 工具链 | AGP 9.3.1、Gradle 9.5、Kotlin 2.4、JDK 21 |
| Compose | BOM 2026.06.00、Material3 1.5.0-alpha25、Lifecycle 2.11.0 |
| 导航 | Navigation3 runtime/UI 1.2.0-alpha07、NavigationEvent 1.2.0-alpha03 |
| 媒体 | Media3 1.10.1、DASH/HLS、MediaSession、Texture surface 连续返回 |
| 视觉 | Miuix 0.9.4-4f86de92-SNAPSHOT、Haze 2.0.0-alpha03、Miuix Backdrop / Liquid Glass、Compose Cupertino |
几个值得注意的技术取向:
- minSdk 26 / arm64-v8a:最低支持 Android 8.0,且当前原生库(如 dolby-ffmpeg-decoder 中的
.so)只面向 arm64-v8a 架构,这与绝大多数现代设备的 ABI 一致; - 导航激进跟随官方:Navigation3 runtime/UI 采用
1.2.0-alpha07,而 Miuix 组件库虽然也提供 NavDisplay 能力,项目明确「不混用其 NavDisplay」,Navigation3 runtime/UI 必须保持官方同版,避免第三方实现与官方语义漂移; - 视觉栈分层:Miuix 提供组件与视觉、Haze 提供模糊、Miuix Backdrop / Liquid Glass 提供背景材质、Compose Cupertino 提供 iOS 风格 facade,三者共同构成三风格 UI 的技术底座。
Gradle 模块划分
架构文档给出了核心模块树:
app
├── design-system
├── settings-core
├── network-core
└── plugin-sdk
baselineprofile ──(benchmark target)──> app
结合仓库根目录的 settings.gradle.kts 可以看到实际注册的完整模块还包括 app-tv、core-data、core-player、danmaku-engine、design-tokens、brand-motion、dolby-ffmpeg-decoder、miuix-navigation、ci-lint 等,说明架构文档聚焦的是主 App 与支撑模块这条主线。各模块职责与边界如下:
| 模块 | 职责 | 当前边界 |
|---|---|---|
app/ |
Application、Activity、业务 UI、导航、播放器、Repository、UseCase 与应用测试 | 绝大多数产品逻辑仍在此模块,新增代码优先沿既有 feature/core 分层 |
design-system/ |
MD3/Miuix/iOS 主题、语义 token、组件 facade、动效、模糊预算与自适应策略 | 不承载网络、业务状态或页面 ViewModel |
settings-core/ |
可复用播放速度等设置策略 | 只放跨页面、可独立测试的偏好域逻辑 |
network-core/ |
网络 fallback 与首页推荐匿名化策略 | 不依赖 feature UI |
plugin-sdk/ |
推荐、播放器、弹幕插件接口与能力 manifest | 作为外部插件稳定边界,不暴露 app 内部实现 |
danmaku-engine/ |
普通 Canvas 弹幕引擎、共享 XML/Protobuf/Mode 7 解析及 Mode 9 BAS 语法与时间线 | BAS 解析/求值不依赖 App;原生 BAS 视口绘制与播放器交互由 App 覆盖层承担 |
baselineprofile/ |
启动、首页、底部 Pager、设置返回、视频详情与非实时 surface 基准 | 只负责 benchmark/profile,不承载产品代码 |
模块划分的核心动机是稳定边界:plugin-sdk 面向外部插件,绝不暴露 app 内部实现;danmaku-engine 把 BAS 解析与求值从 App 中剥离,App 只负责原生 Canvas 视口绘制与播放器交互;baselineprofile 作为 benchmark target 单独存在,避免基准代码污染产品代码。settings.gradle.kts 中还通过 includeBuild("build-logic") 引入组合构建(ASM 补丁插件 ComposeDetachedOwnerGuard),且支持通过 bili.miuix.source 属性在无 GitHub Packages 凭据时切换到本地 Miuix checkout,保证确定性构建。
App 源码分层
主源码路径为 app/src/main/java/com/android/purebilibili/,架构文档给出如下分层:
| 目录 | 职责 | 典型内容 |
|---|---|---|
app/ |
应用启动与全局装配 | PureApplication、初始化、遥测、进程级 owner |
core/ |
跨业务公共能力 | network、store、database、player、plugin、theme、ui、cache、lifecycle |
data/ |
数据模型与数据访问 | API/数据库 model、Repository、加载与缓存策略 |
domain/ |
可复用业务规则 | UseCase 与不依赖 Compose 的业务决策 |
feature/ |
业务场景 | home、video、bangumi、live、dynamic、message、download、settings 等 |
navigation/ |
兼容与顶层入口 | legacy route 映射、首页 Pager、链接解析、入口/外观/播放策略 |
navigation3/ |
当前页面导航内核 | 61 个 NavKey、返回栈策略、59 个显式 Entry、Scene、预测返回和整卡会话 |
androidx/navigationevent/compose/ |
本地 NavigationEvent Compose 兼容层 | 保留完成/取消提交时序与关闭跟手预览能力;需随 NavigationEvent 版本核对 |
应用启动与全局装配的入口是 app/PureApplication.kt 中的 PureApplication(同时实现 SingletonImageLoader.Factory 与 ComponentCallbacks2),负责初始化、遥测与进程级 owner 的注册。这一分层与仓库根目录的 STRUCTURE_GUIDELINES.adoc 保持一致:依赖方向严格单向 feature -> domain -> data -> core,core 禁止依赖任何 feature/*,禁止 feature/A 直接依赖 feature/B 的 UI 组件,跨 feature 共享能力必须先下沉到 core 或 domain。命名上也有强约束:策略类统一 *Policy 后缀、页面 *Screen、状态容器 *State、ViewModel *ViewModel。
navigation3/ 是当前页面导航内核,从源码结构看,BiliPaiNavKey.kt 定义了一个继承 Miuix NavKey 的 sealed interface,包含 MainHost、Home、ListenVideo、Dynamic、SearchTrending 等数十个按键(文档口径为 61 个 NavKey);BiliPaiNavEntryProvider.kt 以 NavEntryBuilder 扩展函数的形式注册 59 个显式 Entry;BiliPaiNavDisplayHost.kt 最终落到 Miuix 的 NavDisplay 上生成 SceneState。
导航与整卡过渡
导航是 BiliPai 架构中复杂度最高的部分之一,核心流程分六步:
AppNavigation持有应用级List<BiliPaiNavKey>,业务事件通过 policy 转换成 push、replace 或 pop;BiliPaiNavEntryProvider把 NavKey 解析为 Entry,并注入来源路由、ViewModel owner 和视觉状态;BiliPaiNavDisplayHost使用官方 Navigation3 runtime/UI1.2.0-alpha07生成SceneState,统一普通返回与预测返回;- 视频入口创建不可变
VideoCardTransitionSession,冻结 bvid、来源 key/route、边界、圆角、方向与封面身份; - 整卡几何只由一个 shell/shared bounds 所有;封面、标题、UP 和统计跟随卡片,不创建竞争的独立 bounds;
- 转场时钟负责 Opening、SettledHidden、BackPreview、Returning、Restoring;详情稳态保留返回会话,但停止无收益的模糊、Miuix Backdrop 和来源重录。
其中第 4 步的「冻结」语义在源码中有非常清晰的体现:VideoCardTransitionSession.kt 是一个 click-time snapshot,注释明确写道「Feed state is intentionally not consulted after this object is created. Lists can refresh, reorder or leave composition while a detail page is open; using their mutable click globals at return time can otherwise redirect the whole-card morph to a different card」——即列表刷新、重排或退出组合都不能影响已经开始的整卡变形,会话冻结了 bvid、sourceRoute、sourceKey、卡片/封面/信息面三个矩形、圆角、方向、封面身份与宿主原点,返回时整卡落到同一坐标系(Root)下。
配合 navigation3/ 目录下的其他 policy 文件(BiliPaiNavBackStackPolicy、BiliPaiNavMotionPolicy、BiliPaiCardMorphDestinationPolicy、BiliPaiNavContentTransformPolicy 等,以及 predictiveback/MiuixVideoCardNavTransition),可以推断导航内核将「返回栈策略」「运动曲线」「卡片变形目标」「内容变换」全部拆成可独立测试的策略对象,这正是结构维护原则中「可测试决策优先抽成 Policy」的直接落地。
播放主链路
播放链路是典型的分层数据流:
Feature UI
-> screen state holder / ViewModel
-> domain UseCase / playback policy
-> data Repository
-> core network + account/auth state
-> ViewInfo / PlayUrlData / player intent
-> Media3 player + surface + overlay
几个关键约束:
- 登录态三要素:Cookie、CSRF 与 access token 共同参与鉴权;画质由用户偏好、账号权限、编码能力和 fallback 策略共同决定;
- 播放形态全覆盖:播放器覆盖普通视频、番剧、直播、离线、竖屏 feed 与听视频模式,页面必须明确 player/surface 的 owner,避免多个页面争夺同一 surface;
- 整卡返回的画面策略:视频整卡返回保留 Texture 实时画面或最后一帧;从未播放时由封面承担转场,不为动画启动无意义播放器帧——这是性能预算意识在 UI 层的体现;
- 系统级状态共享:PiP、后台播放与系统媒体控制通过 MediaSession/通知继续共享播放状态。
高级弹幕分流与 BAS
这是架构文档着墨最多、也最能体现 BiliPai 技术深度的部分。B 站弹幕体系按 mode 分流:
- Mode 7:单文本 JSON 数组,保留现有兼容解析和
AdvancedDanmakuOverlay;它不等同于 BAS 脚本; - Mode 9:BAS 脚本,由
danmaku-engine/.../parser/bas/BasScriptParser独立解析,支持def text/button/path、let对象构造/复制、默认参数模板、位置/具名实参、复合时间、转义字符串和//注释,不执行 JavaScript。
BAS 解析器是独立 Kotlin 实现。从 BasScriptParser.kt 源码看,它暴露两个入口:parse(source) 返回完整 BasProgram,以及 parseDurationMs(source) 以 timingOnly = true 模式只编译生命周期元数据——用于冷跳转时「用同一语法检查时长,不保留 SVG/文本字符串或生成可绘制对象」。解析器预置了保留字 def/let/set/then/apply/clone,按元素类型区分可变更属性集合(文本 x/y/scale/alpha/color/rotateX/rotateY/rotateZ/content/fontSize,按钮 x/y/text/fontSize,路径 x/y/alpha),未知/不可变的 set 属性被忽略。
时间线语义由 BasTimeline.kt 承载:BasTimeline 预编译各属性轨道,递归串行组取时长之和、并行组取最大值;空 set 提供延时,文本/字号在 set 开始时改变;重叠同属性及变换通道按源码后声明优先,保留无冲突的透明度/颜色动画。位置保留像素/百分比单位,到帧布局时按视口或父文本尺寸求值,字号百分比基于视口宽度;播放进度可直接前进/倒退求值,无需重放动画历史;父层级生命周期结束后隐藏其子树。
架构文档给出了一份已执行的模板与串并行样例:
def text T(c="default" x=0) { content=c x=x }
let a=T("模板", x=50%,)
let b=(a {content="复制" x=100})
{ set a {alpha=0} 2s set b {} 1s then set b {y=50%} 3s }
then set a {rotateY=90} 1s
其中 let b=(a {...}) 展示对象复制、{...} then ... 展示串并行组、set a {rotateY=90} 展示三轴旋转变换。
数据通路:DanmakuParser 的 ParsedDanmaku.basList 与 DanmakuManager.basDanmakuFlow 将脚本场景送到普通视频的 BasDanmakuOverlay。这一点在源码中得到印证: DanmakuParser.kt 对 Protobuf 与 XML 两条路径都产出 basList;DanmakuManager.kt 以 StateFlow<List<BasDanmaku>> 暴露 basDanmakuFlow;BasDanmakuOverlay.kt 与 AdvancedDanmakuOverlay.kt 则分别在 VideoPlayerSection.kt 中被挂载。文本、按钮、路径使用保留的原生 Canvas 场景;SVG 使用 AndroidX PathParser,viewBox 按等比居中映射,支持锚点、三轴旋转、层级透明度及描边。
内存与性能设计是 BAS 模块的亮点:
- 按 token 词法扫描:数字词法直接扫描原字符串的当前 token,不对完整脚本反复创建正则匹配器。文档特别指出 Android ICU 的
MatcherState::updateInput会复制完整输入到原生堆,即使 Java 堆与播放窗口不大,这类按 token 的全输入复制仍可能触发系统低内存杀进程——这是选择手写词法扫描而非正则匹配的根因; - Range 索引读:在线特殊分片由
SpecialDanmakuIndexReader用最多 1 KiB 的 HTTP Range 头部读取建立偏移/开始时间索引,跳过脚本文本;从源码看,SpecialDanmakuIndexReader.kt 是单消费者迭代器,通过推进 offset 跳过 length-delimited 与 fixed-width 字段,而不是下载其字节,只保留一个有界 Range 缓冲,next()返回SpecialDanmakuEntry(offset, byteLength, startTimeMs, mode); - 窗口化加载:SpecialDanmakuWindow.kt 对会话只保留紧凑的偏移/生命周期元数据,对播放窗口才编译场景;窗口最多 4 个载荷并发,只保留当前位置仍有效及未来 3 秒(
lookAheadMs = 3000)的场景;不缓存完整线上分片,也不把整段视频写入磁盘缓存;索引逐片就绪即可接入窗口,不依赖文件名或分片的时间顺序; - 冷跳转语义:Protobuf 没有结束时间,冷跳转复用已知生命周期,此前未知的 BAS 用
parseDurationMs检查时长,检查过期载荷后立即丢弃,只将重叠场景完整编译;冷跳转仍可能需要读取此前未知的脚本,不能用固定回看秒数丢掉长持续弹幕——因此parseDurationMs的 timingOnly 编译路径是冷跳转的关键优化。
交互与屏蔽:BAS 特殊/彩色/权重/关键词/用户屏蔽复用应用设置;关键词匹配定义与后续动画文本而非标识符;Mode 9 插件接收原始脚本、改写后重新解析,无效脚本丢弃、不回退滚动显示源码。按钮仅支持 seek、av/BV、bangumi 类型:seek 使用当前播放器;其他目标暂停播放后在外部浏览器打开完整分 P/时间链接,避免现有 App 深链入口丢失这些参数;未命中按钮时不消费播放器手势。apply、clone、成员/算术表达式、块注释与单引号不是此可执行 BAS 契约的一部分,解析失败携带行列信息。
验证数据(均为文档记录的实测,非推断):真实载荷 BV1Wneiz5EpD 的 JVM 冒烟在 -Xmx192m 下通过——17 个分片、735 条索引、原始 265.74 MiB,索引实际读取约 0.71 MiB;检查了 0.1 秒、6.1 秒、141.29 秒及回退窗口,相同暂停位置不改变场景或重复读取载荷。Android 36 模拟器已完成该视频的 BAS 实际显示、暂停保持与前后跳转冒烟:暂停在 104.531 秒时两次截图的 1080×607 视口像素完全一致;前跳约 140 秒、回退约 65 秒后重新加载相应窗口并显示场景,同一进程也经历了 3 倍速播放。数字扫描修复后,三次整个调试 App 的 RSS 采样约 565/582/564 MiB,末次 PSS 约 433 MiB、Java 堆已分配约 40 MiB、Native Heap RSS 约 90 MiB、Swap PSS 约 0.28 MiB,未再次观察到 OOM。文档同时强调这些是采样值而非峰值或 BAS 独占内存,且 JVM 结果不代表 Android 原生绘制的像素或内存。引擎 76 项回归、手机调试 APK 构建及 TV Kotlin 编译通过;字体/透视/按钮手势与其他设备仍不在完整视觉验收范围。BAS 语法与求值属独立 Kotlin 实现(文档注明未复制未标明许可的官方 JavaScript,官方仓库仅作语法依据,PiliPlus 2.1.5 的锁定 canvas_danmaku 只提供 Mode 7 参考)。
视觉与自适应
design-system 提供 MD3、Miuix 与 iOS 三套 facade,feature 层只消费语义 token 和能力接口——从架构文档看,正式 UI 设计合同、三风格边界、组件入口和页面档案由独立的 UI 设计规范文档说明,本页不重复设计规则。技术侧的关键约束:
- Haze、Miuix blur 与 Miuix Backdrop 都受平台能力、运行时视觉预算和转场安全门控三重约束;
- 手机使用底栏/单栏为主;平板和折叠屏使用 rail、双栏或影院布局;
- 液态玻璃复用遵循 sibling/combined backdrop 拓扑,避免控件采样自身造成黑边或 RenderThread 问题。
这套视觉架构与 design-system/ 模块的「不承载网络、业务状态或页面 ViewModel」边界相呼应,模糊预算与自适应策略被当作纯视觉能力管理。
插件边界
插件是 BiliPai「可扩展」定位的核心,当前按形态分级:
| 形态 | 当前能力 |
|---|---|
| 内置插件 | PureApplication 注册 10 个实现,可接入推荐、播放器、弹幕与投屏链路 |
JSON / .bp |
URL 导入、规则预览、启停与本地执行 |
外部 .bpplugin |
manifest、SHA-256、签名状态、能力声明和包预览;外部 Dex 执行仍未正式开放 |
| 源码示例/皮肤 | plugins/samples/ 提供源码插件和数据型皮肤包示例 |
架构文档特别强调了一条安全边界:外部执行正式化前必须先定义签名信任、能力授权、隔离、版本兼容与失败回滚,不能由 UI 预览状态推导为「可安全执行」。这解释了为何 plugin-sdk 只暴露「推荐、播放器、弹幕插件接口与能力 manifest」作为稳定边界,而不暴露 app 内部实现。
测试与性能
- App policy/structure tests:
app/src/test/; - Design system、network、settings、plugin SDK 各模块拥有独立纯 Kotlin 测试;
baselineprofile/覆盖 Startup、FrameTiming、视频详情、首页、底部 Pager 和设置返回;- 性能相关改动按「目标测试 →
:app:compileDebugKotlin→ Macrobenchmark/真机」逐级验证; - 当前 AGP 9 迁移后的 app 单元测试注解解析仍是路线图 P0;恢复前不得把生产编译成功等同于测试全绿——这是对 CI 可信度的一条重要纪律。
结构维护原则
架构文档给出六条可执行的原则,与 STRUCTURE_GUIDELINES.adoc 的目录归属规则互为补充:
- screen composable 消费不可变状态与事件 lambda,ViewModel 不向叶子组件传播;
- 可测试决策优先抽成
Policy/UseCase;业务 I/O 不放入 Composable; - 跨 feature 的视觉能力先进入
design-system,业务公共能力进入core/独立模块; - 新模块只在边界稳定且能减少反向依赖时建立,不以模块数量代替架构质量;
- 更完整的目录归属与依赖规则见
STRUCTURE_GUIDELINES.adoc。
事实入口
架构文档将各类事实的权威来源收敛如下,便于按需核对:
- 当前优先级:docs/wiki/ROADMAP.md
- 版本与依赖:
app/build.gradle.kts、根build.gradle.kts、Gradle wrapper - 发布历史:
CHANGELOG.md - 功能状态:docs/wiki/FEATURE_MATRIX.md
- 回归标准:docs/wiki/QA.md
整体来看,BiliPai 的架构呈现出「官方优先、边界严格、性能预算贯穿始终」的特征:导航紧跟官方 Navigation3 而不混用第三方 NavDisplay,弹幕引擎独立实现 BAS 但公开声明契约边界,视觉层以语义 token 隔离三套风格,插件能力以 manifest 和签名体系渐进开放。对于想深入 Android 大型 Compose 应用架构、Navigation3 整卡转场或 BAS 弹幕引擎实现的开发者,这份架构说明与对应源码目录(navigation3/、danmaku-engine/、design-system/、plugin-sdk/)是绝佳的阅读起点。