iOS 检测无用代码的核心原理是将「项目中定义的所有代码集合」与「实际被引用的代码集合」进行差集运算,其主流实现方案主要分为静态结构分析(Mach-O文件解析)、源码AST语法树分析以及动态运行期检测三大类。 [1]
一、 静态结构分析原理(基于 Mach-O 文件)
这是 Objective-C 项目最常用的方案。该方法不读取源码,而是直接解析编译产物 Mach-O 二进制文件中的 Data 段信息。 [2, 3]
1. 无用类检测
编译器在编译 Objective-C 时,会将类信息和引用信息注册到 Mach-O 的 __DATA 段中: [3]
- 获取所有类:通过解析
__DATA,__objc_classlist获取项目里注册的所有类列表。 - 获取已引用类:通过解析
__DATA,__objc_classrefs获取程序中实际被引用的类列表。 - 求差集:
__objc_classlist减去__objc_classrefs,剩下的就是未被引用的类。 [1, 3]
2. 无用方法检测
- 获取所有方法:解析
__DATA,__objc_selrefs能够获取所有被引用的方法 Selector。 - 获取实现方法:解析
__DATA,__objc_classlist内部各个类的method_list获取所有实现的方法。 - 求差集:如果某个类实现的方法没有出现在任何
__objc_selrefs中,则该方法大概率未被使用。
二、 源码 AST 语法树分析(基于 编译器/工具链)
由于 Swift 是静态强类型语言,具备动态特性的 Mach-O 解析法对 Swift 的方法级检测效果有限。因此,Swift 项目通常借助编译器前端技术。
1. Swift 抽象语法树(AST)分析
像开源工具 Periphery 主要通过调用 SourceKit 接口来建立索引: [4, 5]
- 解析 AST:将源码解析为抽象语法树,提取出所有的类、结构体、属性、方法定义。
- 建立图关系:分析代码间的依赖与调用关系,构建出逻辑显式调用图(Graph)。
- 根节点遍历:从主入口(如
@main、AppDelegate)开始向下遍历,未被遍历到的叶子节点代码即为无用代码。
2. 静态规则扫描
利用 SwiftLint 等工具,通过正则或 AST 匹配一些局部无用代码,例如未使用的局部变量、未使用的私有属性(private 且无内部读取)等。 [6]
三、 动态运行期检测
静态分析无法彻底解决动态调用(如 NSClassFromString、performSelector:)导致的误报。动态检测作为补充,主要在 App 运行时收集真实的数据。 [7]
1. 运行期 ObjC 注册类遍历
在真实用户环境或自动化测试中,通过 Objective-C Runtime 机制:
- 周期性调用
objc_copyClassList获取所有类。 - 调用类底层的
isInitialized状态位(或通过 hook+initialize/+load方法)来判断该类在当前生命周期内是否被加载过。 [8]
2. 线上代码覆盖率(Code Coverage)
基于 LLVM 编译器提供的插桩技术(Clang Profile Coverage): [9]
- 编译期插桩:开启特定的编译参数(如
-fprofile-instr-generate),编译器在每个函数入口插入计数器。 - 运行期导出:App 运行过程中记录每个代码块的执行次数,并导出
profraw文件。 - 分析覆盖率:通过自动化测试跑完所有业务用例后,执行次数为 0 的函数和分支即为冷代码/潜在无用代码。 [9]
⚠️ 检测的已知局限性(误报场景)
无论哪种原理,都存在以下无法完美识别的硬伤,清理时必须人工二次确认: [7]
- 动态反射:通过字符串拼接动态生成的类名或方法名(如
NSClassFromString(@"MyView" + type))。 - 仅通过
+load触发的业务:某些类没有被任何地方显式引用,但实现了+load方法,在 runtime 统一加载时会自动触发逻辑。 - 协议/多态方法:子类实现了某个协议方法,但外部始终通过基类或协议调用,静态分析有时难以精准归属到具体子类。
- 单元测试引用:部分类和方法仅用于 Unit Test,在生产包的常规扫描中会被误判为无用。 [7, 8]