本文为『移动前线』群在 4 月 8 日的分享总结整理而成,转载请注明来自『移动开发前线』公众号。
嘉宾介绍
胡波,来自于网易杭州研究院,之前在网易杭研移动应用部参与网易公开课 / 网易看游戏 / 网易云阅读 / 基础组件等产品的开发,目前主要在网易杭研漫画产品部负责网易漫画的 iOS 端开发。
网易漫画 App 是网易杭州这边最早采用 Swift 的产品,今天主要为大家分享下网易漫画 App 在 Swift 上的实践。
主要内容:
- 使用 Swift 历程?
- Swift 混编实践
- 基于 Swift 的架构演变及建议
1. 使用 Swift 历程?
在公司量级产品中尝试新的不稳定技术其实是风险比较高的而矛盾的。一方面有来自产品的需求,需要保证产品的稳定性及快速迭代,另一方面有来自技术人员迫切想使用新技术的需求。因此,我们转到 Swift 也是逐步尝试地过程。
- Swift 1.0 Beta 版本发布到 1.2 版本,中间经历了各种 Beta 版本的迭代及改进,Swift 其实并不稳定。 ----> 考虑到产品稳定性及迭代周期,我们对是否采用 Swift 还持保留态度。
- Swift 1.2 版本发布后,Swift 在编译速度、类型安全及稳定性上进行了进一步的改进。同时 Objective-C 增加了 Nullability 等特性以增强对 Swift 交互的支持。 ----> 我们也决定先小范围尝试 Swift 混编,主要是比较独立的、不涉及网络请求的独立模块,比如一些自定义 View,但此时我们对 Swift 的写法也是停留在 Objective-C 面向对象的 Swift 重写。
- Swift 2.0 版本发布,Swift 增加了一些新特性,如面向协议编程范式、Guard、Defer、异常处理等等。Objective-C 也增加轻量级泛型支持和 __kindof 关键字。 ----> 基于之前版本的小范围尝试,我们认为 Swift 和 Objecitve-C 混编也没有太大的技术门槛,至少从 1.2 版本到 2.0 的过渡,我们并没有花费过多的时间。Swift 面向协议等新特性也足够吸引到了我们,因此,我们决定从 2.0 版本开始,所有新的业务代码,包括公用组件采用 Swift 开发。
- Swift 2.0 ~ Swift 2.2 ,Swift 进一步进行了各种改进并开源 ----> 此时,我们 Swift 基础组库也逐步丰富,比如 NESwiftKits(HTTP),NEExtensionHelps,NEShortCutManager,NEUserDefaults,PublicUI(CustomLoading,NEAnimatedTabbar)…
- Swift 3.0 ~ 不久的将来 ----> Swift 层架构逐渐探索演变中… TODO:业务层架构逐步改进,底层库改用 Swift 封装、SPM 替代 Cocoapods…
2. 项目中 Swift 混编及建议
Apple 其实为我们做了大部分混编所需要做的工作,苹果的官方文档对 Swift & Objective-C 混编也总结地很好,并可以很好地解决你混编的大部分问题。
Swift 和 Objective-C 混编无非涉及到两个方向的调用:
- Swift 调用 Objective-C;
- Objective-C 调用 Swift,两个方向的调用其实是一致的。
如果你的项目 based on Objective-C,那在混编初期,大部分的情况都是 Swift 调用 Objective-C 的代码。通过 Bridging Header 文件即可 import 需要提供给 Swift 的 Objective-C 头文件,Swift 即可调用对应的 Objective-C 代码。 这里只是概括下网易漫画 App 混编实践中,语言层面上,值得关注的 8 条实践总结。
2.1 Optional
上图为 Optional 在标准库中的定义,Optional 其实为可解包的遵循 NilLiteralConvertible 协议的枚举类型。在 Swift 中,nil 用来表示值缺失,任何可选类型都可以被设置为 nil。而 Objective-C 中 nil 表示空指针,完全不同的意义。
建议:
- 如果你无法更改你原有 Objective-C 代码添加 Nullability 属性,比如用到第三方 Framework。此时,如果你不确定返回的 object 是否为空,则需要加判断条件 if object != nil {},然后进行强制解包。
- 避免对可选类型强解包,除非你确定该可选值不为 nil,或者希望该值为 nil 值触发运行时错误(Debug 时)。
- Objective-C 属性或者方法如果不加 Nullability 属性的话,则默认为隐式可选类型。要慎用隐式可选类型,如果确定你的数据一直有值,则可用隐式解析可选。
- 建议所有 Objective-C 代码所有可空的属性前加上 nullable 标识,并用 if let 可选绑定进行 Optional 类型的判断。 在涉及网络请求中,使用 Optional 需特别注意。如果 Model 的定义都是用 Objective-C 定义,最好用 Nullability 属性表明 data 是 nullable or __nonnull。事实上,很多涉及网络请求的业务不太确定某一值是否为空。比如用户昵称 String,如果你在业务开发时认为服务端不可能返回一个空的昵称(可能你旧版本的 bug 导致很多昵称为空的用户昵称),然后强制解包,此时就会触发运行时错误。
2.2 Closure
在混编时 Objective-C block 可以映射到 Swift closure 类型。如:大部分网络请求都是 block 回调,如下图为网易漫画项目中 Swift 调用 Objective-C 的网络请求接口。
(点击放大图像)
(点击放大图像)
关于 Closure 有以下几点需要注意:
- Closure 会自动持有被截获变量的引用,这样可以在内部直接修改变量。Swift 同时做了一些性能优化,由于持有变量的引用的开销比直接持有变量开销大,Swift 会判断你是否在 Closure 中或者外面是否修改了该变量,如果没有修改则 Closure 会直接持有该变量。
- Swift 循环引用 weak & unowned:当一个引用在其生命周期中可能为 nil,就把这个引用定义为 weak。相反,则定义成 unowned 引用。
- 非 class 类型的协议不能被标识为 weak, 当一个协议需求所定义的行为能够确保:遵循这个协议的类型是引用类型而非值类型的时候,使用 class 类型协议。
- 尽量使用尾随闭包,代码更简洁。
2.3 AnyObject
Swift 中 AnyObject 定义为 Protocol,所有的 Swift 类类型都遵循 AnyObject 协议,AnyObject 的类型需要在运行时才能确定。Objective-C 的 id 定义为指向对象的指针,Objective-C id 可以无缝地转换到 Swift AnyObject 类型。
- 慎用 as!:建议使用 as? 进行 AnyObject 类型的转换,if let xxx = aAnyObject as? aObjectType {xxx},除非你能够确定 AnyObject 的类型才使用 as! 进行强制类型转换,或者先使用 is 进行类型判断。
- Objective-C llvm 7.0 编译器开始支持轻量型泛型,集合类型 NSArray、NSDictionary 等转换为 Swift 时对应的 Object 都默认转换为 AnyObject 类型。建议你的 Objective-C 代码对应的集合类型都指定泛型类型,如 NSArray<BookCityUpdateItem *>。
2.4 抛弃 OOP? 拥抱 POP?或者 FP?
Swift 是多编程范式的语言,支持面向协议、面向对象、函数式编程、泛型编程,同时 Swift 更推荐值类型而非引用类型,值类型相对引用类型是线程安全的,并且 Swift 对值类型的拷贝进行了足够的优化。Swift 对枚举、结构体、函数给予了更大的能力。 关于语言编程范式的问题其实已经超过了语言层面关于混编的范畴。但我们在开发一些组件或者业务时,使用 Swift & Objective-C 混编必然会导致我们在老和新的编程范式上进行抉择。
- 如果你的模块不涉及混编,那你可以很大胆地去使用 Swift 的面向协议 & 函数式范式,只不过在 Objective-C 调用你的 Swift 模块时,你需要在接口层考虑对 Objective-C 的兼容性。
- 如果当你现在的模块基于 Objective-C,当用 Swift 去扩展现有 Objective-C 模块时,你需要在一定程度上做出取舍。继续沿用之前 Objective-C(面向对象)架构或者用 Swift 进行重构。
我们目前项目中大部分还是基于 OOP 的代码。无法抛弃也完全没必要抛弃 OOP 的原因:
- Cocoa 的核心是基于 OOP,比如我们要去自定义 UIKit 相关组件时必须使用继承。
- 我们必须继承一个现有 Objective-C 代码的基类来去获取基类定义的方法或者属性。事实上,我们工程中还存在不少 Super Class,如各种 Objective-C 工厂类。然而当我们用 Swift 去扩展时,不破坏原有框架地同时很简单地方法就是采用继承(多态)。
- 我们项目 Model 是基于 Objective-C Class 定义。
当然,Swift POP 的引入也给我们在架构上带来了更多的空间。
- 协议能够被类、结构体和枚举遵守,而基类和继承只能限制在类上使用。
- 协议扩展为值类型和类提供了一种定义默认行为的能力。比如通过协议扩展很容易将 UITableViewDelegate、UITableViewDataSource 分离。
- 一个类型能够实现多于一个协议,从而实现多继承所拥有的能力。
- 值类型是线程安全的。
我们目前混编中:
- 在业务层,我们目前也尽量不去更改原有 Objective-C 的代码来过渡到 Swift,因为原有代码已经足够稳定了。
- 在框架层,我们会逐渐过渡到 Swift,除了运用 Objective-C 一些黑魔法而无法实现的功能,当然这种过渡也是需要时间周期去逐渐演化的。
- 我们目前项目中还没有运用太多函数式编程范式。
2.5 Enum
Enum 在 Swift 赋予了更大的能力,支持原始值、关联值、定义函数、扩展、遵循协议等特性。
- 使用 typedef NS_ENUM(NSUInteger, xxxx) {},不要使用 C-Style 枚举定义。
- 如果你的 Objective-C 代码用到 Swift 定义的枚举,相对于 Objective-C 的新特性将无法使用。
2.6 Objective-C 调用 Swift
Swift 的类或协议必须用 @objc 属性来标记,以便在 Objective-C 中可访问。这个属性告诉编译器 Swift 代码可以从 Objective-C 代码中访问。如果你的 Swift 类是 Objective-C 类的子类,编译器会自动为你添加 @objc。
Runtime 支持 Swift 语言本身对 Runtime 并不支持,需要在属性或者方法前添加 dynamic 修饰符才能获取动态型,继承自 NSObject 的类其继承的父类的方法也具有动态型,子类的属性和方法也需要加 dynamic 才能获取动态性。
2.7 With C
Swift 对 C 的交互性也提供了很好地支持,如原始类型 CBool、CUnsignedLongLong,指针类型 CConstVoidPointer、COpaquePointer,类型化指针 CMutableVoidPointer
但是目前,Swift 对 C++ 的交互不是很好的支持(原因苹果认为 C++ 是个很复杂的语言,与 C++ 的交互性需要考虑很多东西,是件很长远的事情,至少在 3.0 及 3.0 版本之前 Swift 不支持),所以如果有些库需要与 C++ 混编可以考虑用 Objective-C 作为桥接。
2.8 其它更多
如宏定义、基本类型和 Foundation 类型转换、Swift 方法重命名和重定义(NS_SWIFT_NAME、NS_REFINED_FOR_SWIFT),这里就不一一展开讲了。
但需要注意的是,Swift 所特有的特性而 Objective-C 没有是无法在 Objective-C 调用的, 解决办法是通过 Objective-C 所支持的特性去重新封装外部接口。
3. 基于 Swift 的架构演变及建议
3.1 现有架构
我们基于 Swift 的架构也不断在演变和探索中。下图这是我们目前大概的一个混编架构图。
(点击放大图像)
其中红色部分和红色箭头是我们目前需要考虑 Objective-C 和 Swift 兼容的地方。
- Service 层统一对业务层的 Swift & Objective-C 接口兼容。其中包括:网络请求 NEKits,数据缓存(图片、文件、数据库等),Hybird,统计,Crash 组件,Hotpatch,Autolayout 组件、动画、公用 UI 组件等等,又分为外部组件和公司内部组件(公司内部组件考虑到稳定性全部采用 Objective-C 进行编写)。
- Model 的定义,我们一直沿用 Objective-C 定义,用 Mantle 进行 Runtime 解析。这部分主要是业务层调用,兼容 Swift 并没有花费我们太多时间。由于 Swift 对 Runtime 并不是很好地支持,我们目前没有打算用 Swift 对 Model 进行重写。
- 业务层部分 Swift 业务模块和老的 Objective-C 业务模块相互调用,此时也需要考虑接口的兼容性。
3.2 现有问题
在 Swift 混编前期,很多情况下是业务逻辑开发过程中,才发现原有的 Objective-C 代码(特别是 C 类型的接口)无法很好地用在 Swift 中,在一定程度上影响了我们业务开发的效率。这种不兼容和接口不友好等问题基本对外封装兼容性接口就能解决。
我们现有的混编架构还在不断尝试和演变中,我们的 Swift 模块也逐渐在业务 & 基础组件化。
- Swift 编码规范。
- OC 的旧接口,如果涉及 Swift 代码调用,需要考虑旧 OC 代码的 Swift 接口兼容性。
- Swift 的新接口,如果涉及旧 OC 代码的调用,需要考虑 OC 的接口兼容性。
- 新架构考虑,我们也在探索中…
<p>POP + MVVM? 或者 POP + MVP? 或者 ……<br></br>NO OOP? NO Inherit?Only Protocol?<br></br>RxSwift?<br></br>Swift Hotpatch? <br></br>Runtime?</p>
- 未来:Swift 3 compatible ?
下图,当然不是我们期待的架构?
(点击放大图像)
3.3 混编建议
- 先小范围、不重要的业务模块尝试 Swift。
- Swift 组件化。
- Objective-C 考虑与 Swift 的交互性,如范型、Nullability。如果可能,对你现有的 Objective-C 代码也提高与 Swift 的交互。
- 没必要去更改现有的 Objective-C 代码,成本很大。除非现有的 Objective-C 代码需要重构,而 Swift 在设计层面很好地解决了你的重构问题。
- 没必要去追求你工程中 Swift 的代码占有量,用 Objective-C 能够解决但是 Swift 解决不了的问题,那就使用 Objective-C 吧,虽然这种情况比较少,如 Objective-C Runtime。
- 考虑适合你们产品的 Swift 架构和最佳实践。
未来,Swift 的开放性、跨平台、多编程范式、核心库的逐渐丰富,也给予了我们更大的发挥和想象空间。 谢谢大家,今天的内容分享结束,希望对大家有帮助,也期待后续与大家共同交流探讨~ ~
QA 环节
Q:Swift 对你们开发效率的提升有多大作用,有这方面的统计么?
A:Swift 并没有提升我们的效率,现在的开发效率和 OC 差不多,同时开发效率也和个人开发者对 Swift 的熟练程度有关。
Q:Swift 组件化问题: 如果某个 pod 内部带有 Swift 代码,则会导致整个 app 最低版本支持 iOS 8,无法支持 iOS 7,请问这个是如何解决的?
A:我们目前也在考虑组件话的问题,我们 app 最低支持 iOS 7,不过由于产品属性(偏年轻),我们不久会只支持 iOS 8,到时会考虑用 Carthage。目前所有 Swift 代码都放在工程内的。
Q:Swift3 之后还需要在应用中带 Swift 运行时吗?ABI 是否固定了?
A:Swift 3.0 会保持 ABI 的稳定性,意味着,即便源代码语言发生了变化,用以后版本的 Swift 开发的应用程序和编译库能在二进制层次上和 Swift 3.0 版本的应用程序和编译库相互调用。
Q:Swift 的高效率表现在什么地方?
A:非运行时对消息事件的处理,值类型的使用,对高阶函数地支持等等。
Q:请问你们混合业务组件的接口是怎么设计的?
A:混合业务组件的接口如果涉及到 OC 的调用则需要考虑 Swift 的兼容,基本方法就是语言重新封装为 OC 的接口。
Q:Swift 的优势在哪里,与 OC 相比有什么不同?
A:1. 性能。 2. 多编程范式。 3. 跨平台并开源。
Q:Swift 为了与 OC 兼容,在某些特性上做了一些妥协,比如不完善的 runtime 支持,您是否认为这是 Swift 的一个缺点?
A:Swift 其实做了很多妥协,比如 Cocoa 的妥协,我认为这并不是 Swift 缺点,兼容需要过渡时期。
Q:在混编的情况下,单元测试怎么做呢?可以用 Swift 写单元测试,来测 OC 代码吗?
A:如果 Swift 的代码需要 OC 调用的话,我们会用 OC 去写单元测试。但是也并没有统一规定。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论