macOS SDK 入门¶
概述¶
macOS SDK 是一个磁盘目录,其中包含 macOS API 的头文件和元信息。Apple 将 SDK 作为 Xcode 应用程序捆绑包的一部分进行分发。每个 Xcode 版本都带有一个 macOS SDK,即 Xcode 发布时最新发布的 macOS 版本的 SDK。SDK 位于 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
。
在 macOS 上编译 Firefox 需要 macOS SDK。构建系统默认情况下会引导一个合适的 SDK,但您可以使用 mozconfig
选项 --with-macos-sdk
选择不同的 SDK。
ac_add_options --with-macos-sdk=/Users/username/SDKs/MacOSX11.3.sdk
支持的 SDK¶
首先,Firefox 运行在 10.15 及更高版本上。这称为“最低部署目标”,与 SDK 版本无关。
我们官方的 Firefox 构建在 CI(持续集成)中编译,目前使用的是 13.3 SDK(最后更新于 bug 1833998)。这也是本地构建的最低支持 SDK 版本。
使用不同的 SDK 编译有时会中断。此类中断应 在 Bugzilla 中报告 并快速修复。
获取 SDK¶
有时您需要一个与 Xcode.app 中不同的 SDK,例如,检查您的代码更改是否会中断使用其他 SDK 的构建,或验证 CI 构建中使用的 SDK 的运行时行为。
一种简单但略有疑问的方法是从公共 github 仓库下载 SDK。
这是另一个选项
准备好您的 Apple ID 登录详细信息,并为 5GB 的下载留出足够的时间和耐心。
查看 Xcode 维基百科文章中的这些表格 并找到包含您需要 SDK 的 Xcode 版本。
在 xcodereleases.com 上查找 Xcode 版本号,然后单击其“下载”链接。
使用您的 Apple ID 登录。然后下载应该开始。
等待 5GB Xcode_*.xip 下载完成。
打开下载的 xip 文件。这将提取 Xcode.app 捆绑包。
在应用程序捆绑包内,SDK 位于
Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
。
SDK 版本的影响¶
SDK 只包含 API 的声明。它不包含这些 API 的实现。
API 的实现由应用程序运行的操作系统提供。它在运行时提供,当您的应用程序启动时,由动态链接器提供。例如,AppKit 实现来自应用程序运行的操作系统中的 /System/Library/Frameworks/AppKit.framework
,无论编译应用程序时使用了哪个 SDK。
换句话说,使用更高版本的 macOS SDK 并不会神奇地使新 API 在旧版本的 macOS 上可用。相反,如果您的应用程序在较新版本的 macOS 上运行,则使用较低版本的 macOS SDK 不会限制您可以使用的 API,假设您设法说服编译器接受您的代码。
用于构建应用程序的 SDK 决定三件事
您的代码是否可以编译,
您的应用程序可以运行的 macOS 版本范围(可用的部署目标),以及
运行时行为的某些方面。
第一个很简单:SDK 包含头文件。如果您调用未在任何地方声明的 API - 无论是在头文件还是在您自己的代码中 - 那么您的编译器将发出错误。(特殊情况:调用未知的 Objective-C 方法通常只会发出警告,而不是错误。)
第二个方面,可用的部署目标,通常不值得担心:SDK 支持大量的 macOS 部署目标。例如,10.15 SDK 支持在 macOS 版本上运行您的应用程序,一直追溯到 10.6。此信息记录在 SDK 的 SDKSettings.plist
中。
第三个方面,变化的运行时行为,也许是最阴险和令人惊讶的方面,将在下一节中进行描述。
基于 macOS SDK 版本的运行时差异¶
当发布新版本的 macOS 时,现有 API 的行为可能会发生变化。这些更改通常在 AppKit 发行说明中描述
有时,这些行为差异有可能破坏现有的应用程序。在这些情况下,Apple 通常会提供旧的(兼容的)行为,直到应用程序使用新的 SDK 重新构建,期望开发人员在更新到新 SDK 的同时更新他们的应用程序,使其与新的行为兼容。
这里有一个 来自 10.13 发行说明的示例
NSCollectionViews 中的响应式滚动仅在 macOS 10.13 或更高版本上链接的应用程序中启用。
这里,“在 macOS 10.13 或更高版本上链接”表示“针对 macOS 10.13 SDK 或更高版本链接”。
Apple 的期望是,您在发布新版 macOS 时升级到新版本,在发布新版 Xcode 时下载新版 Xcode,在所有参与应用程序开发的开发人员的机器上同步这些更新,使用最新 Xcode 中的 SDK 编译您的应用程序,并在您更新 Xcode 时更改您的应用程序以兼容任何行为更改。这种期望并不总是与现实相符。它肯定与我们对 Firefox 的做法不符。
对于 Firefox,依赖 SDK 的兼容性行为意味着,如果本地构建 Firefox 的开发人员使用的 SDK 与 CI 中使用的 SDK 不同,则他们可能会看到与我们的 CI 构建的用户不同的运行时行为。也就是说,除非我们更改 Firefox 代码,使其无论 SDK 版本如何都具有相同的行为。通常可以通过以更符合 API 推荐使用方式的方式使用 API 来实现。
例如,我们遇到过 搜索字段中的占位符文本损坏、缺失 或 双绘制焦点环、启动崩溃、全黑窗口、全灰窗口、活力效果损坏 以及 暗模式下颜色损坏 等问题。
在大多数情况下,损坏要么非常轻微,要么是由 Firefox 执行一些明确不鼓励的操作造成的,例如创建意外的 NSView 层次结构,或者依赖未指定的实现细节。(有一个例外:在 10.14 中,支持 HiDPI 的 NSOpenGLContext
在分层窗口中的渲染功能完全失效。)
并且在所有这些情况下,都是依赖 SDK 的兼容性行为保护了我们的用户免受损坏的影响。我们的 CI 构建继续工作,因为它们是用旧版 SDK 构建的。
我们已解决了使用较新 SDK 构建 Firefox 时所有已知的损坏问题。截至撰写本文时(2020 年 6 月),我还没有意识到任何当前的此类问题实例。
有关这些兼容性技巧如何工作的更多信息,请阅读 覆盖依赖 SDK 的运行时行为 部分。
支持多个 SDK¶
如 支持的 SDK 中所述,Firefox 可以使用各种 SDK 版本进行构建。
此功能需要付出一些人工成本;它需要一些恰当放置的 #ifdefs
和头文件定义的复制。
每个 SDK 都使用与 SDK 版本匹配的值在 SDK 的 AvailabilityMacros.h
头文件中定义宏 MAC_OS_X_VERSION_MAX_ALLOWED
。此头文件还定义了版本常量,如 MAC_OS_X_VERSION_10_12
。例如,我有一个 10.12 SDK 的版本,其中包含以下行
#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_12_4
名称 MAC_OS_X_VERSION_MAX_ALLOWED
具有误导性;更好的名称应该是 MAC_OS_X_VERSION_MAX_KNOWN_BY_SDK
。使用旧版 SDK *不会*阻止应用程序在较新版本的 macOS 上运行。
借助 MAC_OS_X_VERSION_MAX_ALLOWED
宏,我们可以使我们的代码适应正在使用的 SDK。这是一个 示例,其中 10.14 SDK 更改了 NSApplicationDelegate
方法 的签名
- (BOOL)application:(NSApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14
restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler {
#else
restorationHandler:(void (^)(NSArray*))restorationHandler {
#endif
...
}
我们还可以使用此宏以不与 SDK 中的定义冲突的方式提供缺失的 API 定义。这在“使用 macOS API”文档中,“使用旧版 SDK 的新 API”部分有描述。
覆盖依赖 SDK 的运行时行为¶
本节包含有关依赖 SDK 导致不同运行时行为的兼容性技巧的更多详细信息,如 基于 macOS SDK 版本的运行时差异 中所述。
工作原理¶
AppKit 是我所知道的唯一一个使用这些技巧的系统框架。让我们通过回到上面 NSCollectionView 示例 来探讨 AppKit 如何实现这一点。
NSCollectionViews 中的响应式滚动仅在 macOS 10.13 或更高版本上链接的应用程序中启用。
对于这些依赖 SDK 的行为差异中的每一个,新版 macOS 中附带的 AppKit 版本都实现了旧行为和新行为。在运行时,AppKit 通过调用 _CFExecutableLinkedOnOrAfter()
基于 SDK 版本选择其中一种行为。此调用会检查正在运行 AppKit 代码的进程的主可执行文件的 SDK 版本;在我们的例子中,是 firefox
或 plugin-container
可执行文件。链接器将 SDK 版本存储在可执行文件的 mach-o 标头中。
AppKit 兼容性技巧的一个有趣的设计方面是,大多数这些行为差异可以通过“用户默认”偏好设置来切换。例如,“NSCollectionViews 中的响应式滚动”行为更改可以通过名为“NSCollectionViewPrefetchingEnabled”的用户默认值来控制。只有当“NSCollectionViewPrefetchingEnabled”未设置为 YES 或 NO 时,SDK 检查才会发生。
更准确地说,此示例的工作原理如下
-[NSCollectionView prepareContentInRect:]
是支持旧行为和新行为的功能。它调用
_NSGetBoolAppConfig
获取“NSCollectionViewPrefetchingEnabled”的值,并提供一个“默认值函数”。如果未设置用户默认值,则会调用默认值函数。此函数名为
NSCollectionViewPrefetchingEnabledDefaultValueFunction
。NSCollectionViewPrefetchingEnabledDefaultValueFunction
调用_CFExecutableLinkedOnOrAfter(13)
。
如果列出以 DefaultValueFunction
结尾的 AppKit 符号,例如执行 nm /System/Library/Frameworks/AppKit.framework/AppKit | grep DefaultValueFunction
,则可以找到许多类似的切换。
覆盖依赖 SDK 的运行时行为¶
您可以以 _NSGetBoolAppConfig()
可以获取的方式以编程方式设置这些偏好设置,例如使用 registerDefaults
或如下所示
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSViewAllowsRootLayerBacking"];
AppKit 发行说明中提到了此功能,但要求仅将其用于调试目的
在某些情况下,我们提供默认(偏好设置)设置,可用于获取旧行为或新行为,而与应用程序构建的目标系统无关。通常,这些偏好设置仅用于调试目的;在某些情况下,可以通过注册值来全局修改应用程序的行为(在非常早期的某个地方执行此操作,使用
-[NSUserDefaults registerDefaults:]
)。
有趣的是,他们根本提到了这一点,因为据我所知,这些值中没有任何一个有文档记录。