原文在这儿
Uber架构总工程师Tuomas Artman谈了iOS端使用Swift重构过程中遇到的问题,以及一些经验成果总结。
Swift with a hundred engineers - 动机,架构和收获
和一百个工程师写Swift代码意味着什么。
选择Swift的动机,架构。简要陈述。
这次大规模重构的收获。重点阐述。
Uber's beginning - 为什么重构
4年前整个移动端团队只有三个工程师,随着团队的扩张和人员的几何级增长,我们开始看到架构的破碎以及特征开发(feature development)将会变得相当困难。由于我们在不同的团队里共享了许多view controllers,我们不得不测试多个代码路径。我们真的开始被老的架构刺痛,因为它是由两个工程师书写的,然而现在我们的团队已经超过百人。同时我们想要重新设计乘客端app的整体用户体验。基于以上这些,我们决定不再修补原有的架构,而是突破原有架构重新开始。
Architectural goals for rewrite - 可靠并且能够支撑Uber的特色
四条可靠的中心流,低崩溃率,不能卡死在某个界面,能够支持Uber未来几年的发展。
我们选择Swift
我们认为编译器的类型安全将会帮助我们更早地捕捉到问题,而不是通过在正式产品使用时崩溃显现出来。
并且我们知道未来四年Swift将会走进巅峰时期,成为苹果公司唯一倚重的语言。
Timeline
从二月开始,我们花了五个月时间,召集了平台团队的核心工程师们,开始关注架构,这五个月没干别的事情;架构基础差不多完成了。
六月,我们认为已经有了一个好的架构,核心流团队开始运行。我们一起花了两个月时间审查这个架构,尽量确保我们提出的架构适合构建产品。结果证明我们遗漏了一些东西。
比如在视图层面,一旦工程师们开始做转场操作和复杂的视图操作,我们就不得不稍微改变这个架构来满足他们的需求。但是两个月后我们感觉架构的基础代码已经稳定,因此我们向所有人开放了这个平台,告诉他们如果想要的话就可以移植他们的功能。
十一月,我们成功发型了新版本。
Uber的架构
我们叫它“Riblets”,意思是:Router, Interaction and Builder and possibly a Presenter and a View。
这些是App的核心部件。有点像VIPER。我们关注了MVVM,VIPER,MVC,最终我们选择了基于VIPER的创新架构。我们想要做的能够划分所有,能够测试所有。因此在一个Riblet里的每一个部件都有协议接口,我们可以单独拎出一个单元并完美地测试它。所有的Riblets被组织起来成为一个树形结构。因此我们没有状态机,我们有一个状态树。每一个方框代表一个Riblet,并且我们架构的核心不是基于视图的,而是基于逻辑的。我们想要所有的业务逻辑决定都是局部的。
uploads-1491903532242-Tuomas+image+1.png
每一块业务逻辑都是独立的,只需要关注自己的问题,单独做决定,不需要关心其他的事情。也许会有其他模块在监听你的业务逻辑,但是你不知道它在哪儿,知道了也没什么用。
比如“App”这个部件,它只关心一件事情:我们是否有一个session token。如果没有就导向“Welcome”部件,然后在某一时间点,有了session token,就会中断“Welcome”部件,转向“Bootstrap”部件。
许多许多文件,许多许多行代码
所有这些创建了许多代码。我们在所有部件之间都有协议。在我们的代码库中有五百个以上的文件,超过五十万行的Swift代码。
关于Swfit的收获
我们学到了一些关于Swift的好的,不好的以及丑陋的东西。
好的地方
我们相信Swift是一个更好的语言,我们使用了Swift的所有语言特色。
可靠性
四个月的架构开发期间,我的IDE和应用程序竟然没有崩溃过!我的团队其他成员也一样。直到整个架构开发完成也没有遇到崩溃的情况,即使是调试模式。第一次崩溃发生在,我们尝试32位设备时,解析JSON数据,发生了整数溢出。这是整个开发过程中第一次遇到崩溃。
我们发布了重新架构的App版本,最终崩溃率在0.01%左右!这对于一个全新的App来说,以前从没有这种情况。
我们确保没人能够无条件地解包任何东西,如果你这么做了,你会被抓住,并且无法提交修改,这奠定了一个良好App的基础。
Android engineers are now more welcome
我们的架构是跨平台的,iOS和Andriod完全一样,特别是如果Andriod工程师如果使用Kotlin语言的话,更容易接受。如果使用Objective-C,我不认为能够这样。
坏的地方
这是从错误中学习的最有趣的地方。
测试很困难
Swift是一个静态语言,因此你不能像OC一样依赖你的mock框架去测试。并且由于所有类都是基于协议的,我们必须找到一些方法测试这些协议。我们必须自己来写mock测试这些协议。
我们写了一个脚本来自动生成mock。只需要敲下“script/generate-mocks”,就能搜索整个源码文件,在协议顶端如果有@CreateMock声明,就会为你创建mock。
工具问题
另一个不好的地方,我们称它“无尽的索引”,你可能见过。索引器一直在运转,代码量越大越明显,伴随着超高的CPU负荷,笔记本会变得很热,如果不插电源,只能使用一个半小时。这时在Xcode里输入会变得特别困难。
因此你能做什么呢
你可以删掉Xcode……然后转向AppCode。或者在AppCode里面编辑代码,然后粘贴到Xcode里进行编译。
我们把应用拆分成多个framework,这样每个framework中文件数量会少点。似乎一个framwork里面文件越多,工具问题就越明显。
因此起初我们定义架构时就已经拥有了多个framework。总共大概七八十个。
或者你也可以禁用indexing,如果你想在没有代码补全的情况下写代码的话。
二进制文件大小(binary size)
下一个不好的地方,二进制文件的大小。打包后每个app差不多都得100M。
这里有几点需要注意:
structs会增加包的大小
如果你在列表里存储了一些结构体,他们会增加你的二进制文件大小。起初我们所有的模型都是结构体,结果二进制文件大小差不多80M,这不太好。
可选类型(Optional)的使用也会增加二进制的大小。编译器必须做许多事情,必须检查,必须解包。因此即使你只使用了一个问号,在二进制文件里会增大许多。
泛型转化是我们遇到的另一个问题。无论何时你使用泛型,如果你想要泛型更快速,编译器会指定他们,并且增加二进制的大小。
并且Swift运行库需要被包含到你的App里。所有人都说那个差不多12-20M。真实的下载尺寸是4.5M,至少我们是这样,因为它们压缩的很好,还没有加密。因此4.5M是我们测量所得的尺寸。
因此你能做什么呢
你或许可以看看优化设置。打开全模块优化,有时会减小包体积,大多数时间会更大。你需要知道这些体积都从哪儿来。
启动速度
下一个不好的事情,启动速度。通常二进制中动态库的数量直接线性影响mian函数之前的启动速度。因此启动速度分为Pre-main和Post-main。Pre-main是main函数执行前发生的事情。如果你有许多动态库,那么会发生许多事情,许多时间被花费掉。
例如,在一台iPhone6s上,Swift运行时库花费250毫秒去做它的事情,意味着250毫秒之内肯定启动不了。真是无奈。
虽然我们拆分framework解决了工具问题,但是越多的framework意味着越长的启动时间。
因此真的你能做什么呢
好吧,你可以重新链接所有东西到你的二进制文件里,我们就是这么做的。我们编译了所有这些framework,然后我们有一个编译后的步骤,提取所有这些framework的标志符,然后在静态库里面链接他们。这样我们解决了启动速度的问题。
在iPhone6上,不这样做我们的应用启动需要4-5秒。你不能依赖Xcode提供的那些工具。
你也会遇到企业PP文件问题。如果你的设备上有企业PP文件,你的应用可能会花费10秒来加载,取决于文件的多少。
你如果重新链接,那么post-main运行时间会增加。
丑陋的地方
编译速度着实恐怖。我们的基础应用,clean built大约需要15-20分钟。
解决编译速度问题
我们尝试在代码里不使用类型推断。
最后我们开始整合文件。我们发现把所有的200多个model整合到一个文件中,使编译时间从1分35秒降到了17秒!据我所知是因为,编译器为每个单独的文件执行类型检查。
全模块优化做了我们想要做的事情。它会一次编译所有文件。
The problem with whole module optimization is that it optimizes, so its pretty slow. But if you add a user-defined custom flag; SWIFT_WHOLE_MODULE_OPTIMIZATION and set it to yes, as well as set the optimization level to none. It will do whole module optimization without optimizing. And it'll be super fast.
这样做以后,我们的编译时间从20分钟降到了6分钟!
Uber is contributing to Facebook's 'Buck' and adding Swift support
通过Buck你可以继续减少编译时间。
网友评论