首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >揭秘 @available

揭秘 @available

作者头像
酷酷的哀殿
发布2020-10-26 10:14:17
2.3K0
发布2020-10-26 10:14:17
举报
文章被收录于专栏:酷酷的哀殿酷酷的哀殿

# 【引言】为什么开启本话题

从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 实现之前,我们先梳理一下整体上的编译流程:

  • **预编译** 对源码执行预处理操作,比如展开 `#includes` `#defines`
  • **编译**
    • 解析预处理后的文件,构建 AST(源码中间语言)
    • 根据 AST 产出 IR(编译中间语言)
  • **编译后端** 根据目标机器特性,产出汇编码(可读性高于机器码)

**汇编** 将汇编码转化为机器码

  • **链接** 将多个对象文件组装为单个可执行文件

整理如下:

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;
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 酷酷的哀殿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档