引言
Swift 作为 Objective-C 的替代者,苹果每年都为其投入了大量的资源。随着 Swift ABI 稳定,国内大厂也开始投入人力推广 Swift。诚如苹果所言,他们应该是拥有 Objective-C framework 数量最多的公司了,他们一直在持续更新其 Objective-C framework 的 interface 使其对 Swfit 友好,更有甚者是直接重写。同时也提供了很多纯 Swift 的原生框架,如 Combine、CoreML、SPM、RealityKit 等。
而今天我们要讨论的是,要如何改造 Objective-C frameork 使其更友好的支持 Swift API。
本文主要翻译自 SDWebImage 6.0 提案:Rewriten Swift API with the overlay framework instead of Objective-C exported one,感谢作者 @Dreampiggy。
背景
对普通 iOS 开发者而言,让现有的 Objective-C 框架更好的支持 Swift 也是我们无法绕开的问题之一。尽管 Swift 编译器在转换 Objective-C 接口时做了很多不错的优化工作,但仍旧无法满足所有开发者的需求。
Tips: WWDC20 有专门 Session-10680 来讨论 Refine Objective-C frameworks for Swift,也推荐看sketchk.xyz 文章。
以 SDWebImage 5.0 的 Objective-C 代码为例:
1[imageView sd_setImageWithURL:url placeholderImage:nil options:0 context:@{SDWebImageContextQueryCacheType: @(SDImageCacheTypeMemory)} progress:nil completion:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *url) {
2 // do something with result
3 if (error) {
4 NSLog(@"%@", error);
5 } else {
6 // do with image
7 }
8}];
对于这样的 API,clang 编译器可以将其转换成如下的 Swift 形式的代码:
1imageView.sd_setImage(with: url, placeholderImage:nil options:[] context:[.queryCacheType : SDImageCacheType.memory.rawValue] progress:nil) { image, error, cacheType, url in
2 // do something with result
3 if let error = error {
4 print(error)
5 } else {
6 let image = image!
7 }
8}
相比于纯 Swift API,它还是少了一些 Swifty 的味道。一个简洁优雅 Swift API 设计应该能够充分的利用其语法,包括但不限于以下特性:
它看起来应该是这样的:
1imageView.sd.setImage(with: url, options: [.queryCacheType(.memory)] { result
2 switch result {
3 case .success(let value):
4 let image = value.image
5 let data = value.data
6 case .failure(let error):
7 print(error)
8 }
9}
注意:options
参数在这里是以关联类型枚举来声明,它将 SDWebImageOptions
和 SDWebImageContext
结合到一起。而在 Objective-C 中我们就无法做到这样的效果。你知道的 Objective-C 是 C 语言的超集,而在 C 语言中 Int 类型的枚举是无法绑定对象的。
可选方案
想要为现有的 Objective-C framework 提供优雅的 Swift API,我们有哪些路径呢 ?
0x01 利用 NS_SWIFT_NAME
修改 Objective-C 的 Public Interface
NS_SWIFT_NAME
能够解决重命名的问题。例如,可以将 SDImageCache
重命名为 SDWebImage.ImageCache
以此来去除其前缀 SD:
1NS_SWIFT_NAME(SDWebImage.ImageCache)
2@interface SDImageCache : NSObject
然而,重命名的方式无法解决需要将 SDWebImageOptions
和 SDWebImageContext
结合为 Swift 提供的枚举结构,如 SDWebImage.ImageOptions
:
1enum ImageOptions {
2 case queryCacheType(SDImageCacheType)
3 case priority(SDImageQueryPriority)
4 ...
5}
0x02 Overlay Framework & 重写旧 Swift API
通过创建的 overlay framework 来提供 Swfit 友好的 API。Swift 社区有说明 Apple 是如何实现 传送门。
对于稳定的商业化产品而言,如果推倒重来直接用 Swift 来新写内部组件,其开发成本和潜在的风险都是需要合理评估的。因此,能够提供 Swift 友好的 API 层也不失为一个合理方案。
我们以 SDWebImage 为例。
首先,创建一个名称为 SwiftWebImage
(名字待定) 的 framework,内部包含了一个名为 SDWebImage.swift
的文件且内容如下:
1@_exported import SDWebImage
利用 @_exported 关键字来扩展 import 框架的可见性,这样就不需要在每个使用 SD 的地方进行 import。
接着将原有 Objective-C 的公共 API 标记为 @unavailable,具体如下:
1@available(*, unavailable, renamed: "SwiftWebImage.ImageOptions")
2public struct SDWebImageOptions {}
3
4@available(*, unavailable, renamed: "SwiftWebImage.ImageOptions")
5public struct SDWebImageContext {}
最终,重写的 Swift API 将通过调用原有 SDWebImage 的 API 来完成逻辑:
1/// Wrapper for SDWebImage compatible types.
2public struct SDWebImageWrapper<Base> {
3 public let base: Base
4 public init(_ base: Base) {
5 self.base = base
6 }
7}
8
9extension SDWebImageCompatible {
10 /// Gets a namespace holder for SDWebImage compatible types.
11 public var sd: SDWebImageWrapper<Self> {
12 get { return SDWebImageWrapper(self) }
13 set { }
14 }
15}
16
17extension UIImageView : SDWebImageCompatible {}
18
19public protocol ImageResource {}
20
21extension URL : ImageResource {}
22
23extension SDWebImageWrapper where Base: UIImageView {
24 @discardableResult
25 public func setImage(
26 with resource: ImageResource?,
27 placeholder: UIImage? = nil,
28 options: ImageOptions? = nil,
29 progress: LoaderProgressBlock? = nil,
30 completion: ((Result<ImageResult, ImageError>) -> Void)? = nil) -> CombinedOperation? {
31 // convert the `ImageOptions` into the actual `SDWebImage` and `SDWebImageContext`
32 // finally call the exist `sd_setImage(with:) API
33 }
34}
当用户引入 SwiftWebImage
framework 时,旧的 Swift API 将被标记为不可用。此时,我们就能愉快的使用新 API 了。
1import SwiftWebIamge
2import SDWebImage // This will be overlayed and not visible, actuallly you don't need to import this
另外,这里通过 SDWebImageWrapper
实现了 Swift NameSpace 形式的 extension。SDWebImageWrapper
作为装饰器将对原类型进行封装,然后我们再对 SDWebImageWrapper
进行自定义方法的扩展,从而避免了命名冲突的问题,方便我们对系统库中的已有类型作自定义扩展。
0x03 Overlay framework naming
我们发现一个问题:像苹果提供的标准库 Network.framework
, 它在 Swift Runtime 时能够提供一种 overlay framework
其名称与原有 framework 一样,同为 Network
。此时我们可以像下面这样使用:
1import Network
其背后,我们 import 的并非 Network.framework
, 而是 libSwiftNetwork.dylib
及其对应的 module。
1import SwiftNetwork // Actually what you do
2// The libSwiftNetwork has this:
3@_exported import Network
4
5@available(*, unavailable, renamed: "Network.NWInterface")
6typealias nw_interface_t = OS_nw_interface
7
8public class NWInterface {
9 // ...Call C API for internal implementations
10}
SDWebImage 也想采用这样的方案,如此就不用将 import SDWebImage
替换为 import SwiftWebImage
,然而事与愿违,毕竟我们同时支持了 3 种包管理方式:
- CocoaPods: 支持自定义的 script phase,
prepare_script
来完成module name
的替换; - Carthage: 支持在 Xcode Project 中自定义 Build Phase;
- SwiftPM: 不允许在一个 module name 下同时声明两个 framework;
同时,对现有的 SDWebImage 5.0 用户,如果他们不愿意更新为新的 Swifty API,仍可以通过 import SDWebImage
来使用原有 Objective-C 生成的 API。
以重命名的方式提供不同于 SDWebImage
名称的 overlay framework 可以支持这样的操作。
什么意思呢 ?意味着你的项目中可同时存在两种不同的 Swift API,它取决于你导入的 framework。
- 不使用 overlay framework
1import SDWebImage
2
3let imageCache = SDImageCache.shared
4imageView.sd_setImage(with: url, options: [], context: [.imageScaleFactor : 3])
- 使用 overlay framework
1import SwiftWebImage
2
3let imageCache = ImageCache.shared
4imageView.sd.setImage(with: url, options: [.scaleFactor(3)])
毕竟,我们都知道 Swift 是有 name space 隔离的。
0x04 放弃 Objective-C 用户 & 用 Swift 重写
作为个人意见,这并非一个好主意。在 iOS 社区中已经有很多很棒的纯 Swift 的图片加载框架,如 Kingfisher, Nuke 等。
大家都有一些共通的设计和解决方案。如果完全重写将需要花费大量的时间和单测来保证功能的稳定性。况且,仍然有很多 Objective-C 项目和用户在使用 SDWebImage 5.0。尤其在国内,80% 以上采用 Objective-C 的公司都在使用 SDWebImage (非个人)。他们中还有大量使用 Swift 与 Objective-C 混编。放弃这些用户值得深思!
对于现阶段而言,不论是选择 Objective-C 或者 Swift 都是实现细节,而采用 Swift 可能有的优势:
- 线程安全:不赞同,在 Kingfisher 和 Nuke 中同样有线程安全问题,这并非语言层面可以解决的。 当然,Swift 提供了严格的 Optioanl 类型来避免一些常见的错误,如 null 检查。另外,Swift 5.5 中提供的 Actor 也能从一定程度上规避并非带来的问题;
- **性能:**对于图片加载框架而言其性能瓶颈并非由 Objective-C runtime 的消息发送架构决定的,而是由一些调度队列、图片解码等其他问题的。这些也并非 Swift 能解决的;
- **维护:**作为主要原因,对于 iOS 程序员的新手来说,他们可能不太了解一些 Objective-C 的最佳实践和良好的代码规范,而用 Swift 实现的话可以吸引更多优秀新人为 SDWebImage 贡献;
因此,现阶段而言,我们仅需要提供 Swift 友好的 API 即可,内核仍以 Objective-C 实现。毕竟 SDWebImage 经过这么多年的迭代,可靠性与稳定性都有很好的保证。
开源实践
在提案的讨论中,也提到了优秀大厂的一些实践。他们的做法也是值得我们考虑的。
facebook-ios-sdk
facebook 家提供的 facebook-ios-sdk,他们为 Swift 用户提供了新的符合 Swift 特性的 Swift Module,不过它基于原有的 Objective-C framework 来包装的产物。因此,用户可以根据其选择来 import 不同的 framework。显然像 facebook-ios-sdk 复杂的 framework,他们选择了成本相对较低的 Overlay Framework
方案。如前面提到用 Swift 重写也未能显著提高性能或者安全性,毕竟他们已通过使用较低的 C API 和线程安全锁解决了,这在 Swift 语言层面无法解决的。
我们通过版本 v12.0.0
的 Package.swift 的以产物之一 FacebookCore
来举例。
1import PackageDescription
2
3let conditionalCompilationFlag = "FBSDK_SWIFT_PACKAGE"
4
5let package = Package(
6 name: "Facebook",
7 products: [
8 .library(
9 name: "FacebookCore",
10 targets: ["FacebookCore", "FBSDKCoreKit"]
11 ),
12 ...
13 ],
14 targets: [
15 .target(
16 name: "FBSDKCoreKit_Basics"
17 ),
18 .target(
19 name: "LegacyCore",
20 dependencies: ["FBSDKCoreKit_Basics", "FBAEMKit"],
21 path: "FBSDKCoreKit/FBSDKCoreKit",
22 exclude: ["Swift"], ...
23 ),
24 .target(
25 name: "FacebookCore",
26 dependencies: ["LegacyCore"], ...
27 ),
28 .target(
29 name: "FBSDKCoreKit",
30 dependencies: ["LegacyCore", "FacebookCore"], ...
31 ),
32 ]
33)
Tips:新提供的 FacebookCore framework 仅支持 SPM,尚未支持 CocoaPods 方式引入。
framework products
看构建产物 library FacebookCore 作为核心 SDK,它提供了两种可导入 module 产物,FacebookCore
和 FBSDKCoreKit
。注意,这里有两种不同的前缀 Facebook
和 FB
。Facebook
代表的则是新提供的 Swift framework,而 FB
则代表的是原有的 Objective-C framework。
这里按语言将 FacebookCore 分成 Objective-C 与 Swift 两个 target 也是不得已而为之,因为 SPM 目前仍不支持 多语言混编。另外为了更好复用编译产物,他们对公用逻辑进行二次拆分,剥离出 LegacyCore
target。
FacebookCore 将通过 overlay framework 来提供面向 Swift 友好的 API,最后用户也将被分为三部分:
- 引入
FacebookCore
来使用完善 Swift API 的 Swift 用户; - 引入
FBSDKCoreKit
来使用由 Objective-C 接口翻译过来的 Swift 用户; - 引入
FBSDKCoreKit
的 Objective-C 用户;
总结
要说使用 Swift 优点的话,对 iOS 开发初学者更具吸引力算是一点。对于需要深入特定领域的问题,如 decoding 或者 transformer,用 Swift 重写和 Objective-C 一样丑陋,并无太多区别 😅。因此,作为框架提供者,要跳出语言的限制,从更高角度看待问题。
纵观整个 SDWebImage 6.0 提案的讨论过程,我们也可以看到其维护者的严谨态度。不仅能从用户使用体验和维护成本角度来权衡方案变更带来的影响,并且做了详细的调研和研究分析。
希望本文能为从 Objective-C 转向 Swift 的开发者提供一些帮助。