人物采访 / T SALON

编辑内容

卡比谈 B 站移动架构:模块化、Monorepo 与持续演进

整理 T Chat 第 3 期三段录播:卡比从模块化的实际困难,谈到 Monorepo、接口生成、声明式 UI,再回答技术选型、架构师成长、重构、跨平台、单元测试、低代码、工具与招聘问题。

T Chat 第 3 期卡比分享 B 站移动架构与架构师成长的访谈封面
T Chat 第 3 期卡比分享 B 站移动架构与架构师成长的访谈封面

本期视频发布于 2022 年。文中的技术状态、个人经历与观点均为当时情况。

在 T Chat 第 3 期,卡比从几种常见的工程现象切入,讨论自己在 B 站做移动架构的实践。他在当期介绍中说,2014 年加入 B 站,从 iOS 开发出发,做过业务、播放器及技术架构,也持续接触 Android、Web 和服务端。

这次讨论贯穿着一个问题:当业务、代码和参与者不断增加,架构怎样帮助大家持续交付?他的回答既涉及仓库和接口,也涉及开发者愿不愿意使用一套方案、改造工作如何获得资源,以及怎样评价一项设计在后续变化中的表现。

技术分享:从真实使用方式设计架构

模块化为什么会偏离预期

卡比用两个极端让抽象原则落地。网络库、日志库这样的底层能力范围相对明确,容易保持与业务的距离,却可能要求调用者自行组装很多步骤;上层业务模块为了让别人“一句调用就能用”,又容易把上下文查找、页面跳转和展示逻辑都包进去。架构师当然能画出更整齐的边界,但赶交付的同学还要面对实际沟通和接入成本。若新方案只增加使用负担,使用者很可能绕回熟悉的写法。

他把青少年模式弹窗代码里的 static 展示入口当作信号:调用方没有传入当前页面或导航上下文,模块内部便只好自己去找全局窗口。方便并非虚假的收益,代价则是测试和复用时难以预测它究竟会把弹窗放到哪里。他甚至用养猫作比喻提醒设计者:不能仅凭“正确的模块化应该怎样”要求所有业务同学改变行为,要给他们在真实压力下仍愿意使用的接口。

卡比先讨论高内聚、低耦合这些熟悉的词,在实际项目里怎样变得复杂。底层工具、公共组件和上层业务面对的使用方式不同,开发者对它们的熟悉程度、交付时间与沟通成本也不同。若只用一组抽象指标要求所有模块,很容易忽略调用者真正怎样工作。

他观察到两类常见困难:拆分得很细的能力,可能需要使用者承担较多组装工作;内部包办了所有事情的模块,调用起来虽然省事,却可能把依赖和变化集中在一起。这个讨论旨在解释工程取舍,并不是重新定义内聚与耦合,也不是断言工具库天然应该低内聚。

他又用复杂度与人的理解能力之间的差距说明问题:系统关系不断增加时,个人很难始终在头脑中掌握所有状态。这里的曲线是帮助理解复杂度的比喻,不是对项目缺陷率的数学预测。

青少年模式弹窗是一个具体例子。它可能出现在多个页面,本来需要考虑调用上下文、导航和视图层级;实际实现却可能提供一个很简单的静态入口,把寻找全局窗口、选择展示位置等工作全部包进模块内部。使用者省事了,隐藏依赖却更多。卡比认为,架构设计需要理解这种减少沟通、尽快完成任务的动机,单靠命令大家把模块拆得更干净,未必能改变实际结果。

自动化、可预测与声明式表达

他把“说出想要什么”与“逐步说怎样做”并列,是为了降低状态变化时的推理难度。某个页面的数据变了,开发者先描述新状态下界面应该是什么样,再让框架组织更新;如果每处 UI 变化都由不同业务代码手动推进,顺序和遗漏就容易成为额外问题。声明式并不消除状态管理,它反而要求团队把状态和输入组织清楚。

为回应这些困难,他介绍了自己的一组原则:尽量自动化重复工作,谨慎增加中间层,并追求可预测的结果。确定的输入应尽可能对应清楚的输出,这也便于推理、测试和协作。他借自己喜欢的构建系统谈可重复、可预测的工作方式,重点在于减少额外的不确定性。

声明式编程是另一条线索。与逐步描述怎么操作相比,它更强调描述希望得到什么状态。卡比把它放在 React、SwiftUI、Compose 等 UI 技术的共同变化中理解,随后用内部实践展开。关于这一思路,也可参见 Android 官方声明式 UI 说明。

Monorepo 服务于多业务共同演进

卡比给出了规模背景:多个业务 App、大量编译单元、公共库与跨平台音视频能力都要共同迭代。在这种结构里,基础库修改可能牵动很多调用方;统一源码让负责人有机会在一次提交中连同调用代码一起改。若分散在多个二进制仓库,先发库版本、再让各业务逐个升级,沟通和发版时序都会变成成本。他也承认代价:要拉取更多代码、学习新的构建工具,所以小项目仍可走组件分发路径。

第一个实践是 Monorepo。卡比说明,B 站的工程并不只对应一个视频 App,还涉及直播、电商、剪辑等业务,以及公共库与跨平台能力。大量项目共享基础设施,又有真实的业务调用关系,这构成了集中管理源码的背景。

他特别质疑一种机械处理:业务本来需要相互协作,却先为了模块隔离禁止直接联系,再引入路由、总线等中间机制把调用接回来。他的主张是审视这些边界究竟解决什么问题,避免为满足形式而持续增加成本。这个例子不能推导成所有解耦机制都多余,而是要求设计与真实依赖关系相符。

当时的工程体系也并非只有一种分发方式。除了主项目的 Monorepo,仍保留中心化的二进制组件仓库,让不适合接入整套代码的小型 App 使用公共能力。集中源码与组件分发,在这里承担不同职责。

用一份定义减少前后端重复转换

他先复述了旧做法里的协作痛点:双方商量 JSON 字段,随后各自决定字段放在路径、请求体还是其他位置,再分别写请求和响应转换。BAPI 把服务、请求与响应放进集中管理的 Protobuf 定义,由生成器给 Kotlin、Swift 的调用端和 Go 的服务端生成对应入口。一份定义减少了结构性重复,却不替双方决定“一键三连”在业务上何时成功、错误怎样反馈。

第二个实践围绕客户端与服务端通信。卡比把需要反复约定字段、请求参数和返回结构的问题,收敛到一份集中管理的接口定义。他介绍的 BAPI 方案参考了 Google API 的思路,用 Protobuf 描述服务、请求和响应,并生成不同端的调用代码与服务端入口。

原片用一键三连接口作例子,展示同一份定义怎样连接 Kotlin、Swift 与 Go 等端的实现。这样可以减少手写数据转换及两端理解不一致的机会,但业务含义、错误处理和演进约定仍需要团队共同设计,不能理解为协议生成后就不再需要沟通。

Protobuf 官方资料也说明了从 .proto 定义生成多种语言代码的基本机制;内部接口体系如何组织,则以嘉宾当时的介绍为准。Protocol Buffers 官方概述

将语言、UI、状态与依赖管理配合起来

卡比展示的例子中,页面声明一个按钮等布局,也声明需要的服务;Activity 作为页面入口提供服务,内部节点再取得相应依赖。这样业务节点不必到处寻找全局对象,也便于测试时换入受控实现。Android 侧谈到 Kotlin、Compose、ViewModel 和依赖注入,iOS 侧则用内部方案对齐这些思想;他明确说当时不能直接照搬 SwiftUI 的使用条件,因此两端追求的是设计关系一致,不是源码完全相同。

第三个实践是当时正在推进的一组移动端方案:现代语言、声明式 UI、MVI(Model–View–Intent)与依赖注入。卡比以 Android 的 Kotlin、Compose 和 ViewModel 等能力为例,介绍它们怎样配合;iOS 则用内部实现对齐类似的设计思想,以适应当时的部署环境和需求。

在展示的结构里,页面以声明式方式描述布局,需要的服务由依赖管理提供。团队没有把所有导航和页面组织一次性重做,仍保留开发者熟悉的 Activity、Fragment 等结构,再让子节点按约定取得相关服务。这个选择体现了推进范围的控制:改进核心问题,同时保留仍然适合的工作方式。

架构有生命周期

分享最后,卡比用人体中相互协作的系统作比喻,解释自己对架构工作的兴趣:让不同部分能够处理自己的职责,又能配合起来实现业务目标。他也提醒,架构不能期待一次设计永久适用,应允许它演进,甚至被更合适的方案替换。

对谈上:选型、成长与改造的条件

技术选型需要先有原则

主持人追问他为什么改变推动方式,卡比回答得很直接:同一句“请用这套方案”,在早期像建议,职位提高以后就可能被听成命令。停止强推之后,他发现有些方案确实还不够好。愿意使用的人会留下可观察的收益;没人使用,也可以成为重新检查接口和迁移成本的证据。这个回答补足了“技术选型要先进”后面的限制:新工具必须在具体工作中证明自己。

谈到 B 站的技术选择,卡比先强调面向后续演进。他不希望每个内部模块为了历史接口无限累积兼容负担,而是希望利用统一源码和自动迁移等条件,在可控制的范围里一起修改调用方。这里谈的是能统一推动改动的内部工程,不能直接推广为对外接口或已发布客户端都可以放弃兼容。

其他原则包括关注新的工具链,以及在确有需要时敢于自研。他以语言演进和播放器能力为例,说明团队怎样接触新技术、解决自己必须面对的问题;不过好的外部方案仍然可以使用,自研本身不是目的。

至于一套方案如何沉淀,他回顾了自己从强推到减少强推的变化。职级和影响力增加后,个人建议更容易被当作命令;如果只是靠权力推动,反而不容易发现方案哪里没有做好。愿意承认选择不合适、让实际使用反馈进入改进过程,是他在这个问题里强调的变化。

架构师怎样保持判断力

主持人问“怎样成为移动架构师”,卡比先声明这问题太大,只能说自己的做法。他特别强调持续写代码:不一定争着接业务需求,但可以照同一份需求文档自己做一个实现,再看负责同学怎么做;如果两者差得很大,就有新的问题可讨论。这样设计者仍会亲身遇到 API 不顺、依赖复杂或调试困难,而不会只从评审会上听到“方案已完成”。

卡比首先讲持续写代码。即便工作安排让自己不再直接承担某个需求,也可以针对同一个问题写出一份实现,再和实际方案比较。这能帮助设计者保持对成本、细节与实现困难的感知,而不是只停留在图和文档上。

其次是接触上下游与相邻技术栈。理解 Android、iOS、Web 和服务端中的不同解法,能够看到共同思想,也更容易比较方案。持续阅读、接受外部输入之后,还需要形成自己的原则:听取意见,最后根据具体问题作判断。

主持人追问成长阶段时,卡比说明这些行为并非获得架构师头衔之后才开始。做业务时也可以持续编码、阅读和尝试,只是关注范围随着责任扩大。实践量有价值,却没有一个做满多少小时就保证成为专家的数字门槛。

从其他平台理解自己的平台

主持人先问技术栈单一的人怎么办,卡比提出从 Web 出发,是因为 Web 上的状态管理、组件组合和声明式 UI 较早积累了大量讨论;移动端可以借它提问,再看自己的平台如何实现。下半场有位主要写 Objective-C 的观众把问题变得更具体,卡比便改从相邻的 Android 开始:同样是列表,iOS 的 delegate、data source 与 Android 的 adapter 各自怎样分工?依赖注入在两边怎样落地?比较后先做一项能让自己项目受益的小改动,比直接宣称“我要转架构”更容易获得团队信任。

对于技术栈单一、想扩展视野的同学,卡比建议关注 Web 生态和相邻移动平台怎样解决类似问题。例如状态管理、声明式 UI、页面组织,以及浏览器与客户端的执行和渲染机制,都可以成为比较对象。他借此鼓励从已有经验出发寻找联系,并没有要求一次学完所有平台,也不能将平台间的类比当作实现完全相同。

这个问题在下半场又被具体追问。面对一位主要写 Objective-C 的观众,卡比建议先看看 Android 中与列表、数据适配、状态和依赖管理有关的做法,再判断哪些思路能够改善自己项目中的问题。通过一个可解释、可验证的改进取得经验,再逐渐接触更大的接口和业务架构,比只追求架构师这个名称更具体。

Monorepo 的起点是交付困难

他用当年的发版夜晚说明痛点有多具体:各模块二进制交付,常常很晚才能组合出一个可运行的包,随后才开始排查版本组合与运行问题,团队长期到凌晨才下班。Monorepo 因而不是先有流行架构再找应用场景,而是针对跨模块联调和交付耗时作出的选择。彼时源码规模仍在可迁移范围,也是能推动变更的条件之一。

回顾推进过程,卡比把起点放在 2018 年前后的一类具体痛苦上:不同模块以二进制交付,赶工时版本组合、运行问题和排查工作集中到发版阶段。据他的描述,团队常常到很晚才得到可继续验证的包,发布与调试的压力已经需要改变。

为什么当时能转向 Monorepo,他的解释包括时机、可控制的代码范围,以及团队有能力完成迁移。过程中并非一路顺利,仓库组织也经历过调整;他提到后端后来有不同的仓库选择,主项目与小型 App 也没有全部采用同一形式。这段经历不支持整个公司从此只用一种仓库模式的说法。

代价同样存在:需要获取更多代码,适应新的构建工具和工作方式。他认为值得的部分,是公共能力复用、包体控制和跨模块同步修改更容易组织。在一次变更中连同调用方一起调整,可以减少多仓库之间反复协调的成本。但这些收益依赖配套工程能力,Monorepo 不是把文件放在一起就会自动生效的答案。

重构怎样与业务和团队配合

他用城市下水道作比喻,不是建议把脏代码永久藏起来,而是说需求高压时要先有可控的承接区域,之后再集中整理。若要求每个新需求都当场完成全面重构,业务和研发很可能谁也做不到;若放任临时代码到处分散,未来更难清理。有了明确边界,团队才有机会安排一次改造,并在改造中与产品讨论哪些旧功能仍值得保留。

主持人随后问解决历史问题的人怎样得到认可。卡比认为,敢接高风险重构的人既要动作快、判断准,也应得到与风险相称的评价;如果所有可见成绩都给新功能,没人愿意负责长期维护。这里的“奖励”不是只看重构规模,而要和前面谈的预期、实际变化以及业务收益放在一起衡量。

卡比把重构理解为降低系统理解和变化成本的过程。他用化简复杂关系作比喻,又谈到在日常业务中保留可以集中整理的边界与空间:需求压力不会凭空消失,团队需要一种方式承接短期工作,再有组织地处理累积的问题。

重构也可能成为产品梳理的机会。清理无人使用或难以维护的逻辑时,可以和产品一起问,为什么这个功能变成现在这样、是否还需要、怎样调整更合适。他还强调解决历史问题的人应获得认可,不能只有推出新功能的人得到成果,维护和改造长期被当作没有价值的附带工作。

怎样评价一次重构

他把评价问题交还给实际变化:重构前若说希望撑过半年或一年,之后就看这段时间里有多少人改过、改动是否属于预计的类型,以及是否总得碰核心代码。有人修改并非失败,关键是方案能不能容纳业务自然增长。主持人进一步追问投入与收益,卡比承认还要和业务负责人讨论成本、稳定性及资源;架构上的“更优雅”不足以单独决定一项改造是否值得做。

对重构后可能仍然复杂的追问,他建议在开始前说清预期,例如希望应对哪段时间、哪些已知类型的变化。后续再看改造区域实际发生了什么:变化是否在预期之内,扩展是否还需要反复修改核心,原来的假设是否成立。

这不意味着改动越少就一定越好,而是把设计和真实演进放在一起检查。投入多久、服务多久,以及对业务和稳定性的收益,也需要与负责人讨论。卡比随后再次用统一源码下同时修改基础库与调用方的能力,解释它怎样支持持续迭代。

工作氛围的现场侧面

上半场还聊到 B 站的日常氛围。卡比举了家庭日、周年活动、生日会、公司里的猫,以及游戏兴趣交流和程序员活动等例子;技术相关活动也有解谜和奖励。这些是他在 2022 年看到的情况,具体活动和福利可能随时间变化。

对谈下:框架、测试与开发者的责任

UIX 是什么,不是什么

这个澄清来自主持人的提问:谈跨平台 UI 时,是否可以把 UIX 算进去?卡比立即把话题拉回它的定位。框架只是 iOS 端对齐声明式 UI、单向状态流和服务依赖的一层结构,很多绘制与交互仍借助已有 UIKit 和第三方能力。他还回应直播间的开源问题:当时更愿意等思路稳定后写文章,让别的团队按自身条件实现,而没有给出发布源码的承诺。

主持人提到内部 UIX 时,卡比先澄清,它并不是把同一套 UI 直接运行到多个平台的跨平台框架,而是在 iOS 上对齐一组声明式 UI、状态与依赖管理思路的内部方案,用来适应当时的系统支持和业务要求。

他解释,框架本身看起来较小,是因为复用了许多已有能力,不等于所有底层功能都只由很少代码完成。当时他没有给出开源计划,更倾向于在方案足够成熟时分享思路,让读者结合自己的环境实现。文章因此不提供未经确认的 UIX 下载或开源地址。

跨平台 UI 的收益与额外成本

他列举 B 站当时不同业务的选择:漫画有 Flutter 的实践,电商有小程序类方案,PC 客户端使用 Electron。它们各自回应不同产品需求,因此他拒绝用一个框架统治所有端。主持人顺势说技术开放似乎意味着业务环境宽松,卡比加了限定:管理者愿意给团队选择空间,前提是方案能带来交付和体验上的结果;开放本身不能替业务承担接入和维护成本。

卡比介绍,当时不同业务使用过 Flutter、小程序类方案和 Electron 等工具。这些选择可以帮助常见场景更快获得一致体验,减少部分重复工作;需要深入定制或连接原生能力时,则可能增加插件、渲染和集成方面的投入。

卡比用“上限、下限”概括这些取舍,关注的是常见场景的开发成本与深度定制成本。不同运行时、渲染路径和业务负载仍需分别考察,不能据此断言所有跨平台框架都慢于原生。

技术开放还需要与业务结果相互支持。卡比回应主持人的判断时强调,团队给予选择空间,技术方案也应回到交付价值上证明自己,不能只把使用新工具当作目标。随后提到的校招与资深岗位需求,均是当时现场信息,不代表今天仍有相同岗位开放。

共享逻辑的语言选择

关于跨平台语言,他谈到自己对 C++ 使用复杂度的感受,并表达了对 Kotlin Multiplatform Mobile、Rust 等用于共享逻辑的兴趣。这个回答讨论的是未来可探索的方向,不是宣布 B 站已经完成相应迁移。语言偏好也不能替代对现有依赖、平台支持与维护能力的具体判断。

测试要让开发者获得实际反馈

他举自己维护框架时接到使用者报错的场景:如果框架已有针对契约的测试,先跑一遍就能较快判断是否由最近的框架改动引入,再决定下一步排查;若没有测试,双方只能先互相猜测。另一方面,早期自动生成的大量后端测试案例并未有效抓到关键逻辑问题,所以单靠覆盖率数字要求每个业务同样投入,也可能把时间花在不能帮助定位的案例上。

谈到单元测试,卡比反对把统一覆盖率指标作为推动所有业务测试的主要手段。他回忆过自动生成测试案例却没有有效发现关键逻辑问题的经历,借此说明覆盖数字并不等于测试质量。

他更希望从模块结构上降低写测试的成本。单向数据流、清晰的输入输出和依赖关系,会让某些状态变化更容易验证。开发者发现测试能减少手工运行、辅助定位问题,就更可能继续使用它。

这段还有一个不能省略的限定:他同时说,UIX 框架自身当时采用了很高、以 100% 为目标的测试覆盖要求,用来增强维护者对修改的信心。因此他的观点并非所有场景都不要覆盖率,而是区分框架、业务与测试目的。已有测试可以提高排查把握,也不能据此宣称测试通过就证明自己绝无缺陷,或无需其他层面的验证。

低代码先圈定使用者和问题范围

主持人问低代码时,卡比先拿 Photoshop 之类的图形工具作类比:把复杂操作收进可见界面,本就一直是降低使用门槛的方式。回到具体项目,运营同学需要的也许只是挑活动模板、改内容顺序;若为他们搭出可自定义任意布局、动效和样式的系统,反而把产品做成了另一个专业开发平台。程序员面对的是另一种“少写代码”:用语言特性减少重复声明,同时保留必要的表达能力。使用者不同,工具范围也应不同。

卡比对低代码这个名称持保留态度,但认可降低使用门槛与减少重复操作的价值。他用图形工具和高级语言作类比,说明把复杂细节藏在合适的工具背后,并不是突然出现的新需求。真正要判断的是谁在使用、需要完成什么,以及它节省了哪一部分工作。

对程序员,可以通过 Kotlin DSL、Swift result builder 等语言能力,让同样的界面或意图用更清楚、更少重复的代码表达。对运营人员,则可能提供预先定义的活动模板与 CMS,让其配置内容和顺序。若需求只是排序与组合,就不必一开始加入任意布局、样式和动画能力。

他也介绍了前端在可视化搭建方面的探索,并说明 Native 当时没有同样的全面搭建计划。不同受众对应不同范围,避免过度设计,是这段讨论最明确的实践要求。他举的代码量和交付时间例子说明想达到的收益,并非所有团队都能达到的固定效率。

工具从重复工作里长出来

当主持人要他推荐提效工具时,他反倒说自己常用命令行,现成产品里主要使用 Notion 整理知识。真正的开发工具往往从自己的重复工作中长出来:看到构建结果、包尺寸或内存信息反复需要人工分析,就写脚本或小程序固定步骤,再让同事试用。如果有人反复使用并反馈问题,这个工具就不仅是一段一次性命令,而成了要继续维护的内部产品。

在提效工具的问题上,卡比没有给出很长的产品清单。他当时会用 Notion 管理知识和安排,也强调为自己的工作保留专注时间。开发工作里,他更倾向于观察重复操作,再写成工具,例如分析构建结果、尺寸或内存相关信息。

工具本身也可以当作一个产品:先解决真实问题,再让其他人用得上。编写过程中既能加深技术理解,也能得到使用者的反馈。重复工作出现几次后,就值得问能否自动化;是否要开发工具,还要看这项工作以后会不会继续出现。

招聘关注好奇心、深入能力与持续负责

最后的问题回到移动开发者本身。卡比首先看重愿意折腾问题、寻找文档和学习的能力。能够把困惑整理成清楚的问题,再沿资料继续调查,是他认为重要的习惯;英语则可以帮助接触更多原始文档。这不意味着所有问题都已经有现成答案,仍需要理解与验证。

其次是对某个方向追求细节的热情。他举 UI 交互为例,有时早期代码未必漂亮,却已经表现出对体验的深入关注。评价一个人的工作,应同时看真正解决了什么,而不是只看代码表面的形式。

好奇心还应继续走向验证:文档告诉自己怎么使用之后,是否会追问为何这样设计,再尝试弄清实现。最后是 ownership,把接下来的事情持续推进、完成,并对交付结果负责。这组标准也与全场讨论呼应:架构的价值,要在实际使用、持续变化和团队协作中检验。

完整原片、分段信息可从第 3 期访谈页继续查看。

查看本期原视频与分段信息:我在 B 站做架构 →

主题
架构移动开发MonorepoT Chat