[Diving into WWDC 2017] What’s New in LLVM

LLVM 的新特性

Xcode 9 中的 Apple LLVM 编译器具有更新的语言特性、改进的诊断技术和更强大的优化。本文主要介绍 LLVM 对 Objective-C 和 C++ 新增的支持、代码警告和静态检查中新增的技术点,并通过 LTO 来了解 LLVM 编译技术如何为 App 提供更快的构建时间和更好的运行时性能。

一、API 可用性检查 

App 在做向后兼容性时,新版 iOS SDK API 的使用是个隐蔽的风险点,如果在旧的 iOS 系统上调用了新 API,程序就会崩溃。

通常开发者会采用在代码里判断运行时的 iOS 系统版本来决定调用什么样的 API,但这种做法并不方便测试,很容易出错。

针对这个问题,Apple 一直在改进 Xcode 和 LLVM 的自动化机制帮助提高新版 API 的使用安全性。早在 2015 的 Swift 版本中,就已经加入了 #available 使编译器检查代码是否在旧版 iOS 上使用了新 API。

在 Xcode 9 中,编译器增加了 Objective-C 版本的 API 可用性检查。

开发者可以使用 @available 标识在运行时判断 API 的可用性,对那些大于 iOS 最低部署版本的 API 调用,如果代码未做可用性判断,在编译时将会产生警告。

@available 的使用形式为

if (@available(iOS 11, *)) {  
    r = [VNDetectFaceRectanglesRequest new];    // iOS 11 新增的类 
    if ([handler performRequests:@[r] error:&error]) { 
        // Draw rectangles 
    } 
} else { 
    // Fall back when API not available 
} 

通过这种写法进行可用性判断之后,编译器将不再产生警告,并且在运行时就根据 iOS 系统版本执行相应的代码。

其中  if (@available(iOS 11,*)) 这一句具体解释为

  1. 在 iOS 设备上如果 iOS11 的 API 可用则返回 true
  2. 在其它系统类型的设备上直接返回 true

也就是说,@available 括号里可以指定 iOS / tvOS / macOS 多个平台的系统版本以实现跨平台使用。

除了系统 API 之外,对于开发者自己的代码,苹果也提供了一个新的宏 API_AVAILABLE 来标注版本兼容性。 (译者注:WWDC Session Video 中展示的名字是 API_AVAILABILITY,但译者在 Xcode 9 beta 中并未找到这个宏,猜测实际对应的是 API_AVAILABLE)

对于开发者自定义的类,如果其中某个方法只在 iOS 11 之后提供,可以采用如下写法。

@interface MyAlbumController : UIViewController 
- (void)showFaces API_AVAILABLE(ios(11.0)); 
@end 

还可以通过 API_AVAILABLE 宏来标注整个类只在 iOS 11 之后提供。

API_AVAILABLE(ios(11.0))  
@interface MyAlbumController : UIViewController 
- (void)showFaces; 
@end

对于 C/C++ 代码,则可以使用 __builtin_available 进行运行时版本可用性的检查。

if (__builtin_available(iOS 11, macOS 10.13, *)) {  
    CFNewAPIOniOS11(); 
}

而 API_AVAILABLE 则可以直接用在开发者自己的 C/C++ 代码中。

#include <os/availability.h> 
void myFunctionForiOS11OrNewer(int i) API_AVAILABLE(ios(11.0), macos(10.13));  
class API_AVAILABLE(ios(11.0), macos(10.13)) MyClassForiOS11OrNewer;  

对于 Xcode 9 新建的工程,编译时会对所有的系统 API 调用都进行检查并对 API 可用版本大于 deployment 版本的情况产生警告,因此开发者需要使用 @available 和 API_AVAILABLE 对相应的 API 进行处理。

对于旧工程,默认只对 iOS 11 / tvOS 11 / macOS 10.13 / watchOS 4 以上的 API 进行检查和警告,旧版 iOS SDK 引入的 API 则不会在编译时检查,这样使得旧工程里的代码不用进行任何重写。如果旧工程也希望像新工程那样进行全面检查,可以在工程配置里进行修改。

二、代码静态检查与分析

苹果鼓励开发者多使用 Xcode 的 Analyze 功能,以提前发现代码中某些隐藏的 bug。

Xcode 9 新增了三项静态检查内容,会在 Analyze 结果中提示给开发者。

2.1 NSNumber / CFNumberRef 的比较

NSNumber 为例,开发者在编写代码时经常需要判断 NSNumber 中的数值是否为 0,在头昏眼花之时就可能写出这样的代码。


然而上面这种写法只是在判断 NSNumber 实例是否为 nil,Xcode 9 则可以检查出这种情况,对应的编译选项如下图所示,默认为 Yes (Aggressive),开发者也可以选择 No 关闭这项检查。

2.2 对实例变量使用 dispatch_once()

dispatch_once() 相信大家都不会陌生,由于其友好的写法和可靠性,已经成为 Singleton 实例的标准写法。但是需要注意的是,dispatch_once() 中的第一个参数 dispatch_once_t 类型的变量只能定义为全局变量或 static 变量,如下图这种写法则是错误的

原因是在堆上的变量,其地址被重复使用,在地址原有值不为 0 的情况下,GCD 无法保证在多线程场景下 block 内容执行且仅执行一次。

所以对于这种实例变量只做一次性操作的情况,苹果给出的做法是使用 NSLock 或者 pthread_mutex 等方式来实现。

2.3 NSMutable* 类型的类属性自动合成 copy 操作

如下图这段代码所示,对于使用 copy 修饰的类属性 photos,编译器会给出警告。

而在实际运行时,程序则会在下图所示的位置抛出异常。这是因为,对 NSMutableArray 实例调用 copy 方法得到的是一个不可变的 NSArray 实例。

同样,苹果也给出了正确的解法,显式重写 setter 方法 + mutableCopy

除了在 Xcode 中通过菜单项 Product -> Analyze 手动调用静态检查外,苹果还在编译配置中加入了开关,使得每次编译都进行静态检查。

由此我们也能看到苹果对代码静态检查功能的重视程度。虽然这样会增加一些编译时间,但确实可以提前发现一些隐藏的问题,提高代码质量。

三、新增的编译警告

3.1 对 block 传出参数的生命期检查更为严格

考虑下面这段代码,Xcode 9 会给出一个警告,这是为什么呢

将上图这个例子再展开分析下,validateDictionary:usingChecker:error 这个方法的出参 error 默认是 autorelease 的,因此编译器在 block 内部处理时会采用先 retainautorelease 的操作。

正常情况下这样做是没问题的,但 enumerateKeysAndObjectsUsingBlock: 这个方法内部实现有一个总的 autoreleasepool,导致的问题是 error 变量在此方法执行结束后会被这个 autoreleasepool 提前释放,产生野指针。


当然,苹果也给出了相应的解决方案,一种是将传入 error 参数改为 __strong,避免被 autoreleasepool 提前释放。

上面这种解法的缺点是改动了方法接口,如果是对外提供的 API 影响会大一些。另一种相对独立的解法就是在方法内 autoreleasepool 的作用域外再创建一个临时变量 strongError

3.2 对函数参数的检查更加严格

如下图所示,Xcode 9 对于未显式指定参数列表的函数和 block 声明会给出警告,对于无参数的函数或者 block,需要显式写明 void


在 Xcode 工程的编译设置中,同样可以针对以上两种检查作单独的开关配置,也可以选择将警告直接升级为编译错误。


四、继续改进对 C++ 的支持

对于 C++ 选手来说,Xcode 9 也带来了一些好消息,对 C++ 代码重构功能做了不少改进。

  • 增加了类以及类成员函数的重命名支持。
  • 增加了根据类成员函数声明自动生成函数定义体的支持。
  • 增加了将一段独立代码自动抽解为新函数的支持。
  • 增加了将一段代码中重复的表达式(值相同)自动抽解为一个临时变量的支持。

另外, Xcode 9 也增加了对 C++17 的支持,包括元组结构绑定、在 if 条件中初始化变量、std::string_view 等诸多新特性。

五、改进链接时优化

链接时优化(以下简称 LTO)是 LLVM 的一项优化特性,其主要原理是:

利用对象文件经过一些优化得到的中间格式在链接阶段再进行深度优化,包含代码逻辑层面的分析,去除实际未用到的函数、变量、甚至局部代码片段,继而减小安装包大小,同时提高了运行时的效率。

希望深入了解 LTO 的同学可以进一步学习 LLVM 官方文档以及 WWDC 2016 对于 LTO 的介绍。

对于 LTO,Xcode 9 做出的改进主要是在进一步优化了编译速度。 苹果演示的例子是以某个大型 C++ 工程为参考,对于一次完整链接,Xcode 9 比 Xcode 8 提升了 35%;对于一次增量链接,Xcode 9 比 Xcode 8 提升了近 60%。


最后,苹果对开发者的建议是在 Release 编译时开启 LTO 并设置为 Incremental 模式,据说对包大小和运行时速度有 10% 左右的优化。

总结

作为一个先进的现代化编译系统,LLVM 不断在改进各方面的特性,既包含了对各语言新版本的支持,也在持续提高自动检测代码缺陷的能力。

感兴趣的同学可以继续了解其它几个相关的文档和 Session。