本文是我在2021年宣布的文章,原文首发在字节技能公众号上,原文地址

布景介绍

本文以抖音中最为杂乱的功用,也是最重要的功用之一的交互区为例,和我们共享一下此次重构进程中的考虑和办法,首要侧重在架构、结构方面。

交互区简介

交互区是指播映页面中能够操作的区域,简略了解便是除视频播映器外附着的功用,如下图赤色区域中的作者称号、描绘文案、头像、点赞、谈论、共享按钮、蒙层、弹出面板等等,简直是用户看到、用到最多的功用,也是最首要的流量进口。

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

发现问题

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

现状

抖音iOS最复杂功能的重构之路--播放器交互区重构实践
上图是代码量排行,排在首位的便是交互区的ViewController,遥遥领先其他类,数据来源自研的代码量化体系,这是一个辅佐事务发现架构、规划、代码问题的东西。

可进一步查看版别改变:

抖音iOS最复杂功能的重构之路--播放器交互区重构实践
每周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,这是一个指数增加的,当数量满足大时,杂乱度会很夸大,联系并不简略看出来,因而很简略发生让人意想不到的改变。

功用的数量大体能够认为是随产品人数线性增加的,即杂乱度也是线性增加,跟着开发人数同步增加是能够继续保护的。假如联系数量指数级增加,那么很快就无法保护了。

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

“变量”与“常量”

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

从曩昔的改变中找到规矩,以习惯未来的改变。

往常提到的封装、内聚、解耦等概念,都是静态的,即某一个时刻点合理,不意味着未来也合理,希望改善能够在更长的时刻规划内合理,称之为动态,找到代码中的“变量”与“常量”是比较有效的手法,相应的代码也有不同的优化趋向:

  • 关于“变量”,需求确保责任内聚、单一,易扩展
  • 关于“常量”,需求封装,削减搅扰,对运用者通明

回到交互区重构场景,发现新加的子功用,基本都加在固定的3个区域中,布局是上下撑开,这儿的变指的便是新加的子功用,不变指的是加的方位和其他子功用的方位联系、逻辑联系,那么改变的部分,能够供给一个灵敏的扩展机制来支撑,不变的部分中,事务无关的下沉为底层结构,事务相关的封装为独立模块,这样全体的结构也就出来了。

“变量”与“常量”相同能够查验重构作用,比方模块间常常通过笼统出的协议进行通讯,假如通讯办法都是详细事务的,那每个同学都或许往里增加各自的办法,这个“变量”就会失掉操控,难以保护。

规划计划

整理问题的进程中,现已在不断的在考虑什么样的办法能够处理问题,大致雏形现已有了,这部分更多的是将规划计划体系化。

思路

  • 通过上述整理功用发现UI规划和产品的规矩:

    • 全体可分为3个区域,左边、右侧、底部,每个子功用都能够归到3个区域中,按需显现,数据驱动
    • 左边区域中的作者称号、描绘、音乐信息是自底向上挨个摆放
    • 右侧首要是按钮类型,头像、点赞、谈论,摆放办法和左边规矩相同
    • 底部或许有个警告、热门,只显现1个或许不显现
  • 为了共同概念,将3个区域界说为容器、容器中放置的子功用界说为元素,容器边界和才能能够放宽一些,支撑弱类型实例化,这样就能支撑物理阻隔元素代码,构成一个可插拔的机制。

  • 元素将View、布局、事务逻辑代码都内聚在一同,元素和交互区、元素和元素之间不直接依靠,责任内聚,便于保护。

  • 许多的接口能够笼统归类,大体可分为UI生命周期调用、播映器生命周期调用,将事务性的接口笼统,分发到详细的元素中处理逻辑。

架构规划

下图是希望到达的最终方针形态,施行进程会分为多步,确定最终形态,防止施行时违背方针。

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

全体指导原则:简略、适用、可演化。

  • SDK层:笼统出和事务彻底无关的SDK层,SDK担任办理Element、Element间通讯
  • 事务结构层:将通用事务、共性代码等低频率修正代码独立出来,构成结构层,这层代码是可由专人保护,事务线同学无法修正
  • 事务扩展层:各事务线详细的子功用在此层完成,供给灵敏的注册、插拔才能,Element间无耦合,代码影响限定在Element内部

SDK层

Container

一切的Element都通过Container来办理,包含2部分:

  • 对Element的创立、持有
  • 持有了一个UIStackView,Element创立的View都加入到此UIStackView中

运用UIStackView是为了完成自底向上的流式布局。

Element

子功用的UI、逻辑、操作等一切代码封装的集合体,界说为Element,学习了网页中的Element概念,对外的行为可笼统为:

  • View:最终显现的View,lazy的办法结构
  • 布局:自习惯撑开,Container中的UIStackView能够支撑
  • 工作:通用的工作,处理handler即可,view内部也可自行增加工作
  • 更新:传入模型,内部依据模型内容,赋值到view中

View

View在BaseElement中的界说如下:

@interface BaseElement : NSObject <BaseElementProtocol>
@property (nonatomic, strong, nullable) UIView *view;
@property (nonatomic, assign) BOOL appear;
- (void)viewDidLoad;
@end
  • BaseElement是笼统基类,公开view特点办法上看view特点、viewDidLoad办法,和UIViewController运用办法的十分相似,规划目的是想靠向UIViewController,以便让我们更快的承受和了解
  • appear表示element是否显现,appear为YES时,view被主动创立,viewDidLoad办法被调用,相关的子view、布局等事务代码在viewDidLoad办法中复写,和UIViewController运用相似
  • appear和hidden的差异在于,hidden仅仅视觉看不到了,内存并没有释放掉,而低频次运用的view没必要常驻内存,因而appear为NO时,会移除view并释放内存

布局

  • UIStackView的axis设置了UILayoutConstraintAxisVertical,布局时自底向上的流式摆放
  • 容器内的元素自下向上布局,最底部的元素参照容器底部束缚,依次布局,容器高度参照最上面的元素方位
  • 元素内部主动撑开,可直接设置固定高度,也能够用autolayout撑开

工作

@protocol BaseElementProtocol <NSObject>
@optional
- (void)tapHandler:(UITapGestureRecognizer *)sender;
@end
  • 完成协议办法,主动增加手势,支撑点击工作
  • 也能够自行增加工作,如按钮,运用原生的addTarget点击体会更好

更新

data特点赋值,触发更新,通过setter办法完成。

@property (nonatomic, strong, nullable) id data;

赋值时会调用setData办法。

- (void)setData:(id)data {
    _data = data;
    [self processAppear:self.appear];
}

赋值时,processAppear办法会依据appear状况更新View的状况,决议创立或销毁View。

数据流图

Element的生命周期、更新时的数据流向示目的,这儿就不细讲了。

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

动画特效

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

图中是实践需求支撑的事务场景,现在是ABTest阶段,老代码完成办法首要问题:

  • 对每处view都用GET_AB_TEST_CASE(videoPlayerInteractionOptimization)判别处理了,代码中共有32处判别
  • 每个View运用Transform动画躲藏

这个完成办法十分涣散,加新view时很简略被遗失,Element支撑更优的办法:

  • 左边一切子功用都在一个容器中,因而躲藏容器即可,不需求操作每个子功用
  • 右侧独自躲藏头像、音乐独自处理即可

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

扩展性

Element之间无依靠,能够做到每个Element物理阻隔,代码放在各自的事务组件中,事务组件依靠交互区事务结构层即可,独立的Element通过runtime办法,运用注册的办法供给给交互区,结构会将字符串的类实例化,让其正常作业。

[self.container addElementByClassName:@"PlayInteractionAuthorElement"];
[self.container addElementByClassName:@"PlayInteractionRateElement"];
[self.container addElementByClassName:@"PlayInteractionDescriptionElement"];

事务结构层

容器办理

SDK中仅供给了容器的笼统界说和完成,在事务场景中,需求结合详细事务场景,进一步界说容器的规划和责任。

上面整理了功用中将整个页面分为左边、右侧、底部3个区域,那么这3个区域便是相应的容器,一切子功用都能够归到这3个容器中,如下图:

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

协议

Feed是用UITableView完成,Cell中除了交互区外只要播映器,因而一切的外部调用都能够笼统,如下图所示。

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

从概念上讲只需求1个交互区协议,但这儿能够细分为2部分:

  • 页面生命周期
  • 播映器生命周期

一切Element都要完成这个协议,因而在SDK中的Element基类之上,继承完成了PlayInteractionBaseElement,这样详细Element中不需求完成的办法能够不写。

@interface PlayInteractionBaseElement : BaseElement <PlayInteractionDispatcherProtocol>
@end

为了更明晰界说协议责任,用接口阻隔的思维继续拆分,PlayInteractionDispatcherProtocol作为共同的聚合协议。

@protocol PlayInteractionDispatcherProtocol <PlayInteractionCycleLifeDispatcherProtocol, PlayInteractionPlayerDispatcherProtocol>
@end

页面生命周期协议:PlayInteractionCycleLifeDispatcherProtocol

简略列了部分办法,这些办法都是ViewController、TableView、Cell对应的生命周期办法,是彻底笼统的、和事务无关的,因而不会跟着事务量的增加而胀大。

@protocol PlayInteractionCycleLifeDispatcherProtocol <NSObject>
- (void)willDisplay;
- (void)setHide:(BOOL)flag;
- (void)reset;
@end

播映器生命周期协议:PlayInteractionPlayerDispatcherProtocol

播映器的状况和办法,也是笼统的、和事务无关。

@protocol PlayInteractionPlayerDispatcherProtocol <NSObject>
@property (nonatomic, assign) PlayInteractionPlayerStatus playerStatus;
- (void)pause;
- (void)resume;
- (void)videoDidActivity;
@end

Manager – 弹窗、蒙层

弹窗、蒙层的view规矩并不在容器办理之中,所以需求一套额定的办理办法,这儿界说了Manager概念,是一个相对笼统的概念,即能够完成弹窗、蒙层等功用,也能够完成View无关的功用,和Element相同,将代码拆分开。

@interface PlayInteractionBaseManager : NSObject <PlayInteractionDispatcherProtocol>
- (UIView *)view;
@end
  • PlayInteractionBaseManager相同完成了PlayInteractionDispatcherProtocol协议,因而具备了一切的交互区协议调用才能
  • Manager不供给View的创立才能,这儿的view是UIViewController的view引用,比方需求加蒙层,那么加到manager的view中就相当于加到UIViewController的view中
  • 弹窗、蒙层通过此种办法完成,Manager并不担任弹窗、蒙层间的互斥、优先级逻辑处理,需求独自的机制去做

办法派发

事务结构层中界说的协议,需求结构层调用,SDK层是感知不到的,由于Element、Manager许多,需求一个机制来封装批量调用进程,如下图所示:

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

分层结构

旧交互区运用了VIPER范式,抖音里全体运用的MVVM,多套范式会增加学习、保护本钱,而且运用Element开发时,VIPER层级过多,因而考虑共同为MVVM。

VIPER全体分层结构

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

MVVM全体分层结构

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

在MVVM结构中,Element责任和ViewController概念很挨近,也能够了解为更纯粹、更专用的的ViewController。

通过Element拆分后,每个子功用现已内聚在一同,代码量是有限的,能够比较好的支撑事务开发。

Element结合MVVM结构

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

  • Element:假如是特别简略的元素,那么只供给Element的完成即可,Element层担任基本的完成和跳转
  • ViewModel:部分元素逻辑比较杂乱,需求将逻辑抽离出来,作为ViewModel,对应现在的Presentor层
  • Tracker:埋点东西,埋点也能够写在VM中,对应现在的Interactor
  • Model:绝大多数运用主Model即可

事务层

事务层中寄存的是Element完成,首要有两种类型:

  • 通用事务:如作者信息、描绘、头像、点赞、谈论等通用的功用
  • 子事务线事务:十几便条事务线,不一一罗列

通用事务Element和交互区代码放在一同,子事务线Element放在事务线中,代码物理阻隔后,责任会更明确,可是这也带来一个问题,当结构调整时,需求改多个库房,而且或许修正遗失,所以重构初期能够先放一同,安稳后再迁出去。

过度规划误区

规划往往会走两个极端,没有规划、过度规划。

所谓没有规划是在现有的架构、形式下,没有额定考虑过差异、特点,照搬运用。

过渡规划往往是在吃了没有规划的亏后,成了草木惊心,看什么都要搞一堆装备、组合、扩展的规划,简略的反而搞杂乱了,过为己甚。

规划是在质量、本钱、时刻等因素之间做出权衡的艺术。

施行计划

事务开发不能停,一边开发、一边重构,相当于在高速公路上不停车换轮胎,需求有满足的预案、备案,才干确保规划计划顺利落地。

改动评价

先估算一下修正规划、周期:

  • 代码修正量:近4万行
  • 时刻:半年

改动巨大、时刻很长,风险是难以操控的,每个版别都有许多事务需求,需求改许多的代码,在重构的一起,假如重构的代码和新需求代码抵触,是十分难解的,因而考虑分期。

上面现已屡次提到功用的重要性,需求考虑重构后,功用是否正常,假如出了问题如何处理、如何证明重构后的功用和之前是共同的,对产品数据无影响。

施行战略

基本思路是完成一个新页面,通过ABTest来切换,中心方针无显着负向则放量,全量后删去旧代码,示目的如下:

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

共分为三期:

  • 一期改造内容如上图赤色所示:抽取协议,面向协议编程,不依靠详细类,改造旧VC,完成协议,将协议之外露出的办法、特点收敛到内部
  • 二期改造内容如蓝色所示:新建个新VC,新VC和旧VC在功用上是彻底共同,完成协议,通过ABTest来操控运用方拿到的是旧VC仍是新VC
  • 三期内容:删掉旧VC、ABTest,协议、新VC保存,完成替换作业

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

其中二期是重点,占用时刻最多,此阶段需求一起保护新旧两套页面,开发、测验作业量翻倍,因而要尽或许的缩短二期时刻,不要着急改代码,能够将一期做完善了、各方面的规划预备好再开始。

ABTest

2个目的:

  • 运用ABTest作为开关,能够灵敏的切换新旧页面
  • 用数据证明新旧页面是共同的,从事务功用上来说,二者彻底共同,但实践情况是否契合预期,需求用留存、播映、浸透率等中心方针证明

两套页面的开发办法

在二期中,两套页面ABTest切换办法是有本钱的,需求开发两套、测验两遍,尽管部分代码可共用,但本钱仍是大大增加,因而需求将这个阶段尽或许缩短。

别的开发、测验两套,不简略发现问题,而一旦出问题,即使能用ABTest灵敏切换,但修正问题、从头上线、ABTest数据有结论,也需求十分长的周期。

假如每个版别都出问题,那将会是上线、发现问题,从头修正再上线,又发现了新问题,无限循环,或许一向无法全量。

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

如上图所示,版别单周迭代,发现问题跟下周修正,那么需求通过灰度、上线灰度(AppStore的灰度放量)、ABTest验证(AB数据安稳要2周),总计要6周的时刻。

让每个同学了解全体运作机制、本钱,有助于共同方针,缩短此阶段周期。

删掉旧代码

架构规划上预备满足,删掉旧代码十分简略,删掉旧文件、ABTest即可,事实上也是如此,1天内就完成了。

代码后入后,有些长尾的作业会继续2、3个版别,例如有些分支,现已修正了删掉的代码,由于文件现已不存在了,只要修正,必定会抵触,合之前,需求git merge一下源分支,将有抵触的老页面再删掉。

防溃散兜底

面向协议开发两套页面,假如增加一个功用时,新页面遗失了某个办法的话,希望能够不溃散

运用Objective-C言语音讯转发能够完成这特性,在forwardingTargetForSelector

办法中判别办法是否存在,假如不存在,增加一个兜底办法即可,用来处理即可。

- (id)forwardingTargetForSelector:(SEL)aSelector {
  Class clazz = NSClassFromString(@"TestObject");
  if (![self isExistSelector:aSelector inClass:clazz]) {
    class_addMethod(clazz, aSelector, [self safeImplementation:aSelector], [NSStringFromSelector(aSelector) UTF8String]);
  }
  Class Protector = [clazz class];
  id instance = [[Protector alloc] init];
  return instance;
}
- (BOOL)isExistSelector:(SEL)aSelector inClass:(Class)clazz {
  BOOL isExist = NO;
  unsigned int methodCount = 0;
  Method *methods = class_copyMethodList(clazz, &methodCount);
  NSString *aSelectorName = NSStringFromSelector(aSelector);
  for (int i = 0; i < methodCount; i++) {
    Method method = methods[i];
    SEL selector = method_getName(method);
    NSString *selectorName = NSStringFromSelector(selector);
    if ([selectorName isEqualToString: aSelectorName]) {
      isExist = YES;
      break;
    }
  }
  return isExist;
}
- (IMP)safeImplementation:(SEL)aSelector {
  IMP imp = imp_implementationWithBlock(^(){
    // log
  });
  return imp;
}

线上兜底下降影响规划,内测提示尽早发现,在开发、内测阶段时能够用比较强的交互手法提示,如toast、弹窗等,别的能够接打点上报计算。

防劣化

需求明确的规矩、机制防劣化,并继续投入精力保护。

不是每个人都能了解规划目的,不同责任的代码放在应该放的方位,比方事务无关的代码,应该下沉到结构层,下降被损坏的概率,紧密的开发节奏,即使简略的if else也简略写出问题,例如再加1个条件,简直都会再写1个if,直至写了几十个后,发现写不下去了,再推到重构,希望重构一次后,能够坚持的尽或许久一些。

更严峻的是在重构进程中,代码就或许劣化,假如问题呈现的速度超越处理的速度,那么将会一向疲于救火,永远无法彻底处理。

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

新计划中,事务逻辑都放在了Element中,ViewController、容器中剩下通用的代码,这部分代码事务同学是没必要去修正,不了解全体也简略改出问题,因而这部分代码由专人来保护,各事务同学有需求改结构层代码的需求,专人来修正。

各Element依照事务线划分为独立文件,自己保护的文件能够加reviewer或文件变更告诉,也能够迁到事务库房中,进行物理阻隔。

日志 & 问题排查

安稳复现的问题,比较简略排查和处理,但概率性的问题,尤其是iOS体系问题引起的概率性问题,比较排查,即使猜想或许引起问题的原因,修正后,也难以自测验证,只能上线再调查。

关键信息提早加日志记载,如用户反馈某个视频有问题,那么需求依据日志,找到相应的Model、Element、View、布局、束缚等信息。

信息同步

改动过广,需求及时周知事务线的开发、测验、产品同学,几个办法:

  • 拉群告诉
  • 周会、周报

开发同学最重视的点是什么时候放量、什么时候全量、什么时候能够删掉老代码,不用保护2套代码。

其次是改动,结构在不行安稳时,是需求常常改的,假如改动,需求相应受影响的功用的保护同学验证,以及承认测验是否介入。

产品同学也要周知,尽管产品不重视怎么做,可是一旦出问题,没有周知,很费事。

确保质量

最重要的是及时发现问题,这是防止或许削减影响的前提条件。

惯例的RD自测、QA功用测验、集成测验等是必备的,这儿不多说,首要探讨其他哪些手法能够愈加及时的发现问题。

新开发的需求,需求开发新、老页面两套代码,相同,也要测验两次,尽管屡次着重,但涉及到多个事务线、跨团队、跨责任、时刻线长,很简略遗失,而新页面ABTest放量很小,一旦出问题,很难被发现,因而对线上和测验用户区分处理:

  • 线上、线下流量战略:线上AppStore渠道ABTest按数据剖析师规划放量;内测、灰度等线下渠道放量50%,新旧两套各占一半,内测、灰度人员仍是有必定规划的,假如是显着的问题,比较简略发现的
  • ABTest产品方针对照:灰度、线上数据都是有参阅价值的,依照ABTest数据量,粗评一下是否有问题,假如有显着问题,可及时深化排查
  • Slardar ABTest技能方针对照:最常用的是crash率,比照对照组和试验组的crash率,看下是否有新crash,试验组放量比较小,独自的看crash数量是很难发现的,也简略疏忽。别的还要别的技能方针,也能够重视下
  • Slardar技能打点告警装备:重构周期比较长,难以做到每天都盯着,关键方位加入技能打点,体系中装备告警,设置好条件,这样在呈现问题时,会及时告诉你
  • 单元测验:单测是确保重构的必要手法,在结构、SDK等中心代码,都加入了单测
  • UI主动化测验:假如有完好的验证用例,能够必定程度上帮助发现问题

排查问题

安稳复现的问题比较简略定位处理,两类问题比较头疼,详细讲一下:

  • ABTest方针负向
  • 概率性呈现的问题

ABTest方针负向

ABTest中心方针负向,是无法放量的,乃至要关掉试验排查问题。

有个共享比方,共享总量、人均共享量都显着负向,大体通过这样几个排查进程:

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

排查ABTest方针和排查bug相似,都是找到差异,缩小规划,最终定位代码。

  • 比照功用:从用户运用视点找差异,交互规划师、测验、开发自测都没有发现有差异
  • 比照代码:比照新老两套打点代码逻辑,尤其是进入打点的条件逻辑,没有发现差异
  • 拆分方针:许多功用都能够共享,打点渠道能够按共享页面来源拆分方针,发现长按弹出面板中的共享削减,其他来源相差不大,进一步排查弹出面板呈现的概率发现显着变低了,大体定位问题规划。别的值得一提的是,不喜欢不是很中心的方针,而且不喜欢变少,意味着视频质量更高,所以这点是从ABTest数据中难以发现的
  • 定位代码:排查面板呈现条件发现,老代码中是在长按手势中,扫除了单个的点赞、谈论等按钮,其他方位(假如没有增加工作)都是可点的,比方点赞、谈论按钮之间的空白方位,而新代码中是将右侧按钮区域、底部共同扫除了,这样空白区域就不能点了,点击区域变小了,因而呈现概率变小了
  • 处理问题:定位问题后,修正比较简略,复原了旧代码完成办法

这个问题能考虑的点是比较多的,重构时,看到了欠好的代码,究竟要不要改?

比方上面的问题,增加了功用后,不知道是否应该扫除点击,很简略被疏忽,长按归于底层逻辑,详细按钮归于事务细节,底层逻辑依靠了细节是欠好的,可保护性很差,可是修正后,很或许影响交互体会和产品方针,尤其是中心方针,一旦影响,没有太多探讨空间。

详细情况详细评价,假如预估到影响了功用、交互,尽量不要改,大重构尽或许先处理中心问题,部分问题能够后续独自处理。

下面是长按面板中的共享数据截图,显着下降,其他来源基本坚持共同,就不贴图了。

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

长按蒙层呈现率下降10%左右,比较天然的猜想蒙层呈现率下降。

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

比照View视图差异承认问题。

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

相似的问题许多,ABTest放量、全量进程要有满足的估时和耐性,这个进程会大大超越预期。抖音中心方针简直都和交互区相关,许多剖析师和产品都要重视,因而先了解一下剖析师、产品和开发同学对ABTest方针负向的认知差别。

大部分方针是正向,单个方针负向,那么会被判别为负向。

开发同学或许想的是规划的合理性、代码的合理性,或许从全体的收益、丢失视点的差值考虑,但剖析师会优先考虑不出问题、别有隐患。两种办法是站着不同视点、方针考虑的,没有对错之分,事实上剖析师帮忙发现了十分多的问题。现在的剖析师、产品许多,每个方针都有剖析师、产品担任,假如某个中心方针显着负向,找相应的剖析师、产品评论,是十分难达到共同的,即使是先放量再排查的计划也很难承受,主张自己学会看方针,尽早跟进,关键时找人帮忙推动。

概率性呈现的问题

概率性呈现的问题难点在于,很难复现,无法调试定位问题,修正后无法测验验证,需求上线后才干确定是否修正,举一个实践的比方的iOS9上crash比方,发现进程:

  • 通过slardar=>AB试验=>指定试验=>监控类型=>溃散 发现的,能够看到试验组和对照组的crash率,其他的OOM等方针也能够用这个功用查看

下面是crash的仓库,crash率比较高,大约50%的iOS9的用户会呈现:

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

crash仓库在体系库中,无法看到源码,仓库中也无法找到相关的问题代码,无法定位问题 ,整个处理进程比较长,尝试用过的办法,供我们参阅:

  • 手动复现,尝试修正,能够复现,但刷一天也复现不了几次,功率太低,对部分问题来说,判别准的话,能够比较快的处理
  • swizzle体系溃散的办法,日志记载最终溃散的View、相关View的层次结构,缩小排查规划
  • 主动化测验复现,能够用来验证是否修正问题,无法定位问题
  • 逆向看UIKit体系完成,剖析溃散原因

逆向大体进程:

  • 下载iOS9 Xcode & 模拟器文件
  • 提取UIKit动态库
  • 剖析crash仓库,通过crash最终所在的_layoutEngine、_addOrRemoveConstraints、_withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists 3个关键办法,找到调用路径,如下图所示:

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

  • _withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists中调用了deactivateConstraints办法,deactivateConstraints中又调用了_addOrRemoveConstraints办法,和crash仓库中第3行匹配,那么问题就出在此处,为便利排查,逆向出相关办法的详细完成,大体如下:
 @implementation UIView
- (void)_withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists:(Block)action {
    id engine = [self _layoutEngine];
    id delegate = [engine delegate];
    BOOL suspended = [delegate _isUnsatisfiableConstraintsLoggingSuspended];
    [delegate _setUnsatisfiableConstraintsLoggingSuspended:YES];
    action();
    [delegate _setUnsatisfiableConstraintsLoggingSuspended:suspended];
    if (suspended == YES) {
        return;
    }
    NSArray *constraints = [self _constraintsBrokenWhileUnsatisfiableConstraintsLoggingSuspended];
    if (constraints.count != 0) {
        NSMutableArray *array = [[NSMutableArray alloc] init];
        for (NSLayoutConstraint *_cons : constraints) {
            if ([_cons isActive]) {
                [array addObject:_cons];
            }
        }
        if (array.count != 0)  {
            [NSLayoutConstraint deactivateConstraints:array]; // NSLayoutConstraint 进口
            [NSLayoutConstraint activateConstraints:array];
        }
    }
    objc_setAssociatedObject(
                self, 
                @selector(_constraintsBrokenWhileUnsatisfiableConstraintsLoggingSuspended),
                nil,
                OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
@implementation NSLayoutConstraint
+ (void)activateConstraints:(NSArray *)_array {
    [self _addOrRemoveConstraints:_array activate:YES]; // crash仓库中倒数第3个调用
}
+ (void)deactivateConstraints:(NSArray *)_array {
    [self _addOrRemoveConstraints:_array activate:NO];
}
@end
  • 从代码逻辑和_constraintsBrokenWhileUnsatisfiableConstraintsLoggingSuspended办法的命名语义上看,此处代码首要是用来处理无法满足束缚日志的,应该不会影响功用逻辑
  • 别的,剖析时假如无法准确判别crash方位,则需求逆向真机文件,相比模拟器,真机的仓库是准确的,通过原始crash仓库偏移量找到最终的代码调用

拿到成果

  • 开发功率:将之前VIPER结构的5个文件,拆分了大约50个文件,每个功用的责任都在事务线内部,增加、修正不再需求看一切的代码了,调研问卷显现开发功率提升在20%以上
  • 开发质量:从bug、线上毛病来看,新页面问题是比较少的,而且出问题一般的都是结构的问题,修正后是能够防止批量的问题的
  • 产品收益:尽管功用共同,但由于重构规划的功用是有改善的,中心方针正向收益显着,试验开启屡次,中心方针结论共同

勇气

最终这部分是考虑良久后加上的,重构自身便是开发的一部分,再正常不过,但重构总是难以进行,有的浅尝辄止,乃至半途而废。公司严厉的招聘下,能进来的都是聪明人,不短少处理问题的才智,短少的是勇气,回顾这次重构和上面提到过的“从前尝试过的办法”,也正是如此。

代码难以保护时是比较简略发现的,优化、重构的想法也很天然,可是有两点让重构无法有效展开:

  • 什么时候开始
  • 部分重构试试

在评论什么时候开始前,能够先看个词,作业中有个盛行词叫ROI,大意是投入和收益比率,投入越少、收益越高越好,最好是空手套白狼,这个词指导了许多决策。

重构无疑是个费力的作业,需求投入十分大的心力、时刻,而能看到的直接收益不显着,一旦改出问题,还要承担风险,重构也很难取得其他人认可,比方在产品看来,功用彻底没变,代码还能跑,为什么要现在重构,新需求还等着开发呢,有问题的代码便是这样不断的拖着,越来越严峻。

固然,有满足的痛点时重构是收益最高的,但仅仅看起来,真实的收益是不变的,在这之前需求许多额定的保护本钱,以及劣化后的重构本钱,从长期收益看,已然要改就趁早改。决议要做比较难,压服我们更难,每个人的了解或许都不相同,对长期收益的判别也不相同,很难达到共同。

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

思者众、行者寡,未知的作业我们偏向慎重,支撑继续前行的是对技能寻求的勇气。

重构最好的时刻便是当下。

部分重构,积少成多,最终全体完成,即使出问题,影响也是部分的,这是自下向上的办法,自身是没问题的,也常常运用,与之对应的是自上向下的全体重构,这儿想着重的是,部分重构、全体重构仅仅手法,选择什么手法要看处理什么问题,假如根本问题是全体结构、架构的问题,部分重构是无法处理的。

比方这次重构时,十分多的人都提出,能否改动小一点、慎重一点,可是规划计划是通过剖析整理的,现已明确是结构性问题,部分重构是无法处理的,从前那些尝试过的办法也证明了这一点。

不能由于怕扯到蛋而忘记奔驰。