BiliPai 架构全景:构建基线、Navigation3 导航内核、播放主链路与 BAS 高级弹幕引擎解析

原创2026-10-14 13:57:311,353 阅读
文章标签:移动开发音视频插件系统

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 架构中复杂度最高的部分之一,核心流程分六步:

  1. AppNavigation 持有应用级 List<BiliPaiNavKey>,业务事件通过 policy 转换成 push、replace 或 pop;
  2. BiliPaiNavEntryProvider 把 NavKey 解析为 Entry,并注入来源路由、ViewModel owner 和视觉状态;
  3. BiliPaiNavDisplayHost 使用官方 Navigation3 runtime/UI 1.2.0-alpha07 生成 SceneState,统一普通返回与预测返回;
  4. 视频入口创建不可变 VideoCardTransitionSession,冻结 bvid、来源 key/route、边界、圆角、方向与封面身份;
  5. 整卡几何只由一个 shell/shared bounds 所有;封面、标题、UP 和统计跟随卡片,不创建竞争的独立 bounds;
  6. 转场时钟负责 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。

事实入口

架构文档将各类事实的权威来源收敛如下,便于按需核对:

整体来看,BiliPai 的架构呈现出「官方优先、边界严格、性能预算贯穿始终」的特征:导航紧跟官方 Navigation3 而不混用第三方 NavDisplay,弹幕引擎独立实现 BAS 但公开声明契约边界,视觉层以语义 token 隔离三套风格,插件能力以 manifest 和签名体系渐进开放。对于想深入 Android 大型 Compose 应用架构、Navigation3 整卡转场或 BAS 弹幕引擎实现的开发者,这份架构说明与对应源码目录(navigation3/、danmaku-engine/、design-system/、plugin-sdk/)是绝佳的阅读起点。

登录后查看全文
BiliPai