滕暁雲による紹介
今年は QZone 誕生 18 年目であり、QZone クライアント チームも 18 歳の誕生日の前夜にアーキテクチャのアップグレードを完了しました。従来は規格外のマルチチーム共同開発だったため、徐々にコードが劣化し、大きなリスクが生じていました。したがって、Qzone は歴史的に莫大な負債に直面し、再構築とアップグレードを選択しました。ここでは、リファクタリングのプロセスで遭遇する問題と、それを解決するためのアイデアを共有したいと思います。
目次
1 空間再構成プロジェクトの背景
2 なぜリファクタリングするのか
3 空間の構造が崩れる仕組み
4 建築の生命力
5 インクリメンタルリファクタリングの仕組み
6 アーキテクチャの拡張性と再利用性を確保する方法 7 複雑さを軽減し、長期的な制御性を維持する方法
8 劣化を防ぐには
9 パフォーマンスの最適化
10 プロジェクト再構築結果の概要
11 展望
18 年前、Qzone はオンラインになり、すぐにインターネット全体で人気を博し、多くの人々にとって青春の思い出となりました。18 年後の今日、Qzone の活力は依然として強く、多くの若いユーザーにとって選ばれるソーシャル プラットフォームです。
最も古いインターネット製品の 1 つである Qzone のコードも比較的古く、コードの動作環境は複雑で、メンテナンスコストが高く、全体的な構造のアップグレードが必要です。
01. 宇宙再構築プロジェクトの背景
このスペースはプラットフォーム型のエントランスとして、多くの兄弟企業の集客を担っており、多くのチームがスペースのコード開発に協力しています。長年にわたって蓄積された独自の機能の反復と相まって、スペースのビジネスは非常に複雑になりました。ビジネスの複雑さはアーキテクチャの複雑さをもたらし、アーキテクチャの複雑さは保守コストの増加を意味します。長年にわたり、このスペースの事業は頻繁に引き継がれ、複数のチームが引き継いできました。それが私たちのチームに引き渡されたとき、その空間のコードは言葉では言い表せないものでした。
ここでスペースのビジネス形態を簡単に紹介します。スペースの現在のメインエントランスはモバイル QQ にあり、これを複合バージョンと呼んでいます。同時に、Space App の独立したバージョンはまだ維持されています (はい、Space Independent App には依然として忠実な視聴者のグループがいます)。Space は Mobile QQ から独立した構造を持ち、統合バージョンと独立バージョンは多くの技術的およびビジネス コンポーネントを共有することがわかります。
02. なぜリファクタリングするのか?
Space は長い歴史を持つビジネスであり、コードの量は非常に多く、コードを結合したバージョンだけでも 150w 行を超えます。同時に、このスペースのコード実行環境も非常に複雑で、5 つのプロセスと 2 つのプラグインが関係しています。頻繁なハンドオーバーと複数チームの共同開発により、スペースのコードは徐々に劣化し、コード品質のほぼすべての指標がモバイル QQ の下部に表示されます。
空间的代码成了著名的原始森林 - 进得去出不来。代码的劣化导致历史 bug 难以收敛,即使一行代码不改,每个版本也会新增历史 bug30+。
面对如此庞大的历史债务,空间已经到了寸步难行,不破不立的地步,重构势在必行。所以,借着空间 UI 升级的契机,空间团队开始空间历史上最大规模的一次重构。
03、空间的架构是如何逐步劣化的?
跳出棋局,站在今天的角度回头看,可以发现空间的代码是个典型案例,很好地展示了一个干净的架构是如何逐步劣化的。
3.1 扩展性低,异化代码无处安放
结合版与独立版涉及大量的代码复用,包括组件、页面和跨 App 的复用等。但由于前期架构扩展性不高,导致异化的业务代码无处安放,开始侵入底层技术组件。底层组件代码开始受到污染。
3.2 代码未隔离且缺乏编程范式
空间是个平台型的业务,广告、会员、游戏、直播、小世界等团队都会在空间的代码里开发。由于没有做好代码隔离,各团队的代码耦合在一起,各写各的。同时由于缺乏编程范式,同一个类中的代码风格迥异。破窗效应发生,污染开始扩散。
3.3 维护成本暴增,恶性循环
空间的业务逻辑本身就很复杂,代码的劣化使其复杂度暴增,后续接手团队已有心无力,只能缝缝补补又三年,恶性循环。
最后陷入怪圈: 代码很乱但是稳定,开发道理都懂但确实不敢动。
3.4 Feeds 流的崩坏
以空间的 Feeds 流为例,最开始的架构思路是很清楚的,核心功能在基类实现,上层业务可以低成本地开发一个新的 Feeds 流页面。同时做了很多动态化和容器化的设计,来满足迭代效率。
但后续的需求迅速膨胀,异化出18种 Feeds 流场景,单 Feeds 流可能出现60多种卡片。这导致基类代码与 Feed View 中的代码迅速膨胀。同时 N 个团队在同一批代码中开发,代码行数和圈复杂度逐渐劣化。
04、架构的生命力
痛定思痛,在进行空间重构前的首件事情就是总结经验,避免重蹈覆辙。如何保证这次重构平稳落地并且避免后续每三年一重构?
我们总结了四点:
渐进式重构:高速公路换轮胎,如何平稳落地? 提高扩展性和复用性:是否能低成本迁移到其他业务,甚至是其他 App? 复杂度长期可控:n 个团队跑来做两年需求,复杂度会不会变高? 做好防劣化:劣化代码被引入,能否快速发现? |
---|
空间的重构都围绕着这四个问题来进行。
05、渐进式重构如何实现?
作为一个亿级日活的业务,空间出现线上问题很容易引起大量投诉。高速公路换轮胎,小步快跑是最合适的方式。因此,平稳落地的关键是渐进式重构,避免步子迈得太大导致工作量扩散。
要做到渐进式重构,核心是保证两点:
一个复杂的大问题能被分解为许多个小问题,可针对小问题重构和回滚; 系统随时都是可用状态。每解决一个小问题,都可以针对性的测试和上线。 |
---|
为了实现以上两点,我们基于以下几点来进行改造:
5.1 先拆解,后治理
我们并没有立即开始对旧代码进行重写,而是先基于团队的 RFW-Part 框架对老代码进行拆解。Part 自带生命周期,可以保证老代码平移前后的运行逻辑一致。
尽管代码逻辑没有翻新,但大问题被拆解为一个个小问题,我们再根据优先级对单个 Part 进行重构。保证无论重构了多少,空间都是可用状态,能立即上线验证。
RFW-Part 框架后文会有介绍,此处不做展开。
5.2 架构融合
我们彻底抛弃了空间老的技术组件,与团队内部沉淀的 RFWComponent 进行架构融合,同时也积极接入手 Q 统一的 UI 体系。保证开发能专注于业务中间层开发。
5.3 提效前置,简化运行环境
在进行业务重构前,我们先还了一部分技术债。包括去插件化、进程统一、工程结构优化和编译优化等。这些工作都在业务重构前完成并上线验证,简化了空间代码的运行环境,提升开发效率,保证了重构工作的敏捷性,达到了针对单点问题快速重构快速验证的目的。
06、如何保证架构的扩展性与复用性?
扩展性和复用性是软件工程永恒的话题。空间历史架构并没有很好处理这两点,其他业务接入时难以处理异化逻辑,使异化逻辑侵入底层代码。同时为了强行实现结合版和独立版的代码复用,使不同的场景耦合在一起,互相干扰。
为了提高架构的扩展性和复用性,我们重新设计了空间的架构层级。
6.1 业务层打薄,专注中间层
为了避免代码跨层级污染,我们对架构的分层比以往更细,隔离做得更严格。
底层技术组件基于 RFW 框架。RFW 中的组件更干净,没有任何业务侵入,能在其他 App 开箱即用。
中间层负责对 RFW 组件和手 Q 运行环境做桥接,并对底层组件进行扩展,实现一些空间相关但与具体场景无关的功能。中间层的代码能在一周之内迁移到其他 App。
6.2 业务层打薄,专注中间层
RFWComponent 是一线开发在实际业务中沉淀出的一套组件库,目前由空间和小世界团队共同维护。所有组件都经过了线上业务的验证,保证了易用性和扩展性。组件也很完整,开箱即用。
最重要的是,RFW 的核心组件都可由上层注入代理实现,这使其并不依赖于手 Q 的运行环境,也避免了业务侧逻辑入侵底层代码。
目前整套架构已在空间、小世界、频道、基础等团队深度使用。空间也是第一个使用这套架构重构老代码的业务,整个过程非常省心。
07、如何降低复杂度并长期可控?
7.1 组合代替继承,Part + Section,拆!
什么是 RFW-Part?RFW-Part 是团队内部沉淀的一套页面级的 UI 容器架构,Part 可感知页面的生命的周期,功能在内部闭环。不同 Part 无法感知对方存在,代码是严格隔离的。
但是 Part 是页面级框架,无法解决 Feeds 流列表复杂的问题,Section 架构作为 Part 的补充,主要解决列表以及 ItemView 的拆解问题。其设计思路与 Part 框架一致。
基于 Part 和 Section 架构,我们将空间的代码拆分为了一个个标准的集装箱。代码复杂度和上手难度大大降低。新人内包入职一周便可独立开发,三天就完成了新功能此刻的消息页。
7.2 使用 Part 架构重塑超级页面
空间80%的流量和功能都集中在好友动态页和个人主页两个 Feeds 流页面,尽管内部已基于 mvvm 分层,但单层内的复杂度仍然过高:
以空间的好友动态页为例,我们将页面不同功能的代码都拆分到一个个 Part 里,Fragment 仅作为一个容器,负责组装自己需要的 Part。
最终页面被拆分为27个 Part,页面代码由6000多行减少到320行。很多 Part 可以直接拿去被其他 Feeds 流页面复用。
7.3 使用 Section 框架重塑 Feeds 流
经过 Part 的改造,页面级的功能都被拆分为子模块。但 Feeds 流整体作为一个 Part,复杂度仍然过高,我们需要设计一套新的框架,对 Feeds 流中的卡片进一步拆解。
7.3.1 空间老的 Feeds 流框架
这里先介绍一下空间老的 Feeds 流框架 - Ditto。
Ditto 框架魔改了 Android 原生的布局体系。其将一个卡片按位置分为不同 Area,每个 Area 作为一个容器。不同类型的卡片根据服务端下发的数据在 Area 内部做异化。
而每个 Area 的布局由 json 文件下发,Ditto 框架解析后使用 canvas 自绘,完成显示。
这套架构的优势是动态化能力强,服务端可定义任意样式,但缺点同样明显:
代码复杂度持续膨胀; 各业务代码耦合; 功能代码分散,AB 测试不友好; 难以扩展。 |
---|
7.3.2 优化方向
为了降低复杂度,我们决定按以下方向优化:
中心化 -> 去中心化; 代码物理隔离; 内部闭环,动态开关; 组装者模式,方便扩展。 |
---|
7.3.3 Section 框架架构设计
和 Part 一样,我们将一个卡片按照功能逻辑拆分为一个个 Section,形成一个 Section 池。不同卡片根据需要组装自己需要的 Section 即可。
Section 的 UI、数据、业务都是内部闭环的。不同 Section 互不感知,保证了代码物理隔离。
每个 Section 会与 ViewStub 绑定,布局可以按需加载。ViewStub 与 Section 是一对多的关系,Section 在查找 ViewStub 前会先去缓存池找,这样实现了多个 Section 修改同一个 View,保证 Section 拆得尽可能细。
上图中各模块的具体职责如下:
Section:某一切片的完整 UI+逻辑; ViewStub:与 Section 一对多,按需加载; Assembler:负责组装 Section,可根据页面异化; SectionManager:绑定数据、分发生命周期; DataCenter:Feeds 相关数据在各页面间的同步; IOC 框架:控制反转,用于 Section 与页面交互。 |
---|
Section 整体的结构图如下:
7.3.4 落地效果
基于这套 Feeds 流框架,我们完成了历史卡片的梳理和重构:
接入36种 Feed,拆分52个 Section,下线28种 Feed; 重构4个核心页面,单类代码不超过500行; 单条 Feed 开发时间缩短一半; 广告/增值团队一个版本即完成历史功能迁移。 |
---|
7.4 完善通信设计,保证代码隔离不被打破
Part 和 Section 之间会有许多通信的需求,比如数据同步,不同模块交互等。为了保证代码隔离不被打破,我们设计了比较完善的通信机制:
页面与 Part:ViewModel + LiveData; Part 与 Part:页面级事件,事件只在 PartHost 内部生效,无需注册与反注册; 页面与页面:DataCenter 数据同步。 |
---|
7.5 异化逻辑抽离,复杂度持续可控
除此之外,另一种容易打穿架构的元素是异化逻辑。比如同一张卡片在不同的页面需要显示不同效果,比如数据埋点的参数需要从页面最外层传递到 Section。针对这种跨层级通信的场景,我们设计了一套 IOC 框架来完成依赖注入,将异化逻辑拆分到了一个个 IOC 实现类中。
IOC 机制的核心是:View 树回溯 + ViewTag 存储 + 接口中心管理。我们注册时将 IOC 实现类与 View 绑定,查找时基于 View 树来回溯,保证了 O(N) 的复杂度,且可以跨越任意层级。
过去,即使传递一个 pageId 参数,也要一层层传递:
现在,层级再深我们也可以很方便拿到需要的 IOC 实现。
08、升级方案
8.1 容灾设计
站在用户的角度,其实对重构与否并没有太大感知,用户只关心稳定性是否有下降。如此大规模的重构,一行代码引起的崩溃便能使几个月的努力功亏一篑。我们上线前的首要目标便是保证用户使用不受影响,不求有功,但求无过。
因此,我们在上线前做了很多容灾设计,保证空间的核心功能可用性。
8.1.1 动态开关
我们在空间的中间层埋了配置,能通过配置下架任意的 Part 或 Section。业务层编写代码时不用再单独为每个小模块添加开关,只要基于框架做好细粒度的拆分即可。
8.1.2 崩溃保护
同时,我们做了崩溃保护的设计,保证非核心功能崩溃不会影响核心功能的使用:
崩溃时进行关键词匹配,达到指定频率时禁用/降级相关功能; 自动对 Part/Section/页面/Feed 做关键词匹配,无需注册; 非必要功能可手动注册关键词,添加保护。 |
---|
8.2 性能监控
同时,为了防止性能劣化,我们做了很多性能监控。
针对线上:
利用手 Q RMonitor 框架的监控和我们自己上报的滑动流畅度指标,来监控页面整体的流畅度; 通过在框架层打点,来监控每一个 Part、Section 或 Feed 的耗时。有劣化的模块引入时能快速发现; 实现 RFWTracer 框架,自动在页面启动流程中打点,统计页面启动各阶段的耗时。 |
---|
针对线下:
我们基于 ARTMethodHook 框架,实现对具体 View 耗时的监控,能快速定位到出问题的控件,节约开发定位性能问题的时间。
整体监控体系如图:
实际效果如图:
09、性能优化
第一次灰度后,我们尴尬地发现启动速度并没有大幅提升,流畅率甚至发生了降低。因此我们做了首屏启动和流畅度的专项优化:
9.1 首屏启动优化
我们重新梳理了启动流程中的数据处理,在启动前和启动后做了一定优化:
- 布局异步渲染
我们将首屏启动前,会根据缓存提前计算需要的布局,实现布局异步预加载。同时,为了保证 Context 的正确性,我们 Hook 了 Activity 的启动流程,提前准备好空的 Activity 对象用于异步 inflate,并在启动后绑定真实的 Context。
- 精准预加载
在首屏启动前读取缓存,提前计算首屏 Feed 对应的 Section 布局并异步加载。
- 生命周期扩展
扩展 Part 生命周期,各个 Part 的次要功能在首屏展示后初始化。
- 优化后的效果
空间好友动态页的冷启动速度提升56%,热启动速度提升53%。
9.2 列表性能优化
经过分析,我们发现列表卡顿的原因集中在两点:
Item 复用率低,导致频繁创建新 View; 布局嵌套多,测量较慢。 |
---|
解决思路:
边滑边异步 inflate:为了解决频繁创建新 View 的问题,我们在滑动时,会提前计算后面卡片所需的 ViewStub,并提前异步加载好。 自定义组件,降低层级,提前计算高度:列表中部分组件测量性能较差,比如部分嵌套 RecyclerView 的组件,会频繁触发子 RecyclerView 的测量,拉高整体测量耗时。对于这些组件,我们使用自定义组件的方式进行了替换。降低布局层级,并且提前计算高度,设置布局的高度为固定值,防止频繁测量。 |
---|
优化后的效果:完成优化后,空间首页 FPS 完成了反超,相比老版本提升了 4.9%。
10、项目重构成果总结
从我们 AB 测试的实验数据来看,重构的整体结果是比较正向的,代码质量提升与性能提升带来了业务指标的提升,业务指标的提升也带来广告指标的提升。
11、展望
宇宙コードは歴史が長く複雑なため、宇宙サービスは長期間にわたってメンテナンス状態にあり、新しい要件を迅速に開発することが困難です。3 つの最大のモジュールは、宇宙ビジネスを圧迫する 3 つの山、つまりフィード ストリーム、フォト アルバム、出版です。このアーキテクチャのアップグレードにより、スペースの基礎となるアーキテクチャの更新が完了し、最も複雑なフィード ストリーミング シーンが完全に書き直され、フォト アルバム モジュールの半分がリファクタリングされました。残りのモジュールの再構築が完了すると、空間の先祖コードが完全に書き換えられます。将来に向けて、18 年の歴史を持つ QZone が生まれ変わって再び軌道に乗ることができるよう、新しいニーズの実装をより迅速にサポートすることもできます。転送と共有を歓迎します~
-終わり-
原作者|イン・シュディ
Qzone から 18 年が経ちますが、Qzone についてどんな思い出がありますか? Tencent Cloud Developer 公式アカウントへのコメントへようこそ。最も意味のあるコメントを 1 つ選択し (暗号化された会話を使用してください)、Tencent Cloud Developer ベースボール キャップ (下の写真を参照) をプレゼントします。抽選は7月27日正午に行われる。