本文是我在2021年宣布的文章,原文首发在字节技能公众号上,原文地址
布景介绍
本文以抖音中最为杂乱的功用,也是最重要的功用之一的交互区为例,和我们共享一下此次重构进程中的考虑和办法,首要侧重在架构、结构方面。
交互区简介
交互区是指播映页面中能够操作的区域,简略了解便是除视频播映器外附着的功用,如下图赤色区域中的作者称号、描绘文案、头像、点赞、谈论、共享按钮、蒙层、弹出面板等等,简直是用户看到、用到最多的功用,也是最首要的流量进口。
发现问题
不要急于改代码,先整理清楚功用、问题、代码,建立全局观,找到问题根本原因。
现状
上图是代码量排行,排在首位的便是交互区的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个区域中,布局是上下撑开,这儿的变指的便是新加的子功用,不变指的是加的方位和其他子功用的方位联系、逻辑联系,那么改变的部分,能够供给一个灵敏的扩展机制来支撑,不变的部分中,事务无关的下沉为底层结构,事务相关的封装为独立模块,这样全体的结构也就出来了。
“变量”与“常量”相同能够查验重构作用,比方模块间常常通过笼统出的协议进行通讯,假如通讯办法都是详细事务的,那每个同学都或许往里增加各自的办法,这个“变量”就会失掉操控,难以保护。
规划计划
整理问题的进程中,现已在不断的在考虑什么样的办法能够处理问题,大致雏形现已有了,这部分更多的是将规划计划体系化。
思路
-
通过上述整理功用发现UI规划和产品的规矩:
- 全体可分为3个区域,左边、右侧、底部,每个子功用都能够归到3个区域中,按需显现,数据驱动
- 左边区域中的作者称号、描绘、音乐信息是自底向上挨个摆放
- 右侧首要是按钮类型,头像、点赞、谈论,摆放办法和左边规矩相同
- 底部或许有个警告、热门,只显现1个或许不显现
-
为了共同概念,将3个区域界说为容器、容器中放置的子功用界说为元素,容器边界和才能能够放宽一些,支撑弱类型实例化,这样就能支撑物理阻隔元素代码,构成一个可插拔的机制。
-
元素将View、布局、事务逻辑代码都内聚在一同,元素和交互区、元素和元素之间不直接依靠,责任内聚,便于保护。
-
许多的接口能够笼统归类,大体可分为UI生命周期调用、播映器生命周期调用,将事务性的接口笼统,分发到详细的元素中处理逻辑。
架构规划
下图是希望到达的最终方针形态,施行进程会分为多步,确定最终形态,防止施行时违背方针。
全体指导原则:简略、适用、可演化。
- 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的生命周期、更新时的数据流向示目的,这儿就不细讲了。
动画特效
图中是实践需求支撑的事务场景,现在是ABTest阶段,老代码完成办法首要问题:
- 对每处view都用GET_AB_TEST_CASE(videoPlayerInteractionOptimization)判别处理了,代码中共有32处判别
- 每个View运用Transform动画躲藏
这个完成办法十分涣散,加新view时很简略被遗失,Element支撑更优的办法:
- 左边一切子功用都在一个容器中,因而躲藏容器即可,不需求操作每个子功用
- 右侧独自躲藏头像、音乐独自处理即可
扩展性
Element之间无依靠,能够做到每个Element物理阻隔,代码放在各自的事务组件中,事务组件依靠交互区事务结构层即可,独立的Element通过runtime办法,运用注册的办法供给给交互区,结构会将字符串的类实例化,让其正常作业。
[self.container addElementByClassName:@"PlayInteractionAuthorElement"];
[self.container addElementByClassName:@"PlayInteractionRateElement"];
[self.container addElementByClassName:@"PlayInteractionDescriptionElement"];
事务结构层
容器办理
SDK中仅供给了容器的笼统界说和完成,在事务场景中,需求结合详细事务场景,进一步界说容器的规划和责任。
上面整理了功用中将整个页面分为左边、右侧、底部3个区域,那么这3个区域便是相应的容器,一切子功用都能够归到这3个容器中,如下图:
协议
Feed是用UITableView完成,Cell中除了交互区外只要播映器,因而一切的外部调用都能够笼统,如下图所示。
从概念上讲只需求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许多,需求一个机制来封装批量调用进程,如下图所示:
分层结构
旧交互区运用了VIPER范式,抖音里全体运用的MVVM,多套范式会增加学习、保护本钱,而且运用Element开发时,VIPER层级过多,因而考虑共同为MVVM。
VIPER全体分层结构
MVVM全体分层结构
在MVVM结构中,Element责任和ViewController概念很挨近,也能够了解为更纯粹、更专用的的ViewController。
通过Element拆分后,每个子功用现已内聚在一同,代码量是有限的,能够比较好的支撑事务开发。
Element结合MVVM结构
- Element:假如是特别简略的元素,那么只供给Element的完成即可,Element层担任基本的完成和跳转
- ViewModel:部分元素逻辑比较杂乱,需求将逻辑抽离出来,作为ViewModel,对应现在的Presentor层
- Tracker:埋点东西,埋点也能够写在VM中,对应现在的Interactor
- Model:绝大多数运用主Model即可
事务层
事务层中寄存的是Element完成,首要有两种类型:
- 通用事务:如作者信息、描绘、头像、点赞、谈论等通用的功用
- 子事务线事务:十几便条事务线,不一一罗列
通用事务Element和交互区代码放在一同,子事务线Element放在事务线中,代码物理阻隔后,责任会更明确,可是这也带来一个问题,当结构调整时,需求改多个库房,而且或许修正遗失,所以重构初期能够先放一同,安稳后再迁出去。
过度规划误区
规划往往会走两个极端,没有规划、过度规划。
所谓没有规划是在现有的架构、形式下,没有额定考虑过差异、特点,照搬运用。
过渡规划往往是在吃了没有规划的亏后,成了草木惊心,看什么都要搞一堆装备、组合、扩展的规划,简略的反而搞杂乱了,过为己甚。
规划是在质量、本钱、时刻等因素之间做出权衡的艺术。
施行计划
事务开发不能停,一边开发、一边重构,相当于在高速公路上不停车换轮胎,需求有满足的预案、备案,才干确保规划计划顺利落地。
改动评价
先估算一下修正规划、周期:
- 代码修正量:近4万行
- 时刻:半年
改动巨大、时刻很长,风险是难以操控的,每个版别都有许多事务需求,需求改许多的代码,在重构的一起,假如重构的代码和新需求代码抵触,是十分难解的,因而考虑分期。
上面现已屡次提到功用的重要性,需求考虑重构后,功用是否正常,假如出了问题如何处理、如何证明重构后的功用和之前是共同的,对产品数据无影响。
施行战略
基本思路是完成一个新页面,通过ABTest来切换,中心方针无显着负向则放量,全量后删去旧代码,示目的如下:
共分为三期:
- 一期改造内容如上图赤色所示:抽取协议,面向协议编程,不依靠详细类,改造旧VC,完成协议,将协议之外露出的办法、特点收敛到内部
- 二期改造内容如蓝色所示:新建个新VC,新VC和旧VC在功用上是彻底共同,完成协议,通过ABTest来操控运用方拿到的是旧VC仍是新VC
- 三期内容:删掉旧VC、ABTest,协议、新VC保存,完成替换作业
其中二期是重点,占用时刻最多,此阶段需求一起保护新旧两套页面,开发、测验作业量翻倍,因而要尽或许的缩短二期时刻,不要着急改代码,能够将一期做完善了、各方面的规划预备好再开始。
ABTest
2个目的:
- 运用ABTest作为开关,能够灵敏的切换新旧页面
- 用数据证明新旧页面是共同的,从事务功用上来说,二者彻底共同,但实践情况是否契合预期,需求用留存、播映、浸透率等中心方针证明
两套页面的开发办法
在二期中,两套页面ABTest切换办法是有本钱的,需求开发两套、测验两遍,尽管部分代码可共用,但本钱仍是大大增加,因而需求将这个阶段尽或许缩短。
别的开发、测验两套,不简略发现问题,而一旦出问题,即使能用ABTest灵敏切换,但修正问题、从头上线、ABTest数据有结论,也需求十分长的周期。
假如每个版别都出问题,那将会是上线、发现问题,从头修正再上线,又发现了新问题,无限循环,或许一向无法全量。
如上图所示,版别单周迭代,发现问题跟下周修正,那么需求通过灰度、上线灰度(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,直至写了几十个后,发现写不下去了,再推到重构,希望重构一次后,能够坚持的尽或许久一些。
更严峻的是在重构进程中,代码就或许劣化,假如问题呈现的速度超越处理的速度,那么将会一向疲于救火,永远无法彻底处理。
新计划中,事务逻辑都放在了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中心方针负向,是无法放量的,乃至要关掉试验排查问题。
有个共享比方,共享总量、人均共享量都显着负向,大体通过这样几个排查进程:
排查ABTest方针和排查bug相似,都是找到差异,缩小规划,最终定位代码。
- 比照功用:从用户运用视点找差异,交互规划师、测验、开发自测都没有发现有差异
- 比照代码:比照新老两套打点代码逻辑,尤其是进入打点的条件逻辑,没有发现差异
- 拆分方针:许多功用都能够共享,打点渠道能够按共享页面来源拆分方针,发现长按弹出面板中的共享削减,其他来源相差不大,进一步排查弹出面板呈现的概率发现显着变低了,大体定位问题规划。别的值得一提的是,不喜欢不是很中心的方针,而且不喜欢变少,意味着视频质量更高,所以这点是从ABTest数据中难以发现的
- 定位代码:排查面板呈现条件发现,老代码中是在长按手势中,扫除了单个的点赞、谈论等按钮,其他方位(假如没有增加工作)都是可点的,比方点赞、谈论按钮之间的空白方位,而新代码中是将右侧按钮区域、底部共同扫除了,这样空白区域就不能点了,点击区域变小了,因而呈现概率变小了
- 处理问题:定位问题后,修正比较简略,复原了旧代码完成办法
这个问题能考虑的点是比较多的,重构时,看到了欠好的代码,究竟要不要改?
比方上面的问题,增加了功用后,不知道是否应该扫除点击,很简略被疏忽,长按归于底层逻辑,详细按钮归于事务细节,底层逻辑依靠了细节是欠好的,可保护性很差,可是修正后,很或许影响交互体会和产品方针,尤其是中心方针,一旦影响,没有太多探讨空间。
详细情况详细评价,假如预估到影响了功用、交互,尽量不要改,大重构尽或许先处理中心问题,部分问题能够后续独自处理。
下面是长按面板中的共享数据截图,显着下降,其他来源基本坚持共同,就不贴图了。
长按蒙层呈现率下降10%左右,比较天然的猜想蒙层呈现率下降。
比照View视图差异承认问题。
相似的问题许多,ABTest放量、全量进程要有满足的估时和耐性,这个进程会大大超越预期。抖音中心方针简直都和交互区相关,许多剖析师和产品都要重视,因而先了解一下剖析师、产品和开发同学对ABTest方针负向的认知差别。
大部分方针是正向,单个方针负向,那么会被判别为负向。
开发同学或许想的是规划的合理性、代码的合理性,或许从全体的收益、丢失视点的差值考虑,但剖析师会优先考虑不出问题、别有隐患。两种办法是站着不同视点、方针考虑的,没有对错之分,事实上剖析师帮忙发现了十分多的问题。现在的剖析师、产品许多,每个方针都有剖析师、产品担任,假如某个中心方针显着负向,找相应的剖析师、产品评论,是十分难达到共同的,即使是先放量再排查的计划也很难承受,主张自己学会看方针,尽早跟进,关键时找人帮忙推动。
概率性呈现的问题
概率性呈现的问题难点在于,很难复现,无法调试定位问题,修正后无法测验验证,需求上线后才干确定是否修正,举一个实践的比方的iOS9上crash比方,发现进程:
- 通过slardar=>AB试验=>指定试验=>监控类型=>溃散 发现的,能够看到试验组和对照组的crash率,其他的OOM等方针也能够用这个功用查看
下面是crash的仓库,crash率比较高,大约50%的iOS9的用户会呈现:
crash仓库在体系库中,无法看到源码,仓库中也无法找到相关的问题代码,无法定位问题 ,整个处理进程比较长,尝试用过的办法,供我们参阅:
- 手动复现,尝试修正,能够复现,但刷一天也复现不了几次,功率太低,对部分问题来说,判别准的话,能够比较快的处理
- swizzle体系溃散的办法,日志记载最终溃散的View、相关View的层次结构,缩小排查规划
- 主动化测验复现,能够用来验证是否修正问题,无法定位问题
- 逆向看UIKit体系完成,剖析溃散原因
逆向大体进程:
- 下载iOS9 Xcode & 模拟器文件
- 提取UIKit动态库
- 剖析crash仓库,通过crash最终所在的_layoutEngine、_addOrRemoveConstraints、_withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists 3个关键办法,找到调用路径,如下图所示:
- _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,大意是投入和收益比率,投入越少、收益越高越好,最好是空手套白狼,这个词指导了许多决策。
重构无疑是个费力的作业,需求投入十分大的心力、时刻,而能看到的直接收益不显着,一旦改出问题,还要承担风险,重构也很难取得其他人认可,比方在产品看来,功用彻底没变,代码还能跑,为什么要现在重构,新需求还等着开发呢,有问题的代码便是这样不断的拖着,越来越严峻。
固然,有满足的痛点时重构是收益最高的,但仅仅看起来,真实的收益是不变的,在这之前需求许多额定的保护本钱,以及劣化后的重构本钱,从长期收益看,已然要改就趁早改。决议要做比较难,压服我们更难,每个人的了解或许都不相同,对长期收益的判别也不相同,很难达到共同。
思者众、行者寡,未知的作业我们偏向慎重,支撑继续前行的是对技能寻求的勇气。
重构最好的时刻便是当下。
部分重构,积少成多,最终全体完成,即使出问题,影响也是部分的,这是自下向上的办法,自身是没问题的,也常常运用,与之对应的是自上向下的全体重构,这儿想着重的是,部分重构、全体重构仅仅手法,选择什么手法要看处理什么问题,假如根本问题是全体结构、架构的问题,部分重构是无法处理的。
比方这次重构时,十分多的人都提出,能否改动小一点、慎重一点,可是规划计划是通过剖析整理的,现已明确是结构性问题,部分重构是无法处理的,从前那些尝试过的办法也证明了这一点。
不能由于怕扯到蛋而忘记奔驰。