iOS 如何检测项目无用的代码

2 阅读4分钟

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)。
  • 根节点遍历:从主入口(如 @mainAppDelegate)开始向下遍历,未被遍历到的叶子节点代码即为无用代码。

2. 静态规则扫描

利用 SwiftLint 等工具,通过正则或 AST 匹配一些局部无用代码,例如未使用的局部变量、未使用的私有属性(private 且无内部读取)等。 [6]

三、 动态运行期检测

静态分析无法彻底解决动态调用(如 NSClassFromStringperformSelector:)导致的误报。动态检测作为补充,主要在 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]