macOS:根据可用性在 运行 时间加载一个或其他系统框架

macOS: Load one or other system framework at run time based on availability

我正在开发一个使用 Apple 的 Safari 框架的 macOS 工具。在 macOS 10.13 中 运行ning 时,工具 links 和从

加载它
/System/Library/PrivateFrameworks/Safari.framework

一切正常。但是当 运行ning 在 macOS 10.12.6 中时,一些行为丢失了。基于对 DTrace 的一些探测,我认为这是因为我的工具需要加载最新的 Staged 框架,它位于:

/System/Library/StagedFrameworks/Safari/Safari.framework

这显然是 Safari 所做的,因为如果我使用 lldb 和 运行 image list 附加到 Safari,在 10.13 中列表仅包含前一个路径,而在 10.12.6 中仅包含后者.

我尝试了以下方法:

NSBundle* stagedBundle = [NSBundle bundleWithPath:@"/System/Library/StagedFrameworks/Safari/Safari.framework"];

在 10.13 中 returns nil 因为目前没有这样的目录。但是,在 10.12.6 中,我得到一个 stagedBundle,然后:

NSBundle* privateBundle = [NSBundle bundleForClass:[BookmarksController class]];
[privateBundle unload];
[stagedBundle load];

卸载和加载显然有效,因为如果我记录这两个包的 -description,在 运行 之前 Private 包是 (已加载) 并且 Staged 包是 (尚未加载),但在 运行 之后根据需要交换这些状态的代码。

但是没有效果。 (1) 如果我再次调用 -bundleForClass:,传递已知在两个框架中都存在的 class,它会给我 Private 包。 (2) 如果我调用 -respondsToSelector:,传递已知仅存在于 Staged 框架中的选择器,我得到 NO.

我尝试按照建议 here 致电 _CFBundleFlushBundleCaches(),但这没有帮助。

我也试过更改我的目标 FRAMEWORK_SEARCH_PATHS,并在我的 Mac 上安装 Staged 框架并 linking 到它, 但由于这个 post 已经太长了,我只想说这导致的热量多于光。

在这种情况下如何有选择地加载框架?

更新

我试过另一种方法。重读 Apple 的 Framework Programming Guide 后,尽管它看起来确实过时了,但我还是决定这个框架需要 弱 linked。这样做了:

这对我来说很有意义,在 10.13 和 10.12.6 中构建和 运行s,但显然 仍在加载不需要的 Private 框架在 10.12.6。 NSLog 报告作为包的路径,class 不响应已知仅在 Staged 框架中的选择器。

还有其他想法吗?

首先,免责声明:我强烈建议您不要依赖于在您交付给用户的任何应用程序中加载私有框架。它很脆弱且不受支撑。

也就是说,如果您真的想这样做,我的建议是使用 Safari 本身用于 select 框架的两个副本之间的相同技术,即 dyldDYLD_VERSIONED_FRAMEWORK_PATH 环境变量。

引用dyld man page

This is a colon separated list of directories that contain potential override frameworks. The dynamic linker searches these directories for frameworks. For each framework found dyld looks at its LC_ID_DYLIB and gets the current_version and install name. Dyld then looks for the framework at the install name path. Whichever has the larger current_version value will be used in the process whenever a framework with that install name is required. This is similar to DYLD_FRAMEWORK_PATH except instead of always overriding, it only overrides is the supplied framework is newer. Note: dyld does not check the framework's Info.plist to find its version. Dyld only checks the -current_version number supplied when the framework was created.

简而言之,这会导致 dyld 在正在加载的框架和版本化框架路径中的框架之间执行版本检查,并加载更高版本。如果版本控制的框架路径不存在或其中不存在相关框架,将使用原始框架路径。

Safari 使用第二个 dyld 功能来简化 DYLD_VERSIONED_FRAMEWORK_PATH 的使用,即 LC_DYLD_ENVIRONMENT 加载命令。此加载命令允许在 link 时指定 DYLD_* 环境变量,dyld 在尝试加载任何依赖库之前将在运行时应用这些环境变量。如果没有这个技巧,您需要在启动应用程序之前将 DYLD_VERSIONED_FRAMEWORK_PATH 设置为环境变量,这通常需要繁琐的 re-exec 才能实现。

将这两个构建块放在一起,您最终会添加如下配置设置:

OTHER_LDFLAGS = -Wl,-dyld_env -Wl,DYLD_VERSIONED_FRAMEWORK_PATH=/System/Library/StagedFrameworks/Safari;

然后您可以 link 静态地针对 /S/L/PrivateFrameworks/Safari.framework,或者尝试在运行时动态加载它。两者都应该导致在运行时加载适当的框架。


为了解决您的问题揭示的一些误解:

The unloading and loading apparently works, because if I log -description of those two bundles, before running that code the Private bundle is (loaded) and the Staged bundle is (not yet loaded), but after running that code those states are swapped, as desired.

不支持卸载包含 Objective-C 代码的共享库。我怀疑 only 它所做的事情是导致 "loaded" 标志在 NSBundle 实例上被切换,因为在 dyld 级别它是忽略。

In Build Settings > Framework Search Paths, listed paths to both frameworks' parent directories, with the Staged path before the Private path, because I want this one to load in macOS 10.12.6, where both exist.

框架搜索路径是一个仅在 compile-time 上使用的概念。在运行时,库的 安装名称 告诉 dyld 在哪里可以找到要加载的二进制文件。