Code Coverage
iOS生成代码测试覆盖率是编译器及其工具链支持的功能,开发者可以通过简单的步骤完成插桩、覆盖率收集以及最终数据html可视化的工作,虽然是一个相对陈旧的技术点,但是其中涉及的一些编译相关的知识还是值得深入挖掘的。
生成代码测试覆盖率基础步骤
1. 添加编译选项(Debug 配置)
在 Xcode 项目中,打开 Build Settings,针对 Debug 添加以下选项:
-
Other C Flags:diff-fprofile-instr-generate -fcoverage-mapping -
Other Swift Flags:diff-Xfrontend -profile-coverage-mapping
2. 运行 App(带覆盖率生成)
正常编译并运行你的 App(如在模拟器中手动操作 App)。App 运行时会生成 .profraw 文件,默认位置在:
Users/yourname/Library/Developer/CoreSimulator/Devices/<UUID>/data/Containers/Data/Application/<UUID>/
或者你可以在 main() 函数中设置输出路径:
objcsetenv("LLVM_PROFILE_FILE", "/tmp/coverage-%p.profraw", 1);
3. 结束运行,收集 .profraw 文件
退出 App 后,找到 .profraw 文件并复制出来,比如到当前目录:
cp /tmp/coverage-12345.profraw .
4. 合并为 .profdata
xcrun llvm-profdata merge -sparse coverage-12345.profraw -o coverage.profdata
5. 生成 HTML 覆盖率报告
xcrun llvm-cov show \
path/to/YourAppBinary \
-instr-profile=coverage.profdata \
-format=html \
-output-dir=CoverageReport
注意:YourAppBinary 一般是 Debug 目录下的可执行文件,例如:
DerivedData/.../Build/Products/Debug-iphonesimulator/YourApp.app/YourApp
也可手动读取编译过程生成的单个Maco-O文件,选择其中需要处理覆盖率的数据合并成一个Mach-O文件,其大小相比.app文件更轻量,本篇以此方案实践落地。
6. 查看报告
打开 CoverageReport/index.html 查看代码覆盖率页面。
关键步骤分析
添加 -fcoverage-mapping 和-fprofile-instr-generate 编译选项,编译器会插入额外的代码来记录每次运行时各个部分的代码是否被执行。这通常包括:
- 函数调用:记录哪些函数被调用。
- 分支执行:记录条件语句(如
if,switch,while,for等)中哪些分支被执行。 - 行执行:记录哪些代码行被执行。
在 macOS 和 iOS 开发中,将 .profraw 或 .profdata 文件中的覆盖率数据与 Mach-O 文件结合来定位具体代码行的过程涉及到 LLVM 的覆盖率工具,主要是 llvm-profdata 和 llvm-cov。这些工具可以解释覆盖率数据和符号信息,将它们映射到源代码文件和行号。下面是详细步骤:
1. 生成和合并覆盖率数据文件
首先,当你运行测试时,Xcode 或 LLVM 工具链会生成 .profraw 文件,这是原始的覆盖率数据文件。这些文件需要合并和转换成 .profdata 格式,以便进一步使用。这可以通过 llvm-profdata 工具完成:
xcrun llvm-profdata merge -sparse *.profraw -o merged.profdata
这个命令将所有的 .profraw 文件合并成一个 .profdata 文件,这个文件包含了所有测试的覆盖率数据。
2. 使用 llvm-cov 映射到具体代码
接下来,使用 llvm-cov 工具来结合 .profdata 文件和 Mach-O 文件(编译后的可执行文件,包含了程序的符号表和调试信息),生成覆盖率报告。这一步是通过符号表和调试信息将覆盖率数据映射到具体的源代码行:
xcrun llvm-cov show \
-instr-profile=merged.profdata \
YourAppExecutable \
-use-color \
-format=html \
-output-dir=CoverageReport \
解释一下关键参数:
-instr-profile: 指向.profdata文件。YourAppExecutable: 指向 Mach-O 文件,即编译后的可执行文件。-format=html: 输出格式为 HTML。
3. 覆盖率数据与源代码的关联
Mach-O 文件包含了程序的符号表和调试信息(如果在编译时开启了调试信息的生成)。llvm-cov 使用这些信息来确定 .profdata 中记录的执行计数和分支信息对应源代码中的哪些行和函数。
- 符号表:包含了函数、方法和变量等符号的名称,这些可以用来识别代码中的特定部分。
- 调试信息:包含了源文件名、行号和其他源码级别的信息,这些信息允许
llvm-cov精确地将执行数据映射到源代码文件和具体行号。
4. 查看覆盖率报告
生成的 HTML 覆盖率报告将提供一个交互式的界面,开发者可以查看每个文件的代码覆盖率,具体到行级别的数据,包括哪些行被执行了,哪些行没有被执行,以及执行的次数。
通过这种方式,开发者可以精确地看到测试覆盖了哪些代码,哪些代码未被覆盖,从而更有效地优化测试案例和提高代码质量。
-fcoverage-mapping 是一个编译器标志,主要用于在编译时启用代码覆盖率数据的生成,这通常用于测试中以确定代码中哪些部分已经被执行(或“覆盖”)。这个选项是 Clang 和 GCC 编译器中与代码覆盖率相关的功能之一。
LLVM和GCC
iOS 覆盖率检测原理与增量代码测试覆盖率工具实现美团的这篇覆盖率相关文章时间比较早,使用的技术方案与本篇介绍的不同,主要原因在于技术方案立足于不同的编译器的编译选项。
LLVM
在 LLVM 编译器框架中,-fcoverage-mapping 主要与 Clang 编译器一起使用。Clang 是基于 LLVM 的 C、C++、Objective-C 和 Objective-C++ 的编译器前端。使用 -fcoverage-mapping 标志时,Clang 会生成额外的代码和数据,用于跟踪程序执行时的代码覆盖情况。这包括函数调用、分支执行和行执行等信息。
- 生成的数据:使用
-fcoverage-mapping时,Clang 会产生.profraw和.profdata文件。.profraw文件包含原始的覆盖率数据,而.profdata文件是使用 LLVM 的llvm-profdata工具处理.profraw文件后生成的,用于更高效的数据管理和访问。 - 覆盖率报告:生成的覆盖率数据可以通过 LLVM 的
llvm-cov工具读取,该工具可以展示覆盖率报告,包括哪些代码行被执行、执行频率以及不同代码路径的覆盖情况。
GCC
GCC(GNU Compiler Collection)也支持代码覆盖率的生成,但它通常使用的是 -fprofile-arcs 和 -ftest-coverage 标志来启用这一功能。GCC 的覆盖率工具是 gcov。
- 生成的数据:在 GCC 中,使用上述标志会生成
.gcno(在编译时生成,包含代码结构信息)和.gcda(程序执行时生成,包含覆盖率数据)文件。 - 覆盖率报告:
gcov工具用于分析这些文件,并生成覆盖率报告,报告显示了代码的哪些部分被执行以及执行的频率。
虽然 -fcoverage-mapping 主要与 LLVM/Clang 关联,GCC 则使用不同的标志来实现类似的功能。两者都提供了生成详细代码覆盖率数据的能力,这对于测试、调试和提高软件质量至关重要。这些工具和标志使开发者能够识别未测试的代码区域,从而确保软件应用的健壯性和可靠性。
LLVM和GCC是两个主要的编译器技术,各自拥有独特的特点和应用场景。以下是它们的比较,以及它们对 iOS 开发中 Objective-C (OC) 和 Swift 的支持情况,以及在代码覆盖率方面的差异。
LLVM与GCC的区别和联系
-
基本架构:
- LLVM:LLVM(Low Level Virtual Machine)最初设计为一种中间表示(IR)和编译器基础架构,提供了一个模块化的重用编译器技术的框架。它包括一系列的编译工具链组件,如 Clang(C/C++/Objective-C 编译器)、LLVM core(提供IR转换支持)等。
- GCC:GCC(GNU Compiler Collection)是一个集成的编译器,支持多种编程语言(如 C, C++, Java, Ada 等)。它传统上是一个单一的大型项目,包含前端、优化器和后端。
-
设计哲学:
- LLVM:更注重于编译器的模块化和可重用性。其设计允许开发者轻松添加新的优化或支持新的编程语言。
- GCC:虽然也支持多语言,但它的模块化程度较低,扩展和添加新特性通常更复杂。
-
性能和优化:
- 在性能和优化方面,LLVM和GCC都非常强大,但LLVM在某些情况下提供了更好的优化和更快的编译速度,特别是在增量编译方面。
对iOS Objective-C和Swift的支持
-
Objective-C:
- LLVM / Clang:Clang是Apple官方推荐的Objective-C编译器,完全支持所有Objective-C的特性,包括ARC(自动引用计数)等。
- GCC:虽然过去GCC也支持Objective-C,但自从Apple转向Clang后,GCC在Objective-C的支持上逐渐减少,现在已不是iOS开发的首选。
-
Swift:
- LLVM:Swift编译器背后就是基于LLVM的。Swift与LLVM紧密集成,这使得Swift能够利用LLVM的所有优化和特性。
- GCC:GCC目前不支持Swift。Swift的开发和编译完全依赖于LLVM基础架构。
在覆盖率层面的区别
-
LLVM:
- LLVM的代码覆盖工具(如llvm-cov)与Clang紧密集成,提供了对代码覆盖率的详细分析,包括分支覆盖和行覆盖等。
- 使用
-fprofile-instr-generate和-fcoverage-mapping标志来生成覆盖数据。
-
GCC:
- GCC使用gcov工具来进行代码覆盖率分析,支持行覆盖率和分支覆盖率。
- 使用
-fprofile-arcs和-ftest-coverage标志来启用覆盖率跟踪。
LLVM和GCC都是强大的编译器技术,各有优势。对于iOS开发,尤其是涉及到Objective-C和Swift,LLVM(特别是Clang和Swift编译器)是首选,提供了更好的集成和支持。在代码覆盖率方面,LLVM提供了更现代化和紧密集成的工具,而GCC则提供了传统且广泛使用的gcov工具。
XCode由GCC转向LLVM
在 2018 年使用 Xcode 时,尽管 GCC 编译器本身不再被 Xcode 直接支持,但 LLVM/Clang 编译器却实现了对一些 GCC 编译器标志的兼容性。这就是为什么开发者仍然可以在 Xcode 中使用 -fprofile-arcs 和 -ftest-coverage 这样的标志来启用代码覆盖率跟踪。
关于 -fprofile-arcs 和 -ftest-coverage 标志:
这些标志原本是 GCC 用来启用代码覆盖率数据收集的,它们会让编译器插入必要的代码来记录哪些代码行被执行了。当 LLVM/Clang 成为 Xcode 的主要编译器后,为了保持对现有项目的兼容性以及简化从 GCC 到 Clang 的迁移过程,LLVM/Clang 开始支持这些 GCC 特有的标志。
编译过程中的使用:
当你在 Xcode 中使用 -fprofile-arcs 和 -ftest-coverage 标志时,你实际上是在使用 LLVM/Clang 编译器,而不是 GCC。Clang 处理这些标志的方式是生成与 GCC 兼容的覆盖率数据,这样工具如 gcov 或其他基于 GCC 覆盖率数据格式的工具仍然可以使用这些数据。
- 编译器:即使使用了 GCC 的标志,编译过程仍然是通过 LLVM/Clang 进行的。
- 兼容性:Clang 对 GCC 标志的支持确保了对旧项目的兼容性,使得开发者可以在不改变工具链的情况下继续使用现有的代码覆盖率工具。
- 代码覆盖率工具:生成的覆盖率数据可以被
gcov或其他兼容的工具使用,尽管数据是由 Clang 生成的。
这种做法体现了 LLVM/Clang 在设计时考虑到的广泛兼容性和用户便利性,使得开发者可以平滑过渡并继续使用他们熟悉的工作流程。
-fprofile-arcs 和 -ftest-coverage可以添加到Other Swift Flag里吗?
不可以。
总结
虽然在过去,Xcode 曾经支持使用 GCC,但自从 Xcode 4.2(发布于 2011 年)之后,Apple 完全转向使用 LLVM/Clang 作为其主要的编译器。这是因为 LLVM/Clang 提供了更好的支持和优化,特别是对 Objective-C 和现在的 Swift(后者完全依赖于 LLVM 的基础设施)的支持。
因此,当前的 Xcode 不再使用 GCC,而是使用基于 LLVM 的 Clang 编译器。
补充其他技术点
llvm-cov show OR genhtml From .info
使用 llvm-cov show 直接生成覆盖率报告和先转换为 .info 文件再使用 genhtml 生成 HTML 报告,这两种方法各有其优劣。下面详细分析两者的不同点及各自的优缺点:
使用 llvm-cov show 直接生成覆盖率报告
llvm-cov show 命令直接从 .profdata 文件和目标程序中生成覆盖率报告。它可以输出到控制台或者生成 HTML 格式的报告。
优点:
- 简单直接:不需要额外的转换步骤,可以直接从
.profdata文件生成覆盖率报告。 - 紧密集成:作为 LLVM 工具链的一部分,
llvm-cov show可以更好地处理 Clang 和 LLVM 生成的数据,可能提供更准确的覆盖率信息。 - 支持多种输出格式:除了 HTML,还可以输出为文本或 JSON 等格式,便于自动化处理和集成。
缺点:
- 功能限制:虽然
llvm-cov show支持生成 HTML 报告,但其生成的 HTML 功能可能不如genhtml丰富,例如缺少图形界面的复杂交互。 - 可定制性有限:与
genhtml相比,llvm-cov show在生成报告的样式和布局上的可定制选项较少。
先转换为 .info 文件再使用 genhtml 生成 HTML 报告
这种方法涉及将覆盖率数据从 .profdata 转换为 lcov 的 .info 格式,然后使用 genhtml 生成 HTML 报告。
优点:
- 高度可定制:
genhtml提供了多种定制选项,可以调整生成的 HTML 报告的外观和功能,例如颜色、布局等。 - 广泛使用:
lcov和genhtml在许多项目中被广泛使用,因此有大量的社区支持和文档资料。 - 图形界面丰富:生成的 HTML 报告具有高度交互性,易于浏览和理解,特别是在处理大型项目时。
缺点:
- 步骤繁琐:需要额外的步骤将数据转换为
.info格式,相比直接使用llvm-cov show更复杂。 - 可能存在 兼容性 问题:在数据转换过程中可能出现格式兼容性问题,尤其是当 LLVM 版本更新时。
选择哪种方法取决于你的具体需求。如果你需要快速生成简单的报告或者需要与 LLVM 工具链紧密集成,直接使用 llvm-cov show 可能更合适。如果你需要生成更具交互性和可定制性的复杂 HTML 报告,且不介意额外的转换步骤,那么使用 genhtml 可能是更好的选择。
遇到的问题
合并Mach-O文件失败:
1: 符号冲突
- 重命名函数:最简单的解决方法是在不同的 .m 文件中使用不同的函数名称。这样可以确保每个函数名在全局命名空间中是唯一的。
- 使用静态函数:如果这个函数只在定义它的文件中使用,可以将函数定义为 static。这样,函数的作用域将限制在其定义的文件中,不会与其他文件中的同名函数冲突。 这样定义后,matrix_availableMemory_c 函数只在定义它的 .m 文件中可见。
- 使用内联函数:如果函数体较小,也可以考虑使用 inline 关键字,这样可以提示编译器在每个调用点直接展开这个函数,减少函数调用的开销,同时避免链接时的符号冲突。
2.同一个.o生成了对应的 "-hash值.o"文件
PROJECT_TEMP_ROOT里的带-hash值.o的文件是怎么生成的?
- 确保唯一性:在大型项目中,特别是在并行编译环境下,使用哈希值可以确保每个编译的输出文件名是唯一的,从而避免文件名冲突。
- 增量编译:哈希值可以反映源文件或其依赖的某些方面的状态。如果源文件或依赖未更改,编译系统可以跳过重新编译这些文件,直接使用已有的 .o 文件,从而加速构建过程。
- 缓存管理:在某些构建系统(如使用 ccache 或其他编译缓存工具的系统)中,哈希值用于管理和索引缓存的编译结果,以便在未来的编译中重用。
最终步骤生成不了覆盖率 执行
touch ~/.lcovrc
open -a TextEdit ~/.lcovrc
derive_function_end_line = 0 // 添加这一行
lvvm-coverage runtime-config
解释: 在一些复杂的编程语言(如 C/C++)或者包含宏、内联函数等特性的代码中,自动推导函数结束行可能会出现错误。将这个选项设置为0,可以让工具采用更保守或者用户指定的方式来确定函数结束行,从而避免错误地统计函数覆盖范围。
参考
llvm-profdata - Profile data tool: 用于处理生成profdata命令
llvm-cov - emit coverage information 覆盖率指令参数
Source-based Code Coverage :llvm官网基于源码对Swift和OC进行代码覆盖率