5

Move Objective-C frameworks for Swift

 2 years ago
source link: https://looseyi.github.io/post/tool/objective-c-framework-for-swift/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2021-11-13 土土Edmond木

Move Objective-C frameworks for Swift

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 参数在这里是以关联类型枚举来声明,它将 SDWebImageOptionsSDWebImageContext 结合到一起。而在 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

然而,重命名的方式无法解决需要将 SDWebImageOptionsSDWebImageContext 结合为 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.0Package.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 产物,FacebookCoreFBSDKCoreKit 。注意,这里有两种不同的前缀 FacebookFBFacebook 代表的则是新提供的 Swift framework,而 FB 则代表的是原有的 Objective-C framework。

这里按语言将 FacebookCore 分成 Objective-C 与 Swift 两个 target 也是不得已而为之,因为 SPM 目前仍不支持 多语言混编。另外为了更好复用编译产物,他们对公用逻辑进行二次拆分,剥离出 LegacyCore target。

FacebookCore 将通过 overlay framework 来提供面向 Swift 友好的 API,最后用户也将被分为三部分:

  1. 引入 FacebookCore 来使用完善 Swift API 的 Swift 用户;
  2. 引入 FBSDKCoreKit 来使用由 Objective-C 接口翻译过来的 Swift 用户;
  3. 引入 FBSDKCoreKit 的 Objective-C 用户;

要说使用 Swift 优点的话,对 iOS 开发初学者更具吸引力算是一点。对于需要深入特定领域的问题,如 decoding 或者 transformer,用 Swift 重写和 Objective-C 一样丑陋,并无太多区别 😅。因此,作为框架提供者,要跳出语言的限制,从更高角度看待问题。

纵观整个 SDWebImage 6.0 提案的讨论过程,我们也可以看到其维护者的严谨态度。不仅能从用户使用体验和维护成本角度来权衡方案变更带来的影响,并且做了详细的调研和研究分析。

希望本文能为从 Objective-C 转向 Swift 的开发者提供一些帮助。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK