Altconf conrad kramer cover

如何编写一个(不)受欢迎的 iOS SDK

Conrad Kramer 作为 Workflow 团队的一员,他一直在维护着项目当中所使用的依赖库和 SDK,同时他也一直在向开源项目贡献代码。在本次 AltConf 2016 讲演当中,Conrad 向大家分享了他在维护工程中所汲取到的经验教训,他还讲述了是何种因素让 iOS(以及其他)SDK 变得如此好用的,此外他还会介绍如何在创建一个新的 SDK 的时候避开一些常见的陷阱。他所讲演的内容涵盖了编程语言、构建系统、编译环境、运行平台等多个方面,其中还有很多您很可能不知道的内容,Conrad 向我们展示了一次非常宝贵的讲演,他将教导您如何以最有用、最友好的方式同他人分享您的代码。


分享您的代码 (0:00)

大家好!我是 Conrad。在做的不少人都会持有这么一个观点:编写一个 iOS SDK 就同喝水一样容易,只需要将代码扔到项目当中,然后编译一下,好的,万事大吉!然而,千里之堤,溃于蚁穴,这就是为什么我在标题那里加了「不」这个词语,搞不好您的 SDK 就会被众人所厌烦;下面我将要讲述在构建一个 iOS SDK 的时候,什么是该做的,什么是不该做的,这将会是一个很长的清单。

我现在在为一款名为 Workflow 的应用工作,在应用当中,我们使用了大量的第三方应用和服务。因此,我们在应用当中集成了大量的第三方 SDK。我们大概有将近 30 个子模块。其中有很多 SDK 我们需要将它们 fork 出来,以维持一个稳定的版本,甚至有些我们还最要编写自己的版本,其原因归结于它们的原始功能并不满足我们的需求。

但是我们和其他开发者所不同的是,我们会真正用心地去改善这些 SDK。而其他开发者和我们的做法不同,如果您的代码不满足他们的需要的话,他们会直接把你的代码 pass 掉,因此您需要让您的 SDK 支持多种不同的配置条件。

另外,有一些 SDK 并没有开源,因此如果这些 SDK 不满足开发者需求的话,因为他们根本没法对其进行定制;只要他们想,他们完全会放弃使用您的 SDK。

(不要)想当然 (1:05)

本次讲演的一个中心内容就是:「不要想当然」。因为开发者们的需求千变万化,因此您需要让您的 SDK 足够灵活,以便能够完全处理他们的需求。

为了帮助您做到这一点,我将例举一些常用的方式来让您的 SDK 尽可能地灵活。

使用现代化的 iOS SDK 编程语言 (1:22)

最重要的一点就是您所使用的编程语言。如今,编写 iOS 应用有两种流行的语言,我很肯定在座的各位没有不知道的(Swift 和 Objective-C)。某些开发者只选择使用 Objective-C,而有些开发者只选择使用 Swift,当然绝大多数都是两者混用。因此关键在于,您应该使用 Objective-C 来编写您的 SDK,这样没有使用 Swift 的开发者也能够使用您的 SDK。

同样重要的是,您应当在项目当中添加 Objective-C 可空性 (Nullability) 和泛型 (Generics) 特性,这样您的项目也能很轻易地使用 Swift SDK。例如,最近新版本的 Dropbox SDK 是完全用 Swift 编写的,这时候我们发现我们根本没法将其完全集成到 Workflow 当中。我们并没有那么多时间将其重写为 Objective-C 版本的。

Receive news and updates from Realm straight to your inbox

此外还有一点,如果您希望为 Swift 开发者提供一流的 SDK 支持的话,那么您应当给您的 Objective-C SDK 编写一层封装,以便让您的 API 更加 Swift 化。您同样应该支持所有活跃的 Swift 版本,目前是 Swift 2 和 3。目前一个很好的例子便是 Realm SDK,他们使用 Swift 在他们的 Objective-C 核心中编写了一层封装,这使得它们的 API 变得非常的 Swift 化

支持多种构建系统 (2:26)

SDK 开发过程中,构建系统 (Build System) (我认为)是极为重要的一环。支持所有的构建系统是非常重要的,这样所有人都可以将您的 SDK 集成进去。例如,最重要的一点就是拥有一个 Xcode 项目

您应当拥有一个已经同时编译了静态库和动态框架的 Xcode 项目,这样人们就可以直接将您的 Xcode 项目拖曳到他们的项目当中,然后完成应用的编译。

您同样也应该让您的动态框架方案 (scheme) 的状态变为「已共享」(shared),然后在 Github 上使用语义化版本号 (Sematic Versioning) 标记您的发布版本,这样您就完成了 Carthage 的支持。如果您不熟悉 Carthage 的话,其实很简单,它主要是用 xcodebuild 来完成框架的构建。

同样,拥有 podspec 也是非常重要的,因为有很多人选择使用 CocoaPods,您同样也会希望这些开发者使用您的 SDK 的。

最后我们需要支持 Swift Package Manager。这是一个非常新颖的玩意儿,但是实际上,开发团队最近增加了对 C、Objective-C 以及 C++ 的支持,因此,现在您可以选择使用 Swift 3 来创立一个 Package.swift 文件,然后指明如何构建 SDK,这样您就可以骄傲地说:我的 SDK 现已支持 SPM。

支持多种编译环境 (3:34)

SDK 能够运行编译的环境同样也是非常重要的。编译环境和运行平台有所不同:编译环境决定了它所使用过程当中的上下文环境。这两者还是有细微差别的,不过由于应用扩展正变得越来越重要,它正成为 iOS 的核心体验之一,因此您需要确保您的 SDK 能够在应用扩展当中正常工作,就如同在主应用当中一样。

这意味着您需要让您的文件路径可以进行配置。如果您需要让 SDK 能够在应用扩展当中使用,如果您需要在 Library 或者 Documents 之间传递数据的话,那么这两个目录的路径不能够使用硬编码 (hardcode) 操作。您需要确保开发者如果有需求的话,他们能够将文件放入到 App Groups 当中。

此外,如果人们不关心 App Group 的话,那么您需要添加一个合理的默认值。

另外,避免与 UIApplication 建立依赖。这是非常重要的一点,因为应用扩展当中不存在 UIApplication 。如果您真的需要这么做的话,您可以将使用了 UIApplication 的 API 标记为 NS_EXTENSION_UNAVAILABLE,这意味着您的代码仍然能够在应用扩展中通过编译,只不过您没法在应用扩展中使用这些标记过的 API 而已。

最后,如果您需要使用 UIApplication 来执行后台任务的话,您可以选择使用另一个新的名为 NSProcessInfo 的 API。它其中包含有一个 expiring task API,允许您申请大概三分钟的后台运行时间,最重要的是它能够同时在主应用和应用扩展当中使用。

处理进程间通信 (4:48)

接下来,我们将讨论一下进程间通信 (Inter-Process Communication) 和协调器 (Coordination) 相关的内容,这是非常难的一个部分。我们的想法是,如果您的 SDK 需要在主应用和 Today 应用扩展当中存储数据的话,那么您应该不希望它们之间发生冲突。例如,如果您有一个日志 SDK,它会将相关时间记录在一个文件当中,此外您又同时希望能���在主应用和应用扩展之间共享这个文件,那么主应用很可能会一遍又一遍地覆盖掉应用扩展的日志,反之亦然。

通常情况下,主应用和应用扩展都会同时运行。例如,当您在应用打开的情况下,滑出通知中心,这样您的 Today 部件就会开始启动,这样您就有两个同时运行的进程了。在这种情况下,您不能使用 NSLock,因为应用拥有自己的 NSLock 机制,而扩展也拥有自己的 NSLock 机制,它们之间是无法互相通信的。因此,就算您同时添加了锁,数据仍然还是会被覆盖掉。

相反,处理这种情况的正确方式是使用内置在操作系统当中的特性,例如命名信号 (semaphore)、文件锁 (file locks)、kQueuenotify_post 以及 CFMessagePost

编者按:Apple 的 Thread Programming Guide 和 NSHispter 的 IPC 文章对这类特性有着详细的介绍。

直到现在,这些特性都是非常底层的,并没有很好的文档来很好地进行说明,这是非常糟糕的,但是您仍然可以使用一些更高层的抽象来整合这些特性。在 Workflow 当中,我们编写了一个名为 WFNotificationCenter 的东西,它的 API 与 NSNotificationCenter 相互兼容。它能够在 App Group 当中运行,因此您只需要在您的 App Group 当中将其初始化,这样它就可以在应用扩展和主应用之间进行工作了。

当应用扩展和主应用之间发生了事件的话,我们使用这个 API 来进行通知操作。

支持所有的 Apple 平台(iOS、macOS、tvOS、watchOS、carOS) (6:23)

支持所有平台也是非常重要的。现如今已经有很多平台面世了,有 watchOS、tvOS、iOS、macOS,可能以后还会有 carOS。如果您构建的 SDK 是网络模块的话,那么没有理由不去支持 tvOS。tvOS 和 iOS 是非常相似的,因此如果您的 SDK 能够在 iOS 上运行,那么它很可能也可以在 tvOS 上运行,所以您应当在 tvOS 进行构建和测试,以确保它同时也能够支持这个平台。

在支持的过程中,通常发生的问题往往是和依赖库相关的。如果您的 SDK 依赖于其他的第三方库,那么您必须进行更新,以便让它们能够在 watchOS 上运转,或者添加 tvOS 支持,这些操作通常是非常简单的,但是也会让人非常沮丧。例如,如果您的 SDK 有一个 iOS 特性的 UI,但是其中的网络核心库和其他分析库的做法是类似的,那么您可以选择将 UI 编译变为可选,这样就可以同时支持 Mac 了。

S设置部署目标 (7:14)

通常的经验告诉我们,至少要支持上一个 iOS 版本,因为绝大多数应用都需要支持之前的版本。现在您至少需要支持 iOS 8 或者 iOS 9。不过,如果不麻烦的话,您应该尽可能地支持更早期的版本。

比如说,如果您只用到了 NSURLSession 的话,那么您应该至少能够支持到 iOS7 或者 macOS 10.9 的版本。

(不要)使用依赖库 (7:45)

接下来,让我们谈谈依赖库这个玩意儿。在绝大多数情况下,您都需要避开强依赖的出现,关于这一点有着数不胜数的原因可以解释这一点,但最主要的理由在于:这可以让您的 SDK 更轻量、更灵活。

如果您想要修改您的 SDK,或者有任何新的平台出现,或者诸如此类的事情发生了,那么不建立强依赖关系可以让您没必要进入到 SDK 当中,从而确保所有的依赖都同样进行了更新。

对于 Swift 来说,最容易出现的问题就是代码级别的兼容性问题了。如果您使用 Swift 3 写一个封装,并且它仍然依赖于某个仍然使用 Swift 2 写的东西的话,那么您不得不将所有东西全部升级到 Swift 3。而如果您只使用到了这个 SDK 的某一小部分的话,那么您这样做基本就是在浪费时间、浪费青春,我相信大家是不乐意这么做的。

因此,您需要尽可能避免使用封装,例如 keychain 的封装、Alamofire 或者 AFNetworking 之类。因为当您对您的 SDK 进行更新的时候,依赖库往往还没有进行更新。此外,它们往往还会提供很多没有必要的功能,您很可能只使用其中的一小部分,所以最好的做法就是自行实现这一部分。

为您的 API 编写文档 (8:49)

此外,您需要为您的 API 编写详细的文档,但是这不仅仅只是需要使用 Xcode 来为您的方法编写使用说明,您同样还需要在 README 当中添加对 SDK 的综述和概览。您需要确保开发者能够理解相关的基本概念,让他们知道如何将您的 SDK 整合到项目当中,并且知道如何进行配置,这两个方面都是非常重要的。

我经常要看一下 SDK 的源代码,以便切实知道这个方法执行了哪些操作,如果它提供了详细的文档的话,这样我就不用去查看源代码就知道它执行了哪些操作了,这无疑是一件非常赞的事情。并且如果您添加了诸如 Nullability 之类的修饰符,这同样也能够指明我是否能够向这个方法中传递 nil 值。

测试代码 (9:26)

虽然测试是一件非常难、非常恼人的操作,但是这对开源项目来说是非常有用的。测试让您有信心将社区提交的贡献代码进行合并操作,而无需担心会有错误发生。这意味着您可以快速合并大量的提交请求,从而吸引一大批开发者加入到您的社区当中。例如,ComponentKit 的测试库是非常庞大的。他们使用快照测试 (snapshot test) 来测试每个单独的组件。所以,一旦有任何变化或者错误,那么您就会很快地发现它。这使得提交请求合并变得更加简单了,因为我们已经知道了没有错误发生了。

此外,和持续集成 (continuous integration)关联可以让测试更加天衣无缝。

优化并传达 SDK 的性能指标 (10:07)

为了帮助使用您第三方库的用户了解到他们所调用的性能耗费,您需要记录下那些性能差的 API 调用。例如,如果您的某个方法当中需要输出文件、上传某个东西、处理图片或者加载网页内容的话,那么您一定要告诉开发者您的操作。不然的话,他们很可能会将这段代码放到某个快速循环当中,或者放到某段性能至上的���码当中,但是由于您没有告知他们,因此他们往往并不会意识到自己在做什么错误的事情。

另外,如果您执行了很多处理工作的话,除非这些工作非常重要,否则请确保将它们放到了一个 GCD 队列当中,并让它们具备低后台优先级。例如,日志记录库只应当在后台队列当中工作,它只能在这个非常低优先级的后台队列当中访问网络或者访问某个文件,因为您不会希望它影响到您应用的主线程操作。

另一个您很可能不会考虑到的就是内存占用率了。这个是比较困难的,因为您很难在内存受限的环境中测试您的 SDK,但是现在在 iOS 平台上这种情况出现了很多。Widget 曾经只能够使用 16M 的内存空间,虽然现在涨到了 26M,但是这仍然不会像主应用那样拥有 650M 的内存使用空间。

因此如果您在 +load 中加载了很多东西或者使用了 objc_copyClassList、使用了 UIWebViewCGBitmapContext 之类的东西,这往往会导致您的进程发生崩溃,而您往往无法意识到错误的原因。如果您尝试在 Widget 当中将一个大图像渲染到位图上下文环境中,那么它就会立即崩溃。

将这些建议组合在一起:WFOAuth2 (11:41)

将这些建议组合在一起,要知道我已经说了很多了,但是我其实想要实现类似这样的东西,因此我写了一个名为 WFOAuth2 的库。WFOAuth2 支持 watchOS、macOS、iOS 和 tvOS。它支持 CocoaPods、Carthage 和 Swift Package Manager,我们在 Workflow 当中用它来执行所有的身份认证操作。

我们使用 WFOAuth2 来进行 Slack、Dropbox、Box 以及其他不同服务的身份认证。它同样也拥有完善的文档,轻量级,没有很多的依赖库。

问答时间到! (12:16)

问:您此前是否使用过从 AWS API 或者其他网络服务 API 所自动生成的 SDK 呢?

Conrad:实际上我并没有太多的使用经验,但是只要这些工具所生成的 SDK 没有违背我们所讨论的这几点的话,那么我觉得是完全没问题的。

问:您是否推荐在框架中加入一个静态库,以便能够同时支持 Carthage 和 CocoaPods?

Conrad:当然。如果您有一个开源的代码库的话,您当然可以直接提供二进制文件,但是我觉得您同样也可以提供一个 Xcode 项目,这样就可以允许开发者从中自行构建静态库或者框架。

问:Swift 是否支持分发闭源的框架,就如同二进制文件那样,而无论代码本身是否是开源的?

Conrad:可以。如果您没有开源库的话,那么您所需要做的就是分发一个二进制文件。然而,这样做的话就会比较麻烦,因为您需要分别为 Swift 2 和 3 提供不同的版本。

问:您对通过 CocoaPods 或者 Carthage 分发私有仓库有没有什么建议呢?

Conrad:您可以轻松地在 CocoaPods 或者 Carthage 当中包含私有仓库。对于同时维护相同的公开和私有仓库来说,我并没有太多的经验,但是我觉得您应该可以同时拥有两个不同的仓库,并且同时使用。

问:您为什么选择在 Workflow 中使用不支持 Carthage 或者 CocoaPods 的子模块呢?

Conrad:之所以使用子模块 (sub-modules) 的主要原因在于:我们需要极高的灵活性,并且我们不能时不时地用一下别的东西。我们最终选择使用了大量不支持 CocoaPods 或者 Carthage 的第三方库。我们使用 Carthage 来帮助构建那些我们不希望在每次运行时候都要构建的东西,但是这仍然对灵活性有影响,而子模块则提供了我们所需要的这种灵活性。

我们所使用的一些框架甚至连 Xcode 项目都没有,因此我们必须将源文件拖曳到我们的项目当中,然后将其作为项目的一部分进行编译。如果我们能够使用构建系统的话那将是极好的,但是随着我们整合进来的东西越来越多,导致现在这样做就不可行了。

问:您觉得有没有 SDK 是能作为榜样举例的,或者说有哪些 SDK 遵循了良好的公约和标准?

Cornad:有一些 SDK 我是非常喜欢使用的。Mantle 很好地遵循了我们所说的这些标准,但是它是专门为 Objective-C 而生的。尽管如此,在构建系统方面,它使用了 Xcode 和框架来进行构建,因此它非常的轻量级。它允许我们无需耗费太多精力就能够在 watchOS 上使用它。

Realm 同样也是一个很好的例子,它支持所有的平台以及所有版本的 Swift。

不过对于人们来说,差劲的东西反而更容易被记住。我们在使用 AFNetworking 的时候遇到了许多依赖方面的问题。因为有些第三方库使用了 AFNetworking 的静态库,一旦当我们想要使用别的也用了 AFNetworking 的库时,他们之间就会发生冲突。

对了,我突然又想起来一个用起来很开心的 SDK,那就是 MailCore。尽管它是由一堆 C++ 核心代码堆砌而成的,但是它支持在 iOS、Mac、Android 等等平台上进行编译,从而简化发送邮件的流程。

问:您推荐为 Objective-C 内核组件编写一个 Swift 封装。那么您对 Swift 内核组件编写一个 Objective-C 封装是怎么看的呢?

Conrad:您当然可以做到这一点,使用 Swift 编写框架,然后让其和 Objective-C 兼容。但是很不幸的是,这会导致您的框架当中被嵌入 Swift 运行时,而这很可能是别人不想要的。在 Workflow 当中,我们现在还不需要 Swift 运行时,因此我们就没办法使用 Swift SDK。它只是能够归结为支持所有的这些配置而已。以后 Swift 运行时很可能会被内置到系统当中,而那个时候,Swift 编写的东西就能够很好的在 Objective-C 上运行了,这是完全可行的。

About the content

This talk was delivered live in June 2016 at AltConf. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Conrad Kramer

Conrad Kramer started developing for iOS after he got involved with the jailbreak scene back in 2010 with his first tweak Graviboard. Since then, he has gotten into regular iOS development and worked on various open source projects in the Cocoa community, like AFOAuth2Client and WFNotificationCenter. Conrad now spends all of his waking hours on Workflow, an automation tool for iOS, where he works on anything from the server backend to building the complex drag and drop interactions.

4 design patterns for a RESTless mobile integration »

close