iOS Availability 编译器实现机制解析

666 阅读7分钟

前言

近期在排查项目一个iOS12系统上@available 相关crash问题的过程中,对编译器 Availability 机制的实现细节进行了研究。

在正式开始介绍Availability的实现机制之前先抛出一个问题,那就是你认为available能完全保证运行时安全么?另外一个实际的例子是你认为下面的+load方法在iOS 13以下的系统会执行吗?

API_AVAILABLE(ios(13.0))
@interface TestAvailability : NSObject

@end

@implementation TestAvailability

+(void)load
{
    NSLog(@"%s", __func__ );
}

@end

带着这个问题,我们继续探究编译器对Availability究竟提供了哪些编译期保障。

Availability能力简介

在iOS开发中,一个关键的挑战是处理不同版本系统间的API兼容性。特别是,当应用尝试在较旧的iOS版本上调用新引入的特性时,如果不进行兼容性检查的话就可能导致崩溃,这是因为旧版iOS系统中并未包含这些新API的符号引用。

为了解决这个问题,编译器引入了Availability机制,包含版本限制声明和可用性检查。开发者能够在保持对旧系统版本支持的同时,灵活地采用新版本的特性。

使用方式

在解析Availability实现机制之前,先来说明下Availability的使用方式:

OC:

Availability 版本限制声明:

在 OC 中对API 支持情况都使用API_AVAILABLE来描述

  • class、protocol:

  • method、property:

可用性检查:

- (void)callMethod {
    //这里的if判断代表超过iOS 13才会执行,如果调用了deployment target版本没有支持的新特性且没有用@avavilable进行包裹,会在编译阶段报错: XXX is only available in iOS 13.0 or newer

    if (@avavilable(iOS 13.0,*)) {
         [NSObject methodForIOS13];
    }
 }

Swift:

Availability 版本限制声明:

  • Class、protocol、struct等

  • method、property:

可用性检查:

func callMethod() {
    if #available(iOS 13.0, *) {
        methodForIOS13()
    } else {
        print("iOS系统在13.0以下")            
    }
}

实现解析

Clang:

工作机制简介:

  1. 前置概念

先简单介绍下后续在讲解时会遇到的几个clang Frontend 关键概念:

  • AST 抽象语法树 :AST 是源代码语法结构的一种抽象表示。它以树状的结构表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。AST节点主要有几个类型:

    • Expr(表达式节点):代表运算符、函数调用、类型转换等表达式
    • Stmt(语句节点):代表if、for、return等语句
    • Decl(声明节点):代表变量、函数、类、协议等声明
    • Type(类型节点):程序中的类型信息,如基本类型、数组类型、指针类型等。
    • Attr(属性节点):附加在声明上的属性,我们本次探索的availability就有对应的Attr类型
  • Parser: Clang 前端的核心组件之一,负责将源代码的文本表示转换成抽象语法树(AST)的结构。它执行词法分析和语法分析,将源代码拆分为标记(tokens),然后根据语言的语法规则构建 AST。

  • Sema: 负责执行语义分析(类型检查、作用域解析、名称绑定、类型推断),通过与 Parser 协作,在构建 AST 的同时进行这些语义检查和处理。

  • CodeGen: 将AST转化为IR,在这个过程中clang 可以应用各种优化技术来改进代码的性能和效率。

  1. Availability 版本限制声明

  1. clang attribute
  • API_AVAILABLE进行宏展开,可以看到availability 实际就是clang attribute的一种,这个在clang文档中也有说明:
//声明:
@property(nonatomic, copy) NSString *test API_AVAILABLE(ios(13.0));

//preprocessor 展开后:
@property(nonatomic, copy) NSString *test __attribute__((availability(ios,introduced=13.0)));
  1. AST定义:Attr.td中声明对应的AvailabilityAttr类型节点

可以看到AvailabilityAttr支持定义平台(platform),引入版本(introduced),废弃版本(deprecated),可用性(unavailable)等多种定义

经过tableGen处理后生成:

  1. Parser clang attribute解析:

在Parser解析Token过程中遇到Decl类型节点时,会对其所有的clang attribute进行解析,availability 就是在这个过程被解析的

对availability attribute声明进行进一步的token解析,并存入该Decl节点的attrs属性中

  1. @available语句

    1.   新增对应的token声明

// TokenKinds.def
OBJC_AT_KEYWORD(available) //OC中的@关键字

KEYWORD(__builtin_available       , KEYALL) //预留关键字
  1. AST定义:新增@available表达式节点,代表一个运行时availability的检查

//clang/include/clang/AST/ExprObjC.h

  1. Parser Avaliable Token解析 :

  1. 语义检查:当前调用是否符合Availability限制

  1. 作用域Context维护
  • FunctionScopeInfo: FunctionScopeInfo 在 Clang 的 Sema 阶段是一个关键的数据结构,用于存储和管理与当前函数作用域相关的各种信息
  • Parser在解析到方法调用等情况时,如果被当前被调用的decl节点有availability属性,就会在当前函数作用域的FunctionScopeInfo中标记上HasPotentialAvailabilityViolations为true,这个标记位代表这个函数中存在潜在的availability问题可能性,等待后续该函数解析完后进行校验。

  1. 校验

在Parser完成一个Function的语法分析后,Sema就会对HasPotentialAvailabilityViolations=true的情况,进行进一步的检查

Availability的校验逻辑都由 DiagnoseUnguardedAvailability这个类进行处理,可以看到这个类维护了一个叫AvailabilityStack的变量,这就是编译器存储函数中各作用域版本信息的结构,该类初始化的时候会以编译参数中设置的target 作为默认的版本信息上下文

DiagnoseUnguardedAvailability的声明中也可以看到它继承了RecursiveASTVisitor,我们可以看出编译器实际就是在函数结束后对所有标记潜在Availability问题(HasPotentialAvailabilityViolations)的函数进行AST遍历,最终完成整个检测。

遍历过程中遇到@available 语句就push新的版本信息

对不符合版本要求的调用进行报错

看到这里可能有些同学会存在疑惑:编译器为什么要先记标志位然后再进行校验。这其实是一种编译效率优化的策略,即早期标记潜在问题,后期再对有潜在问题的场景进行详细的检查。

  1. @available CodeGen

CodeGen过程在对AST进行遍历处理时,将ObjCAvailabilityCheckExpr 转换为runtime方法:__isOSVersionAtLeast,在链接阶段,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) 函数的实现。

  • 这里还有一个需要特殊说明的事情,就是@available并不是任何情况都会被转化为runtime 判断,下面的代码会经过编译器的一个特殊优化,该优化检测到我们设置的运行时版本不会低于 ios12.0(通过参数决定 -target arm64-apple-ios12.0 ),运行时无需判断系统版本。
 if (@available(iOS 3.0, *)) {
     [NSObject methodForIOS3];
 }
-->
 [NSObject methodForIOS3];
  1. 其他方向功能支持

  • Index索引、clang static analyzer静态分析等相关功能支持,这里就不展开说明了,感兴趣的话可以在以下pr中进行了解

主要PR:

Swift:

Swift Availability的机制和OC基于相同,本文暂时不做细致说明,下面是一些关键实现文件的总结:

相关文件概览:

  1. Availability 版本限制声明

  • swift/lib/AST/Availability.cpp

Availability声明的维护

  • AST定义:Attr

  • Parser

  1. #available语句

  • Token :

  • AST定义:

swift直接复用StmtConditionElement

  • Parser解析:

  1. 校验

  • swift/lib/AST/TypeRefinementContext.cpp

类型细化上下文:保存平台、系统版本的信息,其中就包含AvailabilityContext

  • swift/lib/Sema/TypeCheckAvailability.cpp

//负责availability的校验逻辑

  1. CodeGen:SILGenDecl

CodeGen:插入_stdlib_isOSVersionAtLeast方法

  1. runtime方法提供:swift/stdlib/core/Availability.swift

//swift runtime:__stdlib_isOSVersionAtLeast方法实现

主要PR:

结论:

通过对Clang & Swift源码的阅读,我们已经了解到实际上编译器对Availability的检查只限定在方法内部的直接调用。将available 转为运行时方法判断,只能保证所有直接调用的地方是有if 包裹的。

回到前言中提出的问题,对于声明了API_AVAILABLE(ios(13.0))的类TestAvailability来说,它的+load在iOS12系统的设备上依然会执行,也就是说实际上Availability 没有办法保证100%的运行时安全。

参考:

clang.llvm.org/docs/Attrib…

clang.llvm.org/docs/Langua…

Attributes in Clang — Clang 18.0.0git documentation

swiftrocks.com/how-availab…

广告时间

抖音iOS基础技术团队持续招聘,欢迎扫码投递简历加入我们或私信我咨询更多岗位详情~ job.toutiao.com/s/i8C7BSWS

Xnip2023-12-25_11-46-56.jpeg