大多数浏览器和
Developer App 均支持流媒体播放。
-
Clang 和 LLVM 的新功能
了解 Clang 编译器和 LLVM 中的增强功能所带来最新 C、Objective-C 和 C++ 改进。了解静态分析器的新功能,以及如何利用这些新功能来改进您的代码。了解如何利用针对代码长度做出的新优化。
资源
相关视频
WWDC21
WWDC19
-
下载
(Clang和LLVM中的 新功能) 在去年 我们一直非常努力地向编译器中 添加一些很棒的新功能 我是Jessica 今天我和我的同事JF和Devin 将与你们分享其中一些非常棒的功能
今天我们有许多内容要讲 我们要讲新平台支持; 一些低级代码尺寸优化; 一些语言级代码尺寸优化; 一些很棒的新诊断; 然后最后我们讲 新的静态分析器检查 那会帮助你查找你代码中的错误
我们先讲新平台支持 (新平台支持) 我要特别讲一下 Series 4 Watch
Series 4 Watch 使用了全64位芯片
但所有App Store app 使用的都是32位芯片
对此稀奇的是你们所有的app 都将在Series 4 Watch上 第一次无缝运行
这听起来像是一种魔法 你不需要重新编译任何东西 但所有的app都能无缝运行
这是怎么发生的呢?
现在如果坐在前排的观众 或坐在任意一排的观众 可以给我这样一个击鼓声真是太棒了 但如果你们不愿意也没关系
答案是Bitcode
我什么也没解释 因此…请允许我给你们解释一下
让我们看一些源代码 是用你们最喜欢的语言写的 我们要做的就是要把它交给编译器 现在一般来说你们在这里会 继续标准的编译过程 你会得到 比如二进制或什么东西 但我们不要这样做 我们要做的是 提前停止编译过程 我们要生成 LLVM Bitcode
关于LLVM Bitcode 很酷的一件事 就是它在编译器中编码一个中间状态 (什么是 LLVM Bitcode?) 你可以用这个中间状态 从你停止它的地方 继续进行编译过程
这就是我们要做的操作 这一次我们要在 App Store中完成
这样我们可以从一个app中 获取一个Bitcode 并从中生成两个不同的app 一个在32位芯片上运行 一个在新型64位芯片上运行
现在这里有个问题 问题就是编译器实际上不知道 你将在一个64位的设备上执行代码 如果它知道的话 它实际上可以利用那个信息 来进一步优化app 为了应付这个问题 我们也收集64位芯片的 Bitcode 那可以让我们从 Bitcode中创建一个 速度非常非常快的app 这非常酷 这就像…
这是其中一件 让编译器变得有魔力的事 非常棒 嗯?
现在让我们转到代码尺寸改进上 这是我最喜欢的话题之一 (代码尺寸改进 低级别代码尺寸优化) 我们一直致力于让编译器生成 尽可能少的代码 现在代码尺寸非常重要 因为代码越多意味着下载越大、越慢 app越大会占用 用户设备更多的空间 并且你也注意到了这种事 因此为了支持用户 我们想优化代码尺寸 这个需求超越了其它的度量 我们在Xcode中添加了一个 新优化级别
即-Oz
我要继续说-Oz 因为我是加拿大人 因此 我们也可以叫它Oh Canada
今天我要给大家展示的是一个 -Oz优化的例子 但在此之前 接下来的几分钟时间 我要尽我所能 给你们讲一下编译器是如何运作的 (编译器是如何运作的?)
当你编译一些代码时 初始表示是非常非常目标独立的 这是源代码 当你把源代码放到编译器中时 它被降级为IR 一种中间表示 这种中间表示的大部分 仍然是目标独立的 但它确实有一些内置的目标依赖功能
看起来有点像通用程序集
这时候你就可以停止编译过程 并给出一些Bitcode 但我们不会这样做 我们要继续进行编译
那个表示… 将被进一步降级到一个叫做 MIR的级别 它是Machine IR的缩写
在编译过程的最后 Machine IR看起来 与目标的程序集几乎相同 在这个例子中是64位程序集 我们可以比如说把代码并排放在一起
在我们要讲的优化中 我们要使用Machine IR 但为了让你们感到熟悉 我将在程序集中完成我们所有的示例 因为那看起来没有那么可怕
我要讲的是一种叫做函数概述的 代码尺寸优化 (函数概述 一种-OZ代码尺寸优化) 函数概述是其中一种优化 它会尽可能地为你缩小一些尺寸 它在编译器中发挥作用的时间 非常非常晚 它实际上不依赖于任意一种 源代码语言
最好是通过例子来解释它的作用
假如你有一些这样的程序集 hasse和kakutani是 一个随机程序中的两个随机函数
对于hasse和kakutani 有意思的是 它们有一些相同的指令
我们可以用它 我们可以获取这些相同的指令
并把它们输出到一个新函数中
在我们把它们输入到新函数之后 我们要做的就是 替换我们所发现的序列 通过调用或分支实现 结果就是我们最后会得到一个 尺寸较小的程序
有多小呢? 嗯 对于我们的测试程序来说 尺寸最多可以缩减25%
现在有些人可能在想 好的 嗯 这里从哪里节约来的呢? 是否是因为复制和粘贴的代码? 是否是因为你的代码需要被重构? 嗯 都不是 实际上这里所发生的事 有一点深奥 如果你复制和粘贴了一堆代码 那会影响概述行为 但那还不是最重要的事
让我们来看另一个例子
假如你有这个函数
这个函数是做什么的没有关系 但我们想提出的是 如果我们把这个函数放到编译器中 会发生什么 我们会得到一些程序集
嗯 你可能会得到像这样的东西
再一次 你其实无需了解这个程序集 但我想让你们注意一下… 函数开端和末端的指令
这些叫做函数的序言与结语 这些指令不响应源代码中的 任意一行代码 这些是由编译器插入的指令 用于满足某种系统要求
因此这些东西 比如这些存储… 和这些负载 可能会在你整个程序的多个地方出现 这就是概述程序可以用于 缩小程序整体尺寸的东西
但有一些与此相关的陷阱 (陷阱) 首先当你进行概述时 你会修改程序的控制流 在这里你最初可能有ulam 叫做collatz 然后你可能把调用概述 到collatz 这里发生的事就是 我们修改了程序的控制流
那么问题是… 如果在collatz内 发生了崩溃会怎样?
嗯 结果就是 你要把你的程序 扔到LLDB中
你将在反向追踪中看到 你添加的概述函数 这是你要留意的东西 如果你实际上在概述代码的话
另一件事是概述 可以增加程序的执行时间
你增加了调用 并且调用会增加执行时间 但这其实还好 因为 -Oz优化了尺寸 这超越了一切 当你在-Oz时 你就在表达说让它变小点
因为这个原因 我们不推荐你用-Oz 编译性能敏感性代码
如果执行时间对你的程序至关重要 -Oz并不是最好的方案 (不要用-Oz编译 性能敏感性代码!) 然而我们的确推荐你们使用 Instruments Instruments会告诉你 你程序中的热点在哪儿 这可以让你做出 关于app优化的最佳决策 编译器有许多不同的优化级别
这些优化级别
都优化不同的东西
比如 -Oz不惜任何代价优化代码尺寸 结果是你可能会有 稍微慢一点点的执行时间 但在另一方面
你有-O3 -O3会不惜任何代价 优化程序的执行时间 结果就是 你可能会得到一个尺寸较大的程序
-Os是Xcode中的 默认优化级别 因为它很好地平衡了速度和尺寸
但你知道的 你可能有不同的优化需要 因此你可以利用 Instruments来解决
编译器还提供一些额外的优化 我没有足够的时间讲这些 不过我还是想稍微提一下
它提供PGO 即配置文件引导的优化 (额外优化) PGO非常酷 因为它允许你实际上执行你的程序 然后收集关于程序运行得如何的信息 然后当你再次编译程序时 你可以用那个信息来指导编译器
它还提供LTO 即链接时间优化 关于链接时间优化的很酷的事就是 以编译时间为代价 你可以告诉编译器说 “好的 让我们等待直到 我们在程序中得到每一个文件 并用每一个文件来 比如说提供更好的代码嵌入或概述” 优化比如代码嵌入或概述 如果有更多的情境 会更好发挥作用 因此LTO会提供帮助
你还可以合并使用这些额外的优化 通过现有优化级别实现 从而获得非常非常优秀的实际性能
因为我没有足够的时间一一讲到 我推荐你们查看之前的 LLVM中的新功能演讲 你可以了解更多信息
了解这些之后 你可能在想 好的 我要如何启用-Oz? 嗯 只需要进入项目的创建设置 并选择-Oz作为优化级别
你还可以在Xcode的特定文件上 启用-Oz或其它优化级别 通过进入项目的创建阶段 进入编译源列表 并设置编译器标志实现
好的 我给你们讲了很多内容 你很可能在想 好的 这对app的代码尺寸有何影响呢? 我该如何得到这种信息? (这会对app代码尺寸 产生何种影响?) 嗯 要得到代码尺寸 我推荐一个叫做Size的小工具
我经常使用这个工具 它是一个很不错的小的终端app Size会为你提供 关于你app的一些很不错的 低级二进制信息 它不会告诉你app的实际总尺寸 因为它不包含比如像资产一样的东西 如果你在app中 有一个比如巨型照片 并且有像Hello World 这样的代码 编译器将不会给你提供帮助
但假如你想使用Size 这是使用方法 非常简单 你只需要告诉Size说 这是进入我的二进制的路径 它将会 它将为你提供像这样的输出 它会告诉你 你二进制中的每个片段的尺寸 并且它还会告诉你二进制的整体尺寸
但问题是… 二进制中的每个片段实际上 包含许多部分
在这个例子中我只关心可执行指令 因此我要做的就是 为Size提供一些额外标志 这些额外标志是-l和-m标志 我这样做Size将 给我提供每个部分的尺寸 如果我想了解 关于可执行指令的更多信息 我只需要查看文本部分即可
我希望这些可以帮助你们 获得关于app代码尺寸的更多理解 现在我要走了 我要把舞台交给我同事 同样是加拿大人的JF 他会与大家分享一些 语言级的代码尺寸改进 (代码尺寸改进 语言级优化) 谢谢Jessica
我是Jeff 我要讲语言级优化 Jessica给你们讲了低级别 有点像是程序集优化 我要讲的是 当你使用语言自身时所发生的事 好的 那么是你编写代码的构造 我今天要讲四种优化 它们也会影响代码尺寸
第一个与Objective C 有关 当你使用代码块时 是的 那么代码块有一堆相关联的元数据 是编译器为你生成的 它有元数据以及帮助函数 我们会通过一些例子来看具体的内容 假如说你写了一些代码 看起来是这样的 好的 那么请注意… 我有两个不同的代码块 位于两个不同的函数中 代码并不重要 但重点是要注意 代码块执行的是完全不同的功能 好的 代码之间并没有任何关系 但它们的结构非常相似 在这一点上 捕获代码块的方式是相似的 在这个例子中 我捕获了两个强健的ARC指针 现在我告诉你每个代码块都有 与之相关联的元数据 那是什么样子的? 嗯 这是我们正在讲的元数据 好的 编译器自动为你生成元数据 当你使用代码块来追踪 与代码块相关的信息 并给它们提供语言担保的行为时 你要注意的是有代码块尺寸 有一个复制帮助和销毁帮助 我们需要稍微了解一下这两个方法 还有一个代码块方法签名 以及代码块布局信息 好的 如果你看一下 屏幕上的例子 这实际上是编译器生成的合成代码 那看起来是一堆技术术语 但重要的是结构自身是一样的 因此我们可以在许多例子中进行复制 请注意在这个例子中我们不能复制 因为除了捕获两个强健的ARC指针 还存在其它捕获 并且代码块尺寸自身也不一样 好的 我们不能合并这些情况 好的 但总的来说 在某些情况下 我们可以合并它们 是的 请注意在这个例子中
我们有函数 是的 复制帮助和销毁帮助可以合并 请注意自Xcode 11起 我们把它们合并了 是的 那么这些是一样的 那意味着什么? 嗯 复制帮助用于帮助你移动代码块 是的 销毁帮助用于帮你移除代码块 是的 当你这样做时 编译器合成的代码 看起来就像这样
好的 那么在我的例子中 你记得我说过 我们有两个强健的ARC指针 我们所生成的代码是像那样的代码 我们复制它时 就会保留它 当你销毁它时 就会释放它 现在当你复制或销毁时 代码块还有一堆其它事要做 你可能有C++对象 在这种情况下你需要调用 复制构造函数 你可能有…以及销毁构造函数 是的 你可能有一些很弱的ARC指针 你可能有具有意义的C类型 或类似的东西 是的 还需要发生许多其它事 但基本上来说 你使用代码块写代码 当编译器检测到有冗余时 我们尝试尽可能地剔除它 这会得到多少好处? 嗯 我们发现在 Objective C app中 尺寸大致会缩减百分之二到百分之七 是的 并且这是你免费获得的 它是默认启用的
我要讲的第二个优化 与NSObject的 直接子类的实例变量有关 这太拗口了 我要给你看一个例子 并解释一下具体是什么意思 好的 假如我正在写一个卡牌游戏 我的代码就像这样 关键是要记住 我正在从NSObject中 直接提交 是的 当我写 Objective C代码时 我的属性响应实例变量 是的 会自动生成实例变量用于备份 我在这里所拥有的属性 是的 现在我所编写的那个类自身 编译器查看它并生成一个结构 看起来大概就像这样 是的 它列出了成员 一个接一个 在Objective C中的 问题是 你可以从它派生出一个基类 然后把一个框架中的代码 修改为另一个框架的代码 然后基类也会改变 并拥有新成员等等 派生类不会中断 是的 并且Objective… 在C++中你就不能这样做 如果你派生 就会改变基类的布局 你就有新的尺寸 在这里 我从NSObject中进行派生 NSObject实际上 是平台的ABI的一部分 我们知道它不会发生改变 是的 因此我们有类的这种布局 当我们实施类时 是的 我实施这个 initWithName方法 我就知道类中的每一个东西的布局 是的 那么自Xcode 11起 编译器 就可以说我知道插件在哪儿 我可以对它们进行硬编码 嗯 好的 那实际上是什么意思? 我要看一下这个 initWithName方法
它看起来是这样的东西 是的 我让 self.name = name 现在设置函数为它生成了一段代码 在Xcode 11之前 看起来是类似这样的代码 是的 它把查找合成到了一个 了解名称属性的插件 或名称IVAR的插件的表中 代码量很少 但自Xcode 11起 我们想实现的是这样的操作 是的 当你实施方法时 我们要硬编码插件 那个方法是S对象的直接派生物 这就可以理解了 我们知道它不会变 因为我们实施的是你刚写的方法 这看起来不重要 只是三个指令中的一个 是的 但它会缩减app 大概百分之二的尺寸 非常棒
我要讲的下一个尺寸优化 是对C++类型改进了可调试性 你可能会说 等一下 这不是尺寸 它是尺寸 让我来解释一下 好的 我说我写了一些代码 这是非常直截了当的代码 它是命令行app 是的 我要做的就是从命令行中取出参数 作为字符串 并把它们转换为整型 并把它们放到 std::vector中 然后把它们一个接一个地打印出来 是的 这是一个非常直截了当的演示app 重点是我使用了标准库的类型 特别是我使用了矢量 pushback 我想在这里放一个断点 这在Xcode 11之前 可能不那么好用 原因是我们控制着 库C++方法的可见性 就像pushback强制把它们 嵌入你的代码中一样 是吧 一般来说没有问题 问题是pushback是一系列 复杂操作 然后优化程序进城去 四处游逛 删除一些代码 而调试程序 当你告诉它 在pushback时停止时 你不是在尝试进入pushback 你只是想在那一行代码中放一个断点 其实调试程序不知道 pushback在哪 因为到处都是 对吧 那么自Xcode 11起 我们不再强制代码嵌入 我们让代码嵌入程序决定 何时应该发生代码嵌入 是的 在这个具体的例子中 在Xcode 11之前 你放在那儿的断点实际上会在 第二个循环时停止 对吧 因为到处都是pushbacks 自Xcode 11起 我们不再强制代码嵌入 这是调试会话 假如我运行lldb 我运行我的程序 假如我在第12行放了一个断点 是的 非常简单明了 调试程序进入并说耶 断点 找到它了 现在我点击运行 现在会发生这样的事
好的 那么我在第12行停止了 酷 起作用了 是的 这个演示有点无聊 因为它达到了你的预期
它原来不会 现在很酷的是 我正在讲代码尺寸优化 是的 嗯 这个 因为我们不强制嵌入 非常大的东西 如果你在代码中频繁使用stl 你会创建相当大量的代码膨胀 我们测量了在大app发布模式上 如果你这样做 你会缩减代码尺寸 最多可以缩减百分之七 再一次 这是在发布模式中 对吧 这节约了相当大量的代码 并进行了相当好的调试 非常棒 我要讲的最后一个缩小代码尺寸的是 C++静态销毁程序抑制 (C++静态销毁程序抑制) 再一次 让我们通过一个例子来解释 我具体表达的是什么意思 假如我写了一些 非常通用的C++代码 大部分app最后都会有一个记录器 就是像这样的东西 是吧 当你记录时 你不想在app中传递记录器 因此你有一个全局变量 叫做logger 非常简单明了 在C++中 当你拥有一个这样的 全局变量时 会有一个销毁程序 它在app的生命周期的末端运行 好的 请注意 logger包含一个缓冲区 是字符串的std::vector 这就是销毁程序的功能 它会销毁字符串的那个矢量 是的 目前来说简单明了 现在我进入我的app 它是个游戏 是的 我在这里添加这段代码 它只是个游戏 我只有一个app 和一个游戏 所以我在这里有一个全局变量 代码完全合理 是吧 现在问题是如果我进入 并在游戏结构中添加一些记录代码 嗯 请注意 logger是全局变量 游戏也是全局的 那可能不太管用 原因是在C++中 在不同的翻译单元之间 不能保证调用销毁程序的次序 是的 在许多情况下 你会在销毁游戏之前 销毁logger 这会导致崩溃 非常不好 是吧 这个问题有点头疼 然后让我们再深入去看 C++是如何运作的 这是C++在我心目中的样子 是的 那么你开始添加线程本地存储 你开始添加线程 比如C++销毁程序次序的图表 非常复杂 它太过复杂了 甚至和编译器的复杂程度差不多 人们想要了解它是如何运作的 但却没有任何概念 几个月前我需要 在Clang中修复一个错误 在非常罕见的情况下 当我尝试清理Clang自身时 Clang会在终端发生崩溃 是吧 这有点尴尬 这只是表明要得到正确的销毁次序 并不是一件小事 好吧 让我们在深入一点 在iOS上… 这是app的生命周期 是的 其实app并没有 一个关闭的合理时间 有时候当app进入前台时 却进入了后台 并且关闭了 但跟销毁一样 好像app的关闭 对于那种类型的生命周期来说 意义不大 最后的结果就是你实施回调 类似这样的东西 是的 然后app告诉你 你将进入后台 你将返回去 类似这样的 销毁程序不完全是在一个 合理的地方运行 如果我们返回我们之前的代码 是的 它是个app 它有一个logger 这是我们写的 嗯 其实并没有一个合理的时间 让这个logger 刷新它的缓冲区 是吧 但跟在销毁程序中一样 你真正想要做的就是说 嗯 如果你要进入后台 请首先刷新缓冲区 是吧 你在销毁程序中没有任何清理要执行 销毁程序看起来有点傻 生成了一大堆代码 但却没有发挥任何功能 是吧 那么自Xcode 11起 我们添加了一个属性 允许你说 嘿 不要销毁这个东西 是的 它是全局属性 它不需要销毁程序
当然了 当发生回调时 你仍需要手动刷新 并且你可以在Xcode中 进入整个app 并使用设置对整个app设置 那个全局属性 看起来非常微不足道 但它却根据你在代码中 使用了多少C++ 给你提供也许是缩减百分之一的 代码尺寸 是的 那非常棒 让我们从代码尺寸缩减继续讲 我们要讲一下诊断 我要讲五种诊断 在Xcode 11中 默认都是开启状态 第一个是 call-to-pure-virtual函数 来自构造程序或销毁程序 那是什么意思? 让我们写一些面向对象的代码 从表开始 好吧 我有这个表 我想用一个纯虚函数来说明 我正在说什么 我要写这个galahad函数 它是纯虚函数 我要给表添加一个销毁程序 当表被销毁时 我要说 galahad 请查找把手 是的 这非常有意义 我那样做了 我得到了一个警告 自Xcode 11起 你得到这个新警告的原因是 因为从构造程序或销毁程序中 调用纯虚函数没有任何意义 没什么可以调用的函数 因为表是基类 绝大部分派生类已经在这种情况下 被销毁了 是的 没有任何实施需要再调用 这个galahad函数了 那你要如何修复这个问题呢? 嗯 你可以 进入实施那个 galahad的派生类 其销毁程序调用查找galahad 并返回一个grail或类似的东西 那就有点道理了 好的 让我们继续看下一个诊断
带转置参数的memset 假如我这个叫做收件箱的结构 其中有一些邮件 我度假回来 我看到收件箱是零 我该怎么做?我只是把 整个收件箱memset为零 现在我写了这段代码 谁可以指出错误?
是的 我把参数调换成了 memset 我时常会犯这种错误 因为我不知道要memset 参数的顺序是什么 我尝试设置销毁的值是否是 第一个参数或是否是第二个参数 是的 它是否是我尝试要设置的尺寸 那么现在自Xcode 11起 我们会对其进行检测 并告诉你 你该如何修复它?非常简单 你只需要把参数翻转过来即可 是的 在这里你可能想要考虑的一件事是 不想使用难以获取的memset 甚至像看代码一样 仍然不明确它是否正确 是的 你想要做的可能是使用像 std::fill这样的东西 在某些情况下讲得通 你重写代码 看起来就像是那样的代码 现在出错几率更小了 但是更容易了解它正在做什么了 是的 看起来有点整洁了
我要讲的第三个警告是… 标准的move返回 move在C++中有点复杂 但总是有许多诊断可以帮助你 以合适的方式使用它 是的 那么再一次 让我们写一段面向对象的代码 来了解我正在讲什么 假如我有三个结构 狮子、山羊和蛇 以及爱面向对象 我要把它们合成奇美拉 是的 代码不错 我要进入并分配 Bellerophon 我想杀死奇美拉 然后返回一个我杀死了它的证据 嗯 现在有个诊断告诉我说 嘿 你知道吗? 比如你正在返回奇美拉 但你只是比如你正在返回的返回类型 实际上是山羊 是的 我要获取那个矢量 复制它 因为从奇美拉中切出矢量 并把矢量放到山羊中是没有意义的 好的 那么你在这里做什么 你所写的代码基本上就是 你要执行的操作 与std::move有关 你依赖于copy elision 是的 在绝大多数时候 当你执行 返回时不需要std::move 在这个例子中你却需要它 因为它执行复制 是的 语言说返回 只是切出类的一部分 不管怎样 听起来有点古怪 move不应该是隐含的 是吧 因此警告告诉你 你很可能不想这样做 是吧 第一种修复方式是进入并调用 std::move
好的 现在那会把矢量移动到山羊中 是吧 这样执行move更有效率一些 你可能想要做的另一件事是 不仅仅返回山羊 还要让人们相信你 它实际上是奇美拉 嗯 你只要返回奇美拉 那就讲得通了 在这里你得到了 copy elision 是的 如果你添加了std::move 编译器会告诉你 你正在通过添加move 让事情变得糟糕 你想要做的另一件事 因为你不确定 你是否会得到奇美拉 就是你可能想返回一个带 std::optional奇美拉 这再一次做了对的事情 它不会从类中切出东西 因此语言说 是的 这会得到隐含的 copy elision 好的 我想讲的另一个诊断是 size-of-pointer-div 它是什么样的? 嗯 假如我写了这段代码 相当不错的代码 目前没有任何问题 是吧 我说的是我有这个数组 取数组的大小除以第零个元素 那会给你提供数组内的元素数量 这是C样式代码的标准代码 是吧
一个很常用的计算 这里的问题是如果我重构这段代码 我做这样的操作 我把数组作为参数进行传递 嗯 那样C规则会说数组 现在衰减到一个指针 而新诊断告诉你说 嘿 这很可能不是你想要的结果 是吧 这不会给你返回数组中的元素数量 这是一个问题 我们发现了它 你该如何修复它呢? 嗯 你可以用稍微不同的方式写代码 我们不使用那种常见的计算 是的 你可用类似 std::size这样的东西 意思是我们不错误地重构代码 当你尝试重构代码时 你会遇到那个问题 是吧 std::size 恰好会为你处理这个问题 这是一种警告 它捕捉错误
我要讲的最后一个诊断是 defaulted-function-delete 再一次 假如我在这里写了 这段漂亮的代码 好的 我的代码结构失常 我有一些浮动的眼柄 一些眼睛和嘴巴 我想默认这种异常结构 请给我一个默认的异常结构 嗯 编译器会告诉你说 嘿 我不知道什么是默认的异常结构 为什么不知道呢? 嗯 我有一个浮动引用 我不能为那个引用 合成错误的构造程序 那是一种我不能默认创建的类型 是的 有许多其它方式 不只是在C++中进行引用 要创建非默认构造的东西 编译器现在会告诉你 如果你请求默认构造程序 它会告诉你我不能给你提供 默认的构造程序 你该如何修复这个问题呢? 嗯 其中一种方式就是 自己创建构造程序 当你传入眼柄时 那会自动创建一个引用 看起来很整洁 但就我而言 我认为情人眼里出西施 但也许这个异常应该以不同的方式 进行编码 而不是通过一个浮动引用 也许你应该这样做 是的 这样做好多了 是的 现在我可以默认创建异常了 好的 那么这就是 我想与你们分享的诊断 现在我要把舞台交给Devin 他会讲一下新的静态分析器检查
(新静态分析器检查)
到目前为止 我们在本场演讲中所讲的警告 都来自你创建时的编译器 但我们还有其它工具 可以帮你查找错误
其中一个就是静态分析器
分析器查找你代码深处的错误 甚至不运行你的app就可以实现 这使它擅长测试 和捕捉那些难以再现的错误 你甚至从未想过为之写测试代码 它甚至会给你显示 错误产生的步骤序列 这样易于理解问题并修复问题
今天我要讲我们所添加的三种 新C++检查 一种检查用在move错误之后; 一种检查用于通过C++ std::string 把C字符串指针挂起; 还有一种检查用于在新的DriverKit 和IOKit中引用计数错误
让我们从第一个开始讲
在C++中 move可以让你避免多余的复制 这里有个例子 在这个例子中 你可能就想要做这样的操作
让我们假设我写了一本小说 如果你们了解我的话 我是一个非常啰嗦的人 当我把它交给我的发行商时 我不想支付关于复制小说的 全部文本方面的性能成本 所以我使用了一个move
这会从源变量中把它移出来 而不是复制它
最棒的是 它允许我强制执行唯一所有权语义 这样就不再混淆 谁拥有小说的最新版本了 我或我的发行商
但我在这里的确需要小心一些 那是因为move会让源变量 处于一个未指定状态
让我们看看这是如何出错的
假如我要在发行小说之后 添加一个调用来进行拼写检查
这可能会导致出乎意料的结果 或甚至会崩溃 取决于所实施的书的类型
幸运的是静态分析器现在可以捕捉 这种错误
要修复这个错误 我应该对代码进行重排序 在我发行小说之前进行拼写检查 非常有必要
好的 让我们继续讲来自 std::string的挂起指针 (来自std::string 的挂起指针) 你们中搞不清楚 C++和C字符串的那些人 知道这非常棘手 这里有个例子
我创建了这个 generateGreeting函数 它接受一个C字符串名称 并返回一个C字符串问候
在这个函数的实施中 我选择使用 C++ std::string 因为它易于操作
我声明了一个 std::string局部变量 把它初始化为hello…
附加上所传入的名称 然后 因为函数返回一个C字符串 我在C++字符串上 调用c str方法 然后从这里开始就出问题了
在这里有一个关键点要注意 就是c str给 std::string内的缓冲区 返回一个内部指针
std::string超出范围时 这个缓冲区就被解除分配了
这意味着我正在给一个 即将被接触分配的内存 返回一个指针 然后当我使用那个内存时 程序会崩溃
现在静态分析器可以捕捉这种错误了
那么该如何修复这个错误呢?
嗯 我们推荐匹配你的C++ 和C字符串的生命周期
在这里我修改了 generateGreeting函数 以返回一个std::string 然后我把结果存储到一个局部变量中
这意味着当我调用c str方法时 只要我需要使用C字符串 那个局部变量都将保留在范围内
实质上我在这里所做的就是 修改std::string的范围 把它的范围改为 只要我需要它就一直持续
请注意 尽可能长时间地 保留在C++内总是很容易 只需要在我需要的那一时刻 取出C字符串即可
好的 让我们继续 我要讲的第三个也是最后一个检查 是在DriverKit中 和IOKit中引用计数错误
这些驱动框架使用手动保留/释放 来管理内存 对于熟悉 CoreFoundation的人来说 或熟悉不带自动引用计数的 Objective-C的人来说 非常类似 (使用手动保留/释放的驱动) 手动保留/释放可以给你提供 大量内存管理方面的控制 但它确实伴随着一些附加的责任
你需要注意不要过度释放内存 因为如果你释放过度 内存可能会被解除配置 然后当你使用它时 你的程序会发生崩溃
类似地 你也不应该内存释放不足 因为内存可能会发生泄漏
让我给你一个内存泄漏的例子
在这里我写了一些代码 分配一批新设备 然后它会填写那些设备 并对它们进行设置
这里的重点是 那个 OSArray::withCapacity 它分配一个新数组并返回保留它
这意味着如果那个数组不被释放 它将会发生泄漏
现在分析器可以捕捉这种错误
那我该如何修复这个错误呢? 我要做的就是确保当我用完数组后 释放它
现在内存管理规则都是基于命名约定 这在实质上与CoreFoundation 和Objective-C非常类似 在手动保留/释放方面
但我想指出IOKit和 DriverKit的关键不同点
即默认约定返回保留 或者我们有时候把它叫做at +1
意思是客户必须 在他们调用的方法的结果上调用释放
否则对象会发生泄漏
这个规则的一个重要的例外情况是 getter返回不保留 或我们把它叫做at +0 客户不应该释放getter的结果 (内存管理约定)
现在你编写的代码 可能与这个约定不符 这是我写的一个示例代码
这个方法在数组中查找第一台设备
它应用了默认约定 它应该返回保留
但如果我们看一下实施 它返回了getter的结果 并且getter返回不保留 那么这里就发生了不匹配
幸运的是分析器现在可以替我们指出 这个问题了
那我该如何修复这个错误呢?
嗯 我有三种可选方案 第一种是修改行为 使其遵守约定
在这里约定是方法应该返回保留 所以我可以在返回结果之前保留它
另一种可能性是重命名方法
如果我看一下这个 findFirstDevice方法 它看起来很像是个getter 我可以把它重命名为 getFirstDevice 那会遵守约定
但你可能还有一个方法用于实施 你想要的行为 并且它的名称很合理 你不想修改它 没关系
在这种情况下 你应该添加一个注释 告诉代码的阅读器和分析器 你是有意不遵守约定的
在这种情况下 我可以添加 DRIVERKIT返回非保留注释 来表明我的意图
如果你有一个IOKit驱动 或你正在编写一个 新DriverKit驱动 我强烈鼓励你在代码上运行分析器
要运行分析器 你要做的就是 进入Xcode的产品菜单 并选择分析 你甚至可以让Xcode 在你每次点击创建时都运行分析器 通过进入目标的创建设置 并启动在创建过程中分析实现 这会帮助你捕捉错误 甚至在你提交之前就捕捉到错误
好的 我们今天讲了许多内容
我们讲了 LLVM Bitcode如何启动 针对watchOS的 无缝的64位转换 而你的32位app将第一次 在Series 4 Watch上运行
我们讲了如何通过新的编译器优化 和语言功能缩减代码尺寸 以及如何在你的代码上运行 静态分析器
要获取更多信息 请查看演讲网站 我们非常希望能在实验室中 与你们沟通 谢谢
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。