# 【引言】为什么开启本话题
从2017年开始,OC语言可以使用 @available 语法糖判断运行时的系统版本,该语法糖可以帮助我们去掉很多烦人的警告。
2019年,@available 的内部实现进行了优化&升级,随着升级,一个副作用也随之而来:Xcode 10 中编译链接时如果依赖了使用 Xcode 11 打包的动态库或静态库会出现链接错误,导致 APP 无法编译成功( https://juejin.im/post/5d8af88ef265da5b6e0a23ac )。
本文将会介绍 @available 的使用场景、原理并会提供一种解决方案。
# @available 是什么
@available 是一个适配低版本运行环境的工具,该工具通常会与 API_AVAILABLE 宏搭配使用。
首先,我们先扩展一下 NSObject 的能力。请注意,我们通过`API_AVAILABLE(ios(13.0))` 标识了该方法只在 iOS 13及以上系统生效。
@interface NSObject (KK)+ (void)methodForIOS13 API_AVAILABLE(ios(13.0));@end@implementation NSObject (KK)+ (void)methodForIOS13 { NSLog(@"%@", self);}@end
随后,我们通过以下方法调用该能力。
if ([[UIDevice currentDevice].systemVersion floatValue]>=13.0) {
[NSObject methodForIOS13];
}
执行编译操作
clang -x objective-c -target x86_64-apple-ios12.0-simulator -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.1.sdk -c main.m
main.m:92:19: warning: 'methodForIOS13' is only available on iOS 13.0 or newer [-Wunguarded-availability-new]
[NSObject methodForIOS13];
^~~~~~~~~~~~~~~
main.m:78:1: note: 'methodForIOS13' has been marked as being introduced in iOS 13.0 here, but the deployment target is iOS 12.0.0
+ (void)methodForIOS13 API_AVAILABLE(ios(13.0));
^
main.m:92:19: note: enclose 'methodForIOS13' in an @available check to silence this warning
[NSObject methodForIOS13];
^~~~~~~~~~~~~~~
1 warning generated.
通过日志可以看到,clang 很“智能”的产出了一个⚠️。但实际上,我们已经判断运行时的版本号,该⚠️是完全不必要的。
切换到 @available 版本后,再次执行编译,上述的 ⚠️ 立马就消失了。
if (@available(iOS 13.0, *)) { [NSObject methodForIOS13]; }
# @available 是如何实现的?
在讲 @available 实现之前,我们先梳理一下整体上的编译流程:
**汇编** 将汇编码转化为机器码
整理如下:
objective-c -<预处理>-> .ii -<编译>-> ir -<编译后端>-> .s (assembler) -<汇编器>-> .o 对象文件(机器码) -<链接器>-> 可执行文件
下面,我们先看看2017年,`@available(iOS 13.0, *)` 被引入时,该语法是如何生效的。
在**编译**阶段,clang 在 AST 新增 `ObjCAvailabilityCheckExpr` 节点,该节点代表源码中的`@available(iOS 13.0, *)`,
根据 AST 产出 IR 时,如果存在该节点,直接转为执行 `int32_t __isOSVersionAtLeast(int32_t Major, int32_t Minor, int32_t Subminor)` 的函数调用。
其次,在**链接**阶段,clang 会自动链接一个静态库 `libclang_rt.*.a`(`/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.0/lib/darwin/libclang_rt.ios.a`)。该静态库提供了 `int32_t __isOSVersionAtLeast(int32_t Major, int32_t Minor, int32_t Subminor)` 函数的实现。
该实现的主要代码逻辑是,读取系统文件 `/System/Library/CoreServices/SystemVersion.plist`,使用 `scanf` 函数读取该文件中 `ProductVersion` 节点的值,并与开发者传入的参数进行比较。
>> 该方法只能在 Darwin平台使用,其它平台不可用。
>> 2017年版本的原始源码已经附在文章末位。感兴趣的读者可以稍后品读一下。
# 链接失败的问题是如何发生的?
上面的方案虽然实现了动态判断系统的方法,但是它严重依赖系统的 `/System/Library/CoreServices/SystemVersion.plist` 文件,不够通用化。所以,设计者们更换了一种更好的方案。
该方案思想如下:运行时环境提供一个判断运行时版本的函数 `_availability_version_check`。开发者的 `@available(iOS 13.0, *)` 代码将会转为执行该函数的实现。
考虑到低版本系统的兼容性问题(低版本运行时没有实现函数 `_availability_version_check`),最终方案为:`@available(iOS 13.0, *)` 调用 `libclang_rt.*.a` 的新函数 `int32_t __isPlatformVersionAtLeast(uint32_t Platform, uint32_t Major,
uint32_t Minor, uint32_t Subminor)`,该函数动态判断宿主是否实现了函数 `_availability_version_check`,如果没有实现,继续调用原有方法,如果实现了,则调用新的函数。
因为 Xcode 11 中附带的静态库 `libclang_rt.*.a`包含新的方法,自然而然的可以直接编译&链接&运行。
但是,一旦通过 Xcode 11 产出了一个静态库或者动态库,该库就会引用外部符号 `int32_t __isPlatformVersionAtLeast(uint32_t Platform, uint32_t Major,
uint32_t Minor, uint32_t Subminor)`
。一旦库被 Xcdeo 10 使用,就会因为无法找到该外部符合的实现导致链接错误 。
# 我们该如何解决?
链接符号缺失的问题思路很简单,手动补上即可。
手动将下面的代码添加到任何一个 `.m` 或者 `.c` 文件,确保被编译&链接即可。
static int32_t GlobalMajor, GlobalMinor, GlobalSubminor;
int32_t __isPlatformVersionAtLeast(uint32_t Platform, uint32_t Major,
uint32_t Minor, uint32_t Subminor) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
const char *VersionStr = [UIDevice currentDevice].systemVersion.UTF8String;
sscanf(VersionStr, "%d.%d.%d", &GlobalMajor, &GlobalMinor, &GlobalSubminor);
});
if (Major < GlobalMajor)
return 1;
if (Major > GlobalMajor)
return 0;
if (Minor < GlobalMinor)
return 1;
if (Minor > GlobalMinor)
return 0;
return Subminor <= GlobalSubminor;
}
# one more thing
聪明的读者可能会立即想到,如果上述代码能够覆盖系统的方法,那么我们是不是相当于对系统函数进行了hook?
答案是:我们只能hook部分场景。
比如,下面的代码就无法被hook。
if (@available(iOS 3.0, *)) {
[NSObject methodForIOS13];
}
实际上,上述代码会经过被编译器进行一个特殊优化,该优化检测到我们设置的运行时版本不会低于 `ios12.0`(通过参数决定 `-target arm64-apple-ios12.0` ),运行时无需判断系统版本就能执行。
所以当APP 运行时,会直接执行 ` [NSObject methodForIOS13];`方法。
附2017年版本的源码:
/* These three variables hold the host's OS version. */
static int32_t GlobalMajor, GlobalMinor, GlobalSubminor;
static dispatch_once_t DispatchOnceCounter;
/* Find and parse the SystemVersion.plist file. */
static void parseSystemVersionPList(void *Unused) {
(void)Unused;
char *PListPath = "/System/Library/CoreServices/SystemVersion.plist";
#if TARGET_OS_SIMULATOR
char *PListPathPrefix = getenv("IPHONE_SIMULATOR_ROOT");
if (!PListPathPrefix)
return;
char FullPath[strlen(PListPathPrefix) + strlen(PListPath) + 1];
strcpy(FullPath, PListPathPrefix);
strcat(FullPath, PListPath);
PListPath = FullPath;
#endif
FILE *PropertyList = fopen(PListPath, "r");
if (!PropertyList)
return;
/* Dynamically allocated stuff. */
CFDictionaryRef PListRef = NULL;
CFDataRef FileContentsRef = NULL;
UInt8 *PListBuf = NULL;
fseek(PropertyList, 0, SEEK_END);
long PListFileSize = ftell(PropertyList);
if (PListFileSize < 0)
goto Fail;
rewind(PropertyList);
PListBuf = malloc((size_t)PListFileSize);
if (!PListBuf)
goto Fail;
size_t NumRead = fread(PListBuf, 1, (size_t)PListFileSize, PropertyList);
if (NumRead != (size_t)PListFileSize)
goto Fail;
/* Get the file buffer into CF's format. We pass in a null allocator here *
* because we free PListBuf ourselves */
FileContentsRef = CFDataCreateWithBytesNoCopy(
NULL, PListBuf, (CFIndex)NumRead, kCFAllocatorNull);
if (!FileContentsRef)
goto Fail;
if (&CFPropertyListCreateWithData)
PListRef = CFPropertyListCreateWithData(
NULL, FileContentsRef, kCFPropertyListImmutable, NULL, NULL);
else
PListRef = CFPropertyListCreateFromXMLData(NULL, FileContentsRef,
kCFPropertyListImmutable, NULL);
if (!PListRef)
goto Fail;
CFTypeRef OpaqueValue =
CFDictionaryGetValue(PListRef, CFSTR("ProductVersion"));
if (!OpaqueValue || CFGetTypeID(OpaqueValue) != CFStringGetTypeID())
goto Fail;
char VersionStr[32];
if (!CFStringGetCString((CFStringRef)OpaqueValue, VersionStr,
sizeof(VersionStr), kCFStringEncodingUTF8))
goto Fail;
sscanf(VersionStr, "%d.%d.%d", &GlobalMajor, &GlobalMinor, &GlobalSubminor);
Fail:
if (PListRef)
CFRelease(PListRef);
if (FileContentsRef)
CFRelease(FileContentsRef);
free(PListBuf);
fclose(PropertyList);
}
int32_t __isOSVersionAtLeast(int32_t Major, int32_t Minor, int32_t Subminor) {
/* Populate the global version variables, if they haven't already. */
dispatch_once_f(&DispatchOnceCounter, NULL, parseSystemVersionPList);
if (Major < GlobalMajor) return 1;
if (Major > GlobalMajor) return 0;
if (Minor < GlobalMinor) return 1;
if (Minor > GlobalMinor) return 0;
return Subminor <= GlobalSubminor;
}