原文:Comedy Central iOS App: Learnings on the bleeding edge
痛下决心决定重写
在码农界最恐怖的词就是“重写吧”。你可以想象当技术团队做出重写的决定后,产品经理两眼放光,以为从此以后每个界面60fps,bug和头屑一样不见,领导对此寄予厚望,然而有时事实却没你想的那么顺利。
移动开发技术发展很快。每个秋天开发者们都要面对一个新版本的操作系统和新的工具。让用着老代码的app和新系统一起飞的难度越来越大。开发者发现他们忙着兼容老版本甚至都没有什么时间研究新特性和找女朋友。
一些老的第三方库还在使用过时的Restkit和XML格式的数据流。一些依赖着在iOS 9中已经被废弃的NSURLConnection(被NSURLSession取代)。如果你提前就做好了准备,有着很高的抽象层次没有和具体实现代码耦合,你还可以不费劲的迁移替换它们,然而更多的时候不是这样。
我们设定了一个目标:做出一个灵活可拓展的app架构。我们仔细看了这些老app们的代码,区分出哪些是还能用、哪些不能用需要重写、哪些写的不错可以抽取出来重用。
就像很多旧项目,feature一个接一个,随之不断增加的是欠下的技术债。从前端到后端都是一团乱麻。我们的feed流的架构也是一样惨不忍睹。
除了以上提到的几点原因,服务端要发布一套新的、现代的灵活的API更加坚定了我们的态度。在这种不同意就弄死你的强硬态度下,项目经理最终同意我们重写这个项目。
是否选择Swift
大概一个星期前,Swift Evolution的邮件列表里有一封吸引了我的目光。标题是“这次不骗你了,3.0发布后稳定两年!”。(It pled in its subject line “Seriously! Freeze Swift For Two Years After Release 3.0 !”)
这个项目开始的时候我们使用Swift 1.1。现在,Swift 2.3和3.0正处于beta。每一次新版本的发布都让人痛苦不堪。语言经常让我们改变底层的代码。调试和重构变得更加困难,编译的时间也增加了。(compiling times have increased due to the removal of native method currying)
但是优点是,Swift减少了项目里的文件数量。它很简洁。它的类型系统和泛型可以让我们少写很多代码。它也很适合函数式响应式编程范式( Functional Reactive Programming)。Swift的标准库已经提供了大部分你需要用到的功能。我们都认为Swift使用起来比OC更简单。
实践现代编程理念和框架
大多数的iOS应用使用MVC架构。不过考虑到我们的需求我们选择了MVVM(Model View ViewModel)。
我们同样也将函数式响应式编程引入到了项目中。尤其在数据层我们大量使用了它。我们使用了 Promises,一个在并发编程语言(比如scala)里使用的概念。
说到这就不得不提两个开源库:PromiseKit 和 ReactiveCocoa。
PromiseKit是一个实现了很多实用函数的库。它将Promises的模式带入了iOS并且使复杂代码的意图变得清晰。它能用更简洁的语法管理同步和异步的代码执行。它线程安全并且对并发编程有很多易用的辅助方法。
ReactiveCocoa是一个在iOS上进行函数式响应式编程的库。我们用它来绑定数据和UI。也用它的流式抽象iOS事件机制(streamlined abstraction of the underlying iOS eventing mechanisms)。我们选择了ReactiveCocoa而不是RxSwift,因为团队成员更早接触了ReactiveCocoa。而且在那个时候RxSwift刚刚发布不就,我们觉得太新了。
当使用闭包和添加listeners时,PromiseKit有时会出现内存管理问题。PromiseKit在调试时有时会让代码的调用链变得混乱。
这些理念有一定的学习难度,但是长远来看,是值得花费精力去掌握它们的。
不要依赖太多第三方库
每次在我们下载了新版的Xcode之后编译项目,时常会跳出错误。然而有些错误却不是我们能够解决的。Swift迁移工具虽然能做不少工作,但是也不是万能的,我们依然会看到一个像这样的错误:
你没有别的办法,只能手动修改这些库的源码让这些第三方的库跟着Swift的升级步伐集成进项目中。
第三方库们,比如PromiseKit和ReactiveCocoa有着强大的社区支持着它们。他们的团队会在Swift beta期间就解决这些issue并且快速更新。但是并不是你选择的所有的第三方库都能做到这样。所以引入一个第三方库之前要考虑清楚是不是真的需要它。
同时,你应该限制引入的第三方库数量。我们发现引入大量的第三方库依赖后会导致app启动时间变慢。苹果建议引用的外部依赖数量不要超过6个。
选择适合你的布局方式
Xcode的所见即所得工具Interface Builder和Storyboards在不停的改进。它让新手也可以轻松的创建界面布局。但是这并不意味Xib就是所有情景下的都是正确选择。
Storyboards会减慢iOS开发。它对于解决merge的冲突很不友好。也会限制你的流程,界面,业务逻辑在同一个地方。这样会让可移植性下降。
我们想要创建一个这样的应用,UI尽可能是数据驱动并且是动态的。Storyboards不能做到这点。我们用一个叫 PureLayout的库。将我们的界面构建成模块以支持复用,让代码像一块块拼图可以轻松的复用。
了解苹果的最佳实践
业务需要常常会限制正确的技术选型。然而你需要了解苹果的最佳实践并且尽快遵循它。
有限的旧版本操作系统兼容。苹果的推荐是兼容当前版本的前一个版本。比如现在的版本是9.3,那就只支持到8.4。
把警告当做error,在发布前解决所有的警告。在Xcode8中可以设置把警告当做错误对待。
尽早的进行性能测试,并且经常进行。你越早发现一个问题,就会在解决它的时候花费更少的时间。在真机上进行测试总是比在模拟器上测试好。并且在app支持的最老设备上进行测试。
使用Asset Catalogs。确认你的图片资源支持了所有的分辨率。将3x的图片使用在2x的设备上使设备的内存增加。如果图片资源大,这种瞬间的内存增加可能会让iOS直接杀掉你的app。还是老老实实准备2x、3x的图吧。这里强烈推荐一个WWDC上的session:Improving Existing Apps with Modern Best Practices。
支持TLS。http请求只支持到2016年底。
建立自己的最佳实践
我们觉得我们打造了一个迄今为止最棒的app。同时也总结出了一些我们自己的最佳实践。
使用最高的抽象等级。用抽象管理异步操作,比如使用Promises。用抽象管理事件、绑定和流,比如使用ReactiveCocoa。(Manage asynchrony with an abstraction such as Promises. Manage events, binding and streams with an abstraction like ReactiveCocoa.)暴露出核心的功能,比如你的数据层。将功能和责任分离。在view上保持尽量少的状态,尽量让每个view无状态。
保持动态性。让数据驱动app。这可以让你更灵活的控制app。
遵循Gitflow。给每个发布版本标记tag,在发布前锁定pod的版本。在开始一个重大的功能前参加结构设计讨论。完成之后组织一场专门的code review。
采用自动化发布策略。盯紧错误统计就像一支鹰一样,关注是否有着潜在的问题。每个错误都可能在市场上产生巨大的影响,要十分在意。检测重新安装app,考虑是否需要保存用户的设置。
创建自动化测试
MVVM本身就对测试有着良好的支持。很容易模拟数据。你可以在自动化UI测试中展示界面。然后单独测试ViewModel。
我们的数据和网络层有着很高的测试覆盖率。这很好的保证数据获取和解析的稳定性。
我们用Python创建了一套服务端测试架构。我们也创建了一套基础的自动化测试服务端的返回数据。下一步,我们会通过比较界面截图创建一套app的UI测试。使用DVR重现然后在比对中移除实时的数据(And then use DVR replay to remove live data from the equation)。
我们将app分成了很多个部分,将一些部分做成可重用的组件并且用pod管理,我们创建了更多的单元测试,尽量提高这些组件的测试覆盖率。
最终,我们可以向其他成员展示我们的测试结果。然后我们可以安心的研究iMessage extensions这样的新特性,愉快的做一些酷炫的新功能。
欢迎关注我的微博:@没故事的卓同学
网友评论