AcFun俗称为“A站”,作为一款二次元内容社区产品,以“认真你就输了”为文化导向,倡导轻松欢快的亚文化。AcFun涵盖了中长视频,小视频,番剧,文章等众多内容,支撑这些内容的大部分功能都选择了用Swift开发,早在2019年,AcFun的iOS技术团队就已将Swift作为AcFun app和水母app的开发首选语言。Swift的出现为用户提供了更稳定的使用体验和更快的产品更新节奏,同时也为研发工程师创造了更高效舒适的开发体验。Objective-C已成过去时,AcFun正全面拥抱Swift,驶入iOS开发快车道。
谷歌作为苹果最大的竞争对手,除了Android上的Kotlin,还推出了Flutter和Fuchsia里在用的Dart语言,这些语言的口碑和易用性远高于苹果的Objective-C(后面简称OC)。OC历史久远,是C语言的超集,因此其发展受C语言限制。在这样的背景下,大家都以为苹果会忽视其它新语言,但其实苹果对于那些新语言特性垂涎已久,将想法施于行动的是当时还在苹果的 Chris Lattner。Chris是狂热的编译器和编程语言爱好者,C、C++、OC语言编译器LLVM的创造者,在LLVM开发过程中,Chris对类C语言有着很多不满意的地方,比如预处理器、Trigraphs还有多年积累的奇怪东西。
为了能够兼顾类似React一样的编程范式和具备Java等正流行的语言的优秀特性,Swift经历了长期的ABI稳定和语言特性迭代增加的过程,最终推出了能和JSX声明式UI匹敌的Result Builders,并且通过SwiftUI和Combine这种能极大提升开发效率的框架让开发者收获了惊喜。
可能是Swift的ABI稳定得太晚,不止各大APP里已经积累了大量的OC库和业务代码,苹果系统里的OC占比也依然很高,博客《Evolution of the programming languages from iPhone OS 1.0 to iOS 14》[1] 统计了 iOS 历史版本 OC 占比,从文章中可以看到最近的iOS 14版本里OC占比高达88%,C和C++主要用于音视频、电话、网络等比较基础的模块,其占比相对稳定,特别是C并没有明显增加。不过在最近几个版本中,Swift占比持续增高,iOS 14达到了8%,可以看出苹果正在使用Swift重构以前的库。
为了让广大开发者能够用上更方便安全的Swift,苹果采取了一系列实际行动。比如不再给OC新加接口,而是用Swift替换SDK,WWDC17之后就已经看不到OC的例子了,苹果主推的一些前沿技术,比如AR、AI、Health等,在新版里也都只有Swift版本。所以,在未来的发展中,企业不考虑Swift或是缺少Swift人才,都将会影响到新技术的引入。
另外,苹果的RealityKit、CareKit、Create ML、System、WidgetKit、CryptoKit、Combine、SwiftUI等框架在与OC混编时都非常困难,从这些方面可以看出,苹果所有新开发的框架都在避免和OC产生关系,甚至自WWDC2020起新增加的App Widget只能用SwiftUI开发。
对于苹果一系列的行动,社区与之对应的反应是没有热情去回答OC的Bug了,因为有了更好的追求。OC三方库作者也没有维护的意愿,更新周期比Swift长很多,比如大家都知道的OC网络库AFNetworking,最新版本更新用了2年多时间,而该作者用Swift开发的对应的网络库Alamofire,更新频率接近半个月,作者对Swift的热情可见一斑。
iOS开发首选语言也是Swift,以后可能会面临OC工程师后继无人的局面,物以稀为贵,OC开发者的成本也会大增。使用Swift相关技术栈的团队在吸引人才方面也存在一定优势,AcFun的工程师田赛同学此前选择了快手,而拒绝了另外一家公司的offer,一个重要因素就是AcFun可以使用Swift开发。AcFun的iOS开发工程师关旭航说:“起初我们团队在业务开发中探索Swift时,对Swift不够熟悉,并不敢主动尝试使用,通过组内的培训以及业余时间的学习,我对这门语言越来越感兴趣,看到其他同学写的Swift代码既简洁又易懂,我也慢慢开始尝试使用,现在我已经不想写Objective-C了”。
按照目前这个趋势,使用Swift势在必行。
2019年AcFun完成了Swift的调研和初期基础设施建设,团队Swift培训以及业务的试点。在Swift调研探索过程中,AcFun开发同学体验到了Swift的优雅、精简以及安全,也经历混编构建时间长和代码补全慢等问题。其中构建问题只要遵循官方Module的最佳实践就可以规避,代码补全问题在Xcode12中得到了很好的改善。2020年上半年AcFun开始了混编工程优化、组件化以及二进制化建设,借LLVM Module抹平模块API在语言上的差异,基础库进行了Module化问题修复,并基于主站二进制化方案,完善了对Swift混编的支持。目前二进制化率为80%,约50%的组件完成了LLVM Module化,构建速度提升了60%以上。
AcFun 当前的 Swift & OC 混编架构如下图所示。Infra层包含自研基础库、快手系中台SDK以及第三方库。Business Support层为各业务Feature提供通用业务支撑。Business Modules层包含当前已完成解耦和Module化的业务模块,模块之间通过依赖注入容器和路由进行通信。当前Main Target中仍然存在尚未解耦的混编代码,OC 和 Swift 之间通过桥接进行交互,另外有一些尚未Module化的OC基础库仍然需要通过Bridging Header桥接给Swift使用。这些桥接是影响编译时间以及代码补全速度的主要因素。
随着架构的演进和组件化的推进,未来理想目标架构愿景如下图所示。Infra 和 Business Support 层为业务提供更完整的基础和通用业务支撑,业务模块全面解耦、Module化和二进制化,组件均以Module的形式组织和聚合,Main Target 实现壳工程化。
目前AcFun的Swift文件数占工程总数40%之多,崩溃率减少了52%。AcFun采用混编后,性能方面,比如启动时间、页面流畅度、内存、CPU/GPU负载等方面差别不大。AcFun的QA负责人邵国强不禁感叹:“AcFun的移动端研发同学开始探索Swift时,QA团队起初没有明显感知,但随着研发团队Swift建设的推进,发版频率也提速到单周以后,研发同学能持续高质量交付,真是太棒了。”
Swift的内存管理是通过严格的、确定性的引用计数来自动管理的,可以将内存的使用量降到最低,还可以避免垃圾收集在错误线程使用Finalizer,执行多次不能管理数据库句柄之类资源的问题。ARC的Retain和Release开销在垃圾收集里也会有,比如在存储一个对象属性时用Write Barrier。ARC的算法类似Go的Tricolor算法。垃圾回收还会移动和压缩对象,如果调用C代码,可能还会得到一个Dangling Pointer,比如JNI,就明确需要引入和维护对象,无形中增加了复杂度,还很容易出问题。
在atp播客205期节目中 Episode 205: Chris Lattner Interview Transcript [2],Chris Lattner 指出OC之所以不安全的原因是因为OC是基于C语言,有指针,有不完全初始化的变量,会数组越界,即使对工具链和编译器有完全的控制权,也无法很好地解决以上的问题,解决Dangling Pointer就需要解决生命周期问题,而C没有一个框架能解决,改成兼容方式进入系统也是行不通的。因此苹果团队经过思考,决定创建一门“安全”的编程语言,这种安全不止是指没有Bug,而是在保持安全的同时还能够保证高性能,进而推动整个编程模型前进。
Swift消除了整个类别的不安全代码。变量在使用前总是被初始化,数组和整数会被检查是否有溢出,内存会被自动管理,对内存的独占访问可以防止许多编程错误。
Swift有静态调度安全的特性,比C语言更安全,很多问题能在编译时提前发现。代码中发生内存溢出,编译器会发出诊断信息,比如常量中的内存溢出很难查。数组越界检查,还有函数返回可达性检测,确保返回值和函数定义的类型一致。
编译器中的类型安全性可以让问题更早暴露。例如Swift Optional的设计在编译期阻断了空值访问,又如利用范型类型推导在编译期提供约束,从而避免Unsafe Type Casting。水母的研发工程师赵赫在使用Swift过程中,发现代码中的很多问题和隐患都可以在编译期暴露出来,在大部分情况下代码只要能编译通过,运行效果就不会离预期有很大的偏差,这让他对其代码交付质量更加充满信心。
Swift的演进比较稳定,并没有在初期版本一股脑把特性都加上,而是每个版本迭代增加特性。演进之路如下图所示:
Swift第一个版本推出了基本语法,Swift2.0主要是将泛型和协议能力做了提升,并对Linux进行了支持,后端框架Vapor和Perfect也是在Swift2.0时出现的,Swift也是在这个版本开源的。Swift3.0出了Swift Package Manager,对标准库API进行了重新的设计。4.0 推出Codable协议和Key path。5.0终于ABI稳定,Swift运行时内置到了iOS12系统里。5.1版本推出了让大家感到苹果活力的SwiftUI和Combine,新增了一大堆围绕提升开发舒适度的Property Wrapper、Opaque Type等语言特性,随之,社区开始异常活跃起来,与之对应的技术文章大量输出。AcFun就使用了5.1版本的Property Wrapper包装了UserDefaults,Codable,RxSwift Relay等,业务开发过程中避免雷同代码的编写。
Swift 6的 Roadmap[3] 表明了Swift下一步发展方向是优化Swift部署安装,比如LSP和包管理等;丰富开源生态,包括完善标准库,开发类似科学计算这样的新库;围绕开发体验的构建和代码补全提速、丰富诊断信息、稳定调试体验等;DSL能力提升;完善低级别系统编程和机器学习等重要领域的拓展;提供内存所有权和并发等主要语言特性的方案,要做到出色为止。
目前Swift这个项目的负责人叫 Ted Kremenek[4],斯坦福博士,他之前还是Rust的主力开发。在苹果工作的十年,一个人做了Clang的静态分析器,后面一直管理着Clang和Swift项目,向Chris汇报。Swift项目团队核心成员还有Dave Abrahams(已退出)、John McCall、Doug Gregor、Joe Groff、Saleem Abdulrasool(移植Swift到windows)、Tom Doron(创建SwiftNIO)等,他们的身影活跃在Github的Swift各个提案中。
回到我们身边,国内Swift用的情况怎么样?
一些耳熟能详的App,比如微信、淘宝、百度、支付宝、拼多多、京东、哔哩哔哩、优酷、小红书等都已经开始尝试使用Swift,这些App无一例外都采用了Swift和OC混编开发。由于国内业务竞争压力大,很难像国外公司Uber那样花大半年时间全部用Swift重构,因此如果要在现有工程基础上引入Swift开发,不可避开采用混编开发。很多App使用Swift混编,也是因为苹果对Widget功能开发语言设置了限制,即只能使用Swift,看来苹果公司这个策略是相当有效的。
而正式进入混编开发前,需要先做开发框架的选型,我们先从架构演进开始说起。
一般App经过多年发展,架构都会经过如下四个阶段:
如图所示,App架构从单Module,MVC架构到几百个Module,无依赖,动态跳转。团队从小变大,如今App的架构更偏重高质量、稳定性和高可维护性。苹果公司也是顺应发展趋势,先后推出提高稳定性的Swift语言,而后推出提高可维护性的SwiftUI和Combine。
对于一个基于UIKit的项目是没有必要全部用SwiftUI重写的,在UIKit里使用SwiftUI的视图非常容易,UIHostingController是UIViewController的子类,可以直接用在UIKit里,因此直接将SwiftUI视图加到UIHostingController中,就可以在UIKit里使用SwiftUI视图了。
SwiftUI的布局核心是 GeometryReader、View Preferences和Anchor Preferences。如下图所示:
SwiftUI的数据流更适合Redux结构,如下图所示:
如上图,Redux结构是真正的单向单数据源结构,易于分割,能充分利用SwiftUI内置的数据流Property Wrapper。UI组件干净、体量小、可复用并且无业务逻辑,因此开发时可以聚焦于UI代码。业务逻辑放在一起,所有业务逻辑和数据Model都在Reducer里。ACHNBrowserUI[5] 和 MovieSwiftUI[6] 开源项目都是使用的Redux架构。最近比较瞩目的TCA(The Composable Architecture)也是类Redux/Elm的架构的框架,项目地址见[7] 。
提到数据流就不得不说下苹果公司新出的Combine,对标的是RxSwift,由于是苹果公司官方的库,所以应该优先选择。不过和SwiftUI一样,这两个新库对APP支持最低的系统版本都要求是iOS13及以上。那么怎么能够提前用上SwiftUI和Combine呢?或者说现在使用什么库可以以相同接口方式暂时替换它们,又能在以后改为SwiftUI和Combine时成本最小化呢?
对于SwiftUI,AcFun自研了声明式UI Ysera,类似SwiftUI的接口,并且重构了AcFun里收藏模块列表视图和交互逻辑,如下图所示:
通过上图可以看到,swift代码量相比较OC减少了65%以上,原先使用Objective-C实现的相同功能代码超过了1000行,而Swift重写只需要350行,对于AcFun的业务研发工程师而言,同样的需求实现代码比之前少了至少30%,面对单周迭代这样的节奏,团队也变得更从容。代码可读性增加了,后期功能迭代和维护更容易了,Swift让AcFun驶入了iOS开发生态的“快车道”。
SwiftUI全部都是基于Swift的各大可提高开发效率特性完成的,比如前面提到的,能够访问只给语言特性级别行为的Property Wrapper,通过Property Wrapper包装代码逻辑,来降低代码复杂度,除了SwiftUI和Combine里@开头的Property Wrapper外,Swift还自带类似@dynamicMemberLookup[8] 和@dynamicCallable[9] 这样重量级的Property Wrapper。还有ResultBuilder[10] 这种能够简化语法的特性,有些如GraphQL、REST和Networking实际使用ResultBuilder的范例可以参考[11]。这些Swift的特性如果也能得到充分利用,即使不用SwiftUI也能使开发效率得到大幅提升。
网飞(Netflix)App已使用SwiftUI重构了登录界面,网飞增长团队移动负责人故胤道长记录了SwiftUI在网飞的落地过程,详细描述了SwiftUI的收益[12]。网飞能够直接使用SwiftUI得益于他们最低支持iOS 13系统。
不过如最低支持系统低于iOS 13,还有开源项目AltSwiftUI[13] 也实现了SwiftUI的语法和特性,能够向前兼容到iOS 11。
对于Combine,也有开源实现OpenCombine[14],目前都未完全实现所有特性。因此,具体在工程中使用还是需要了解Combine的核心原理。
Combine的灵感来源于RxSwift。RxSwift的核心,这里有份实现了RxSwift核心逻辑的简版样例代码[15],可以窥视其核心逻辑。整体流程如下图:
如上图所示,RxSwift整体流程非常简单,主要就是订阅者和发布者之间进行订阅、发布、取消操作,订阅者会监听和处理这些事件。具体RxSwift数据传递关系如下图:
上图中的Observable是发布者,Observer是订阅者。取消订阅是通过CompositeDisposable来进行管理,管理方式就是加个中间订阅者来决定是否发送事件给原订阅者。SinkDisposable是一个中间层用来把中间订阅者和原订阅者还有事件转发的逻辑放到一起。新增一个操作符就会新增一个SinkDisposable,比如新增filter操作符就会新增FilterObserver和FilterObservable,如果没有操作符就是AnoymousObserver和AnoymousObservable。订阅是通过Disposer类来管理的,会判断是否完成或者出错,执行Dispose方法。
Combine的思路基本和RxSwift一样,只是接口命名不同,这里有份表格[16],列出了Combine和RxSwift功能的对应关系,可以看出目前Combine相较于RxSwift还缺少很多能力,Combine毕竟新生儿,还需要时间成长。但是Combine有个特性是RxSwift没有的,那就是Backpressure,Backpressure可自定义策略控制Subscribe能够接收的数量。
除了SwiftUI和Combine,在Swift开发中还有哪些库是可以直接拿来使用的呢?这里有份 Swift开源库的awesome[17],在这里可以查缺补漏。AcFun主要使用了Swift开源库有Protobuf[18], RxSwift[19], Cache[20], Observable[21]。
以上,为《A站 的 Swift 实践》的上篇内容,下篇我们会继续详细介绍OC和Swift是怎么混编的,以及Swift的动态性。
快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。
在这里你可以获得:
提升架构设计能力和代码质量
通过大数据解决用户痛点的能力
持续优化业务架构、挑战高效研发效能
和行业大牛并肩作战
我们期待你的加入!请发简历到:
[1]https://blog.timac.org/2020/1019-evolution-of-the-programming-languages-from-iphone-os-to-ios-14/
[2]https://atp.fm/205-chris-lattner-interview-transcript
[3]https://forums.swift.org/t/on-the-road-to-swift-6/32862
[4]https://twitter.com/tkremenek
[5]https://github.com/Dimillian/ACHNBrowserUI
[6]https://github.com/Dimillian/MovieSwiftUI
[7]https://github.com/pointfreeco/swift-composable-architecture
[8]https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md
[9]https://github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md
[10]https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md
[11]https://github.com/carson-katri/awesome-result-builders
[12]https://mp.weixin.qq.com/s/oRPRCx78owLe3_gROYapCw
[13]https://github.com/rakutentech/AltSwiftUI
[14]https://github.com/OpenCombine/OpenCombine
[15]https://github.com/sergdort/HandMadeRx/tree/master/HandMadeRx.playground/Sources
[16]https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet
[17]https://github.com/matteocrippa/awesome-swift
[18]https://github.com/apple/swift-protobuf
[19]https://github.com/ReactiveX/RxSwift
[20]https://github.com/hyperoslo/Cache
[21]https://github.com/slazyk/Observable-Swift