抖音iOS最复杂功能的重构之路--播放器交互区重构实践

背景介绍

本文以抖音中最为复杂的功能,也是最重要的功能之一的交互区为例,和大家分享一下此次重构过程中的思考和方法,主要侧重在架构、结构方面。

交互区简介

交互区是指播放页面中可以操作的区域,简单理解就是除视频播放器外附着的功能,如下图红色区域中的作者名称、描述文案、头像、点赞、评论、分享按钮、蒙层、弹出面板等等,几乎是用户看到、用到最多的功能,也是最主要的流量入口。

发现问题

不要急于改代码,先梳理清楚功能、问题、代码,建立全局观,找到问题根本原因。

现状

上图是代码量排行,排在首位的就是交互区的 ViewController,遥遥领先其他类,数据来源自研的代码量化系统,这是一个辅助业务发现架构、设计、代码问题的工具。

可进一步查看版本变化:

每周 1 版,在不到 1 年的时间,代码量翻倍,个别版本代码量减少,是局部在做优化,大趋势仍是快速增长。

除此之外:

  • 可读性差:ViewController 代码量 1.8+万行,是抖音中最大的类,超过第 2 大的类一倍有余,另外交互区使用了 VIPER 结构(iOS 常用的结构:MVC、MVVM、MVP、VIPER),加上 IPER 另外 4 层,总代码规模超过了 3 万行,这样规模的代码,很难记清某个功能在哪,某个业务逻辑是什么样的,为了修改一处,需要读懂全部代码,非常不友好

  • 扩展性差:新增、修改每个功能需要改动 VIPER 结构中的 5 个类,明明业务逻辑独立的功能,却需要大量耦合已有功能,修改已有代码,甚至引起连锁问题,修一个问题,结果又出了一个新问题

  • 维护人员多:统计 commit 历史,每个月都有数个业务线、数十人提交代码,改动时相互的影响、冲突不断

理清业务

作者是抖音基础技术组,负责业务架构工作,交互区业务完全不了解,需要重新梳理。

事实上已经没有一个人了解所有业务,包括产品经理,也没有一个完整的需求文档查阅,需要根据代码、功能页面、操作来梳理清楚业务逻辑,不确定的再找相关开发、产品同学,省略中间过程,总计梳理了 10+个业务线,100+子功能,梳理这些功能的目的是:

  • 按重要性分清主次,核心功能优先保障,分配更多的时间开发、测试

  • 子功能之间的布局、交互是有一定的规律的,这些规律可以指导重构的设计

  • 判断产品演化趋势,设计既要满足当下、也要有一定的前瞻性

  • 自测时需要用,避免遗漏

理清代码

所有业务功能、问题最终都要落在代码上,理清代码才能真正理清问题,解决也从代码中体现,梳理如下:

  • 代码量:VC 1.8 万行、总代码量超过 3 万行

  • 接口:对外暴露了超过 200 个方法、100 个属性

  • 依赖关系:VIPER 结构使用的不理想,Presenter 中直接依赖了 VC,互相耦合

  • 内聚、耦合:一个子功能,代码散落在各处,并和其他子功能产生过多耦合

  • 无用代码:大量无用的代码、不知道做什么的代码

  • View 层级:所有的子功能 View 都放在 VC 的直接子 View 中,也就是说 VC 有 100+个 subView,实际仅需要显示 10 个左右的子功能,其他的通过设置了 hidden 隐藏,但是创建并参与布局,会严重消耗性能

  • ABTest(分组对照试验):有几十个 ABTest,最长时间可以追溯到数年前,这些 ABTest 在自测、测试都难以全面覆盖

简单概括就是,需要完整的读完代码,重点是类之间的依赖关系,可以画类图结合着理解。

每一行代码都是有原因的,即便感觉没用,删一行可能就是一个线上事故。

趋势

抖音产品特性决定,视频播放页面占据绝大部分流量,各业务线都想要播放页面的导流,随着业务发展,不断向多样性、复杂性演化。

从播放页面的形态上看,已经经过多次探索、尝试,目前的播放页面模式相对稳定,业务主要以导流形式的入口扩展。

曾经尝试过的方式

ViewController 拆分 Category

将 ViewController 拆分为多个 Category,按 View 构造、布局、更新、业务线逻辑将代码拆分到 Category。这个方式可以解决部分问题,但有限,当功能非常复杂时就无法很好的支撑了,主要问题有:

  • 拆分了 ViewController,但是 IPER 层没有拆分,拆分的不彻底,职责还是相互耦合

  • Category 之间互相访问需要的属性、内部方法时,需要暴露在头文件中,而这些是应该隐藏的

  • 无法支持批量调用,如 ViewDidLoad 时机,需要各个 Category 方法定义不同方法(同名会被覆盖),逐个调用

左侧和底部的子功能放在一个 UIStackView 中

这个思路方向大体正确了,但是在尝试大半年后失败,删掉了代码。

正确的点在于:抽象了子功能之间的关系,利用 UIStackView 做布局。

失败的点在于:

  • 局部重构:仅仅是局部重构,没有深入的分析整体功能、逻辑,没有彻底解决问题,Masonry 布局代码和 UIStackView 使用方式都放在 ViewController 中,不同功能的 view 很容易耦合,劣化依然存在,很快又然难以维护,这类似破窗效应

  • 实施方案不完善:布局需要实现 2 套代码,开发、测试同学非常容易忽略,线上经常出问题

  • UIStackView crash:概率性 crash,崩在系统库中,大半年时间也没有找到原因

其他

还有一些提出 MVP、MVVM 等结构的方案,有的浅尝辄止、有的通过不了技术评审、有的不了了之。

关键问题

上面仅列举部分问题,如果按人头收集,那将数不胜数,但这些基本都是表象问题,找到问题的本质、原因,解决关键问题,才能彻底解决问题,很多表象问题也会被顺带解决。

经常提到的内聚、耦合、封装、分层等等思想感觉很好,用时却又没有真正解决问题,下面扩展两点,辅助分析、解决问题:

  • 复杂度

  • “变量”与“常量”

复杂度

复杂功能难以维护的原因的是因为复杂。

是的,很直接,相对的,设计、重构等手法都是让事情变得简单,但变简单的过程并不简单,从 2 个角度切入来拆解:

  • 关系

:量是显性的,功能不断增加,相应的需要更多人来开发、维护,需要写更多代码,也就越来越难维护,这些是显而易见的。

关系:关系是隐性的,功能之间产生耦合即为发生关系,假设 2 个功能之间有依赖,关系数量记为 1,那 3 者之间关系数量为 3,4 者之间关系数量为 6,这是一个指数增加的,当数量足够大时,复杂度会很夸张,关系并不容易看出来,因此很容易产生让人意想不到的变化。

功能的数量大体可以认为是随产品人数线性增长的,即复杂度也是线性增长,随着开发人数同步增长是可以继续维护的。如果关系数量指数级增长,那么很快就无法维护了。

“变量”与“常量”

“变量”是指相比上几个版本,哪些代码变了,与之对应的“常量”即哪些代码没变,目的是:

从过去的变化中找到规律,以适应未来的变化。

平常提到的封装、内聚、解耦等概念,都是静态的,即某一个时间点合理,不意味着未来也合理,期望改进可以在更长的时间范围内合理,称之为动态,找到代码中的“变量”与“常量”是比较有效的手段,相应的代码也有不同的优化趋向:

  • 对于“变量”,需要保证职责内聚、单一,易扩展

  • 对于“常量”,需要封装,减少干扰,对使用者透明

回到交互区重构场景,发现新加的子功能,基本都加在固定的 3 个区域中,布局是上下撑开,这里的变指的就是新加的子功能,不变指的是加的位置和其他子功能的位置关系、逻辑关系,那么变化的部分,可以提供一个灵活的扩展机制来支持,不变的部分中,业务无关的下沉为底层框架,业务相关的封装为独立模块,这样整体的结构也就出来了。

“变量”与“常量”同样可以检验重构效果,比如模块间常常通过抽象出的协议进行通信,如果通

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值