前言
近期在排查项目一个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:
工作机制简介:
-
前置概念
先简单介绍下后续在讲解时会遇到的几个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
可以应用各种优化技术来改进代码的性能和效率。
-
Availability 版本限制声明
-
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)));
-
AST定义:Attr.td中声明对应的AvailabilityAttr类型节点
可以看到AvailabilityAttr支持定义平台(platform),引入版本(introduced),废弃版本(deprecated),可用性(unavailable)等多种定义
经过tableGen处理后生成:
-
Parser clang attribute解析:
在Parser解析Token过程中遇到Decl类型节点时,会对其所有的clang attribute进行解析,availability 就是在这个过程被解析的
对availability attribute声明进行进一步的token解析,并存入该Decl节点的attrs属性中
-
@available语句
-
新增对应的token声明
-
// TokenKinds.def
OBJC_AT_KEYWORD(available) //OC中的@关键字
KEYWORD(__builtin_available , KEYALL) //预留关键字
-
AST定义:新增@available表达式节点,代表一个运行时availability的检查
//clang/include/clang/AST/ExprObjC.h
-
Parser Avaliable Token解析 :
-
语义检查:当前调用是否符合Availability限制
-
作用域Context维护
- FunctionScopeInfo:
FunctionScopeInfo
在 Clang 的 Sema 阶段是一个关键的数据结构,用于存储和管理与当前函数作用域相关的各种信息 - Parser在解析到方法调用等情况时,如果被当前被调用的decl节点有availability属性,就会在当前函数作用域的
FunctionScopeInfo
中标记上HasPotentialAvailabilityViolations
为true,这个标记位代表这个函数中存在潜在的availability问题可能性,等待后续该函数解析完后进行校验。
-
校验
在Parser完成一个Function的语法分析后,Sema就会对HasPotentialAvailabilityViolations=true的情况,进行进一步的检查
Availability
的校验逻辑都由DiagnoseUnguardedAvailability
这个类进行处理,可以看到这个类维护了一个叫AvailabilityStack
的变量,这就是编译器存储函数中各作用域版本信息的结构,该类初始化的时候会以编译参数中设置的target 作为默认的版本信息上下文
DiagnoseUnguardedAvailability
的声明中也可以看到它继承了RecursiveASTVisitor,我们可以看出编译器实际就是在函数结束后对所有标记潜在Availability
问题(HasPotentialAvailabilityViolations)的函数进行AST遍历,最终完成整个检测。
遍历过程中遇到@available 语句就push新的版本信息
对不符合版本要求的调用进行报错
看到这里可能有些同学会存在疑惑:编译器为什么要先记标志位然后再进行校验。这其实是一种编译效率优化的策略,即早期标记潜在问题,后期再对有潜在问题的场景进行详细的检查。
-
@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];
-
其他方向功能支持
- Index索引、clang static analyzer静态分析等相关功能支持,这里就不展开说明了,感兴趣的话可以在以下pr中进行了解
主要PR:
- availability __attribute 引入:github.com/llvm/llvm-p…
- @available功能引入:github.com/llvm/llvm-p…
- 编译耗时优化:github.com/llvm/llvm-p…
Swift:
Swift Availability的机制和OC基于相同,本文暂时不做细致说明,下面是一些关键实现文件的总结:
相关文件概览:
-
Availability 版本限制声明
- swift/lib/AST/Availability.cpp
Availability声明的维护
- AST定义:Attr
- Parser
-
#available语句
- Token :
- AST定义:
swift直接复用StmtConditionElement
- Parser解析:
-
校验
- swift/lib/AST/TypeRefinementContext.cpp
类型细化上下文:保存平台、系统版本的信息,其中就包含
AvailabilityContext
- swift/lib/Sema/TypeCheckAvailability.cpp
//负责availability的校验逻辑
-
CodeGen:SILGenDecl
CodeGen:插入_stdlib_isOSVersionAtLeast方法
-
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%的运行时安全。
参考:
Attributes in Clang — Clang 18.0.0git documentation
广告时间
抖音iOS基础技术团队持续招聘,欢迎扫码投递简历加入我们或私信我咨询更多岗位详情~ job.toutiao.com/s/i8C7BSWS