8c2a7e3554c5d8d3ad386a523f2d4817
WWDC 2018: What's new in LLVM

目录

一分钟速读

如果你日常写的比较多的,是业务代码,而非底层的基础库,那么此次 LLVM 的更新你可以基本不看,因为他对你的日常开发不会有太多改变,最多是你会发现 Xcode 变得更加严格了——原来正常的代码出现了新的编译器警告。来简单过一下此次 LLVM 的更新内容:

  1. Xcode 10 的 ARC 支持在 C 结构体中直接使用 Objective-C 的对象指针
  2. Xcode 10 增加了 100+ 个编译器诊断(Compiler Diagnostic),你在写代码的时候可能会看到更多的 warning 警告
  3. Xcode 10 的静态检查器新增了对 GCD 不当使用以及自动释放池变量不当使用的检查,同时提升了性能,优化了 Xcode 中 Analyze 后的界面展示方式
  4. LLVM 编译器通过自动添加一些防御性代码,帮助你防范掉「Stack Buffer Overflow」和 「Stack Clash」两个二进制相关的安全漏洞
  5. Xcode 10 中,你可以针对 iMac Pro / iPhone X / iPhone 8 / iPhone 8 Plus 这几款具有新 CPU 指令集扩展的设备进行针对性的优化,这些优化有的是需要自己写针对性的平台代码来实现,有的则是编译器自动帮你实现 。

在开始之前,我们再来回顾一下 LLVM 是什么

LLVM 是包括 Clang、LLDB、LLVM Core 在内的一系列编译器相关的项目的总称。这个项目的主要意义,就是在 GNU 的 GCC 体系之外,创建一套新的编译器以及相关工具的体系,让 Xcode 或者其他工具能够更好的集成进来。我们可以认为,我们的 OC / Swift 代码从编写到 Xcode 为我们生成最终的二进制代码中间的所有的过程,都离不开 LLVM 。即便你对编译器等相关知识不是那么熟悉,也应该记住我们如今习以为常的 ARC ,就是利用 LLVM 实现的。LLVM 的不断迭代,可以为我们带来:

  1. 更简洁的编程体验(如 ARC)
  2. 更全面的编译期错误检查
  3. 更全面的静态分析
  4. 更快的编译速度
  5. 更高效的二进制代码

『What's new in LLVM』 具体讲了什么?

Updates on ARC

在 Xcode 10 之前,如果我们在 C 的结构体中使用对象指针,编译器会毫不留情的给出我们一个红彤彤的错误警告:

根据 Clang 的 ARC 文档 以及 2011 年的 WWDC Session 323 第 21 页,在之前的 ARC 代码中禁止 C 结构体中使用对象指针,是因为 ARC 要求编译器在编译期就知道结构体中对象指针的引用的变化情况。而在当时,这并不好实现。所以当时苹果的工程师直接建议大家使用类而不是结构体。也有 SO 上的热心网友表示我们可以用 __unsafe_unretained 来去掉编译错误。也就是明确告诉编译器,这个对象指针的生命周期你不用管了,我们开发者来管。

现在,我们终于可以像 Swift 一样,在结构体中快乐的声明对象指针,同时避免了自己管理指针引用的担忧,因为对象的 retainrelease编译器都会自动帮我们完成:

不过需要注意的是,如果我们用动态内存分配的方式使用结构体,我们仍然需要自己去手动将对象指针置为 nil:

最后再来说说我个人认为这个变化给我们编程上带来的改变:

我们可以声明一些『轻量』的模型。比如我们有一非常简单的模型对象,就不用很麻烦的声明一个类,而是用一个结构体迅速解决战斗。虽然这里的结构体并不能像 Swift 中的结构体那样强大,但毕竟是一个进步。

最后需要注意的是,带有对象指针的结构体,是不能引入到 Swift 中的。

New Diagnostics in Xcode 10

这一部分对我们最大的影响,就是我们在写代码的时候可以看到更多的编译警告了 😂。苹果的工程师宣称这一次他们增加了 100+ 个代码诊断项,以帮助我们写出更有质量的代码。在 Session 中,他挑了两个主要的诊断作为演示。

Swift and Objective-C Interoperability

我们知道,当我们想在 OC 的代码中使用 Swift 代码时,需要为 Swift 代码生成一个头文件。但是 Swift 有许多语言特性是 OC 没有的。比如逃逸闭包(Escape Closure)。所以 OC 就增加了NS_NOESCAPE这个宏来做兼容。

在 Xcode 10 之前,如果 OC 代码遵从了 Swift 协议又没有正确声明 NS_NOESCAPE,Xcode 不会给出任何提示,现在如果你这么做,Xcode 会告诉你,你需要把 block 声明为 NS_NOESCAPE的:

Packing Struct Members with #pragma pack

这个新的检查项比较简单,一般来说如果我们声明结构体又有字节对齐的需要时,会使用 #pragma pack (push, 1) 以及 #pragma pack (pop) 来实现。这两个 pragma 一般是成对出现的。在 Xcode 10 中,如果你没有成对使用它们,编译器就会给出警告:

Clang Static Analyzer

静态检查器和编译诊断不同,静态检查需要我们手动的执行 Product -> Analyze 才能看到结果。在这一部分,苹果的工程师表示,我们努力了一年,为大家带来了两个新的静态检查, 同时优化了界面展示效果并提升了静态检查的性能。

Grand Central Dispatch performance anti-pattern

所谓 anti-pattern(反模式),一般指的是哪些乍一眼看上去还 OK ,但是实际运行起来往往得不偿失没有益处的代码编写模式。一个典型的例子莫过于『单例模式』,当大家滥用单例模式以后,单例模式就成了反模式。

而在使用 GCD 的过程中也有一种不太好的模式,我们先来看下面一段代码:

+ (NSString *)requestCurrentTaskName {
    __block NSString *taskName = nil;
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    NSXPCConnection *connection =
    [[NSXPCConnection alloc] initWithServiceName:@"MyConnection"];
    id remoteObjectProxy = connection.remoteObjectProxy;
    [remoteObjectProxy requestCurrentTaskName:^(NSString *task) {
        taskName = task;
        dispatch_semaphore_signal(sema);
    }];
    dispatch_semaphore_wait(
                            sema,
                            dispatch_time(DISPATCH_TIME_NOW, 100)
                            );
    return taskName;
}

乍一看,这段代码好像没有什么问题,我们使用信号量来保证当前方法在异步获取到 taskName 之后才会返回。然后仔细推敲一下,这段代码是有问题的:

  1. 当前线程在等待另外一个异步线程结束,而这个异步线程一般是优先级比较低的线程,因此就出现了优先级高的线程等待优先级低的线程这种优先级逆转的不合理现象。
  2. 新建了一个线程,并使用了锁,产生了多余的线程间交互的成本

Xcode 10 会帮我们检查出来这样的问题代码,并给与我们善意的提醒:

那这段代码应该如何改正呢?

一种方法是如果当前的返回值是同步的,就使用同步版本的底层 API:

另外一种是,将当前的方法风格改为异步回调风格:

top Created with Sketch.