生き残るか、滅びるか?Qzone の 150 万行のコードがニルヴァーナに生まれ変わる

GIF カバー

滕暁雲による紹介

今年は 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日正午に行われる。

写真

写真

图片

图片

おすすめ

転載: juejin.im/post/7257897629648175141