注册

生存or毁灭?QQ空间150万行代码的涅槃重生

腾小云导读


今年是 QQ 空间诞生的第十八年,空间客户端团队也在它十八岁生日前夕完成了架构升级。因为以前不规范的多团队协同开发,导致代码逐渐劣化,有着巨大的风险。于是 QQ 空间面对庞大的历史债务,选择了重构升级,不破不立。这里和大家分享一下在重构过程中遇到的问题和解题思路,欢迎阅读。


目录


1 空间重构项目的背景


2 为什么要重构


3 空间的架构是如何崩坏的


4 架构的生命力


5 渐进式重构如何实现


6 如何保证架构的扩展性与复用性7 如何降低复杂度并长期可控


8 如何防止劣化


9 性能优化


10 项目重构成果总结


11 展望


18年前,QQ 空间上线,迅速风靡全网,成为了很多人的青春回忆。18年后的今天,QQ 空间的生命力依然强劲,是很多年轻用户的首选社交平台。


而作为最老牌的互联网产品之一,QQ 空间的代码也比较陈旧,代码运行环境复杂,维护成本高,整体架构亟需一场升级。


01、空间重构项目的背景


作为一个平台型的入口,空间承担了为很多兄弟业务引流的责任,许多团队在空间的代码里协作开发。加上自身多年累积的功能迭代,空间的业务变得非常复杂。业务的复杂带来了架构的复杂,架构的复杂意味着维护成本的升高。多年来空间的业务交接频繁,多个团队接手。交到我们团队手上时,空间的代码已经一言难尽。


这里先简单介绍一下空间的业务形态:空间目前主要的入口是在手 Q 里,我们叫做结合版。同时独立版的空间 App 还在维护(没错,空间独立 App 仍然还有一批忠实观众)。可以看到,空间有一套独立于手 Q 之外的架构,结合版与独立版会共用大量技术组件和业务组件。



02、为什么要重构?


空间是一个祖上很阔的业务,代码量非常庞大,单统计结合版的代码,就超过了150w 行。同时空间的代码运行环境也极为复杂,涉及5个进程和2个插件。随着频繁的交接和多团队的协同开发,空间的代码逐渐劣化,各项代码质量的指标几乎都在手 Q 里垫底。


空间的代码成了著名的原始森林 - 进得去出不来。代码的劣化导致历史 bug 难以收敛,即使一行代码不改,每个版本也会新增历史 bug30+。


面对如此庞大的历史债务,空间已经到了寸步难行,不破不立的地步,重构势在必行。所以,借着空间 UI 升级的契机,空间团队开始空间历史上最大规模的一次重构。


03、空间的架构是如何逐步劣化的?


跳出棋局,站在今天的角度回头看,可以发现空间的代码是个典型案例,很好地展示了一个干净的架构是如何逐步劣化的。



3.1 扩展性低,异化代码无处安放


结合版与独立版涉及大量的代码复用,包括组件、页面和跨 App 的复用等。但由于前期架构扩展性不高,导致异化的业务代码无处安放,开始侵入底层技术组件。底层组件代码开始受到污染。


3.2 代码未隔离且缺乏编程范式


空间是个平台型的业务,广告、会员、游戏、直播、小世界等团队都会在空间的代码里开发。由于没有做好代码隔离,各团队的代码耦合在一起,各写各的。同时由于缺乏编程范式,同一个类中的代码风格迥异。破窗效应发生,污染开始扩散。


3.3 维护成本暴增,恶性循环


空间的业务逻辑本身就很复杂,代码的劣化使其复杂度暴增,后续接手团队已有心无力,只能缝缝补补又三年,恶性循环。


最后陷入怪圈: 代码很乱但是稳定,开发道理都懂但确实不敢动。



3.4 Feeds 流的崩坏


以空间的 Feeds 流为例,最开始的架构思路是很清楚的,核心功能在基类实现,上层业务可以低成本地开发一个新的 Feeds 流页面。同时做了很多动态化和容器化的设计,来满足迭代效率。



但后续的需求迅速膨胀,异化出18种 Feeds 流场景,单 Feeds 流可能出现60多种卡片。这导致基类代码与 Feed View 中的代码迅速膨胀。同时 N 个团队在同一批代码中开发,代码行数和圈复杂度逐渐劣化。



04、架构的生命力


痛定思痛,在进行空间重构前的首件事情就是总结经验,避免重蹈覆辙。如何保证这次重构平稳落地并且避免后续每三年一重构?


我们总结了四点:



空间的重构都围绕着这四个问题来进行。


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 自绘,完成显示。



这套架构的优势是动态化能力强,服务端可定义任意样式,但缺点同样明显:



7.3.2 优化方向


为了降低复杂度,我们决定按以下方向优化:



7.3.3 Section 框架架构设计


和 Part 一样,我们将一个卡片按照功能逻辑拆分为一个个 Section,形成一个 Section 池。不同卡片根据需要组装自己需要的 Section 即可。


Section 的 UI、数据、业务都是内部闭环的。不同 Section 互不感知,保证了代码物理隔离。


每个 Section 会与 ViewStub 绑定,布局可以按需加载。ViewStub 与 Section 是一对多的关系,Section 在查找 ViewStub 前会先去缓存池找,这样实现了多个 Section 修改同一个 View,保证 Section 拆得尽可能细。



上图中各模块的具体职责如下:



Section 整体的结构图如下:



7.3.4 落地效果


基于这套 Feeds 流框架,我们完成了历史卡片的梳理和重构:



7.4 完善通信设计,保证代码隔离不被打破


Part 和 Section 之间会有许多通信的需求,比如数据同步,不同模块交互等。为了保证代码隔离不被打破,我们设计了比较完善的通信机制:




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 崩溃保护


同时,我们做了崩溃保护的设计,保证非核心功能崩溃不会影响核心功能的使用:



8.2 性能监控


同时,为了防止性能劣化,我们做了很多性能监控。


针对线上:



针对线下:


我们基于 ARTMethodHook 框架,实现对具体 View 耗时的监控,能快速定位到出问题的控件,节约开发定位性能问题的时间。


整体监控体系如图:



实际效果如图:



09、性能优化


第一次灰度后,我们尴尬地发现启动速度并没有大幅提升,流畅率甚至发生了降低。因此我们做了首屏启动和流畅度的专项优化:


9.1 首屏启动优化


我们重新梳理了启动流程中的数据处理,在启动前和启动后做了一定优化:




  • 布局异步渲染

我们将首屏启动前,会根据缓存提前计算需要的布局,实现布局异步预加载。同时,为了保证 Context 的正确性,我们 Hook 了 Activity 的启动流程,提前准备好空的 Activity 对象用于异步 inflate,并在启动后绑定真实的 Context。




  • 精准预加载

在首屏启动前读取缓存,提前计算首屏 Feed 对应的 Section 布局并异步加载。



  • 生命周期扩展

扩展 Part 生命周期,各个 Part 的次要功能在首屏展示后初始化。



  • 优化后的效果

空间好友动态页的冷启动速度提升56%,热启动速度提升53%。


9.2 列表性能优化


经过分析,我们发现列表卡顿的原因集中在两点:



解决思路:



优化后的效果:完成优化后,空间首页 FPS 完成了反超,相比老版本提升了 4.9%。


10、项目重构成果总结


从我们 AB 测试的实验数据来看,重构的整体结果是比较正向的,代码质量提升与性能提升带来了业务指标的提升,业务指标的提升也带来广告指标的提升。



11、展望


空间的代码历史悠久,错综复杂,使得空间业务在很长一段时间都处于维护状态,难以快速开发新的需求。最大的三个模块是压在空间业务上的三座大山:Feeds 流、相册和发表。通过这次架构升级,我们完成空间底层架构的焕新,完全重写了最复杂的 Feeds 流场景,同时相册模块也已经重构了一半。等剩余模块重构完成,空间的祖传代码就被全部重写了。面向未来,我们也能够更迅速地支撑新需求的落地,让十八岁的 QQ 空间焕然新生,重新上路。欢迎转发分享~


-End-


原创作者|尹述迪

0 个评论

要回复文章请先登录注册