Highlight
Xcode 13 的静态分析器可以在不运行 App 的情况下扫描 Objective-C、C 和 C++ 代码,提前发现空返回值、断言副作用、死循环、冗余条件和 C++ move/forward 使用错误。
核心内容
一个混合 Swift 和 Objective-C 的 App,常见问题并不都能靠编译器挡住。
演讲里的示例是一个太阳系浏览 App。项目里有一个 Objective-C 方法 positionAtDate,方法声明返回 non-null(非空)值。Swift 代码会调用它。
问题在于,某条 switch 的默认分支没有初始化 position。方法最后返回了 nil。编译可以通过,测试也可能没有覆盖到这条路径。等 Swift 运行时拿到这个 nil,行为就不可预期。
以前要找到这种 bug,开发者通常要等崩溃报告、加日志、复现用户路径,再回到代码里猜哪条分支漏了处理。路径越少见,排查越慢。
Xcode 的 Static Analyzer(静态分析器)把这一步提前到开发阶段。它不运行 App,只分析源代码。它会沿着控制流和数据流推导变量状态,找出少见路径里的逻辑错误。
Xcode 13 继续扩展了检查范围。它可以发现断言里的副作用、死循环、冗余分支条件,以及 C++ 中 move 和 forward 的使用错误。
从“发现问题”到“解释路径”
静态分析器的价值不只是报一个 warning。
在示例项目里,Xcode 的 issue navigator(问题导航器)会展开一串事件。最后一个事件显示 position 是 nil。往上看,regularPositionAtDate 没有被调用。继续往上看,接收对象从默认分支开始就是 nil。
开发者得到的是一条可读的路径:哪个分支进入、哪个对象保持 nil、哪个调用被跳过、最后为什么返回了 nil。
修复也直接:让 default 分支和 spherical(球形)分支使用相同逻辑,然后重新运行 Analyze。
把检查放进日常开发
静态分析可以手动运行,也可以并入构建。
手动方式是 Product > Analyze,快捷键是 Command-Shift-B。Xcode 会分析当前 active scheme(当前激活方案)里所有 target 的源文件。
如果希望每次构建都检查,可以在 Build Settings(构建设置)里打开 Analyze During ‘Build’。Xcode 会像增量构建一样,只分析修改过的文件。
项目很大时,还可以改成 shallow mode(浅分析模式)。它更快,但会避开跨多个函数的问题。安全敏感代码则可以单独打开 security issues(安全问题)相关检查。
详细内容
运行静态分析器(01:21)
Apple 提供的入口在 Xcode 菜单里。打开项目后,选择 Product > Analyze,或按 Command-Shift-B。
Xcode
└── Product
└── Analyze Command-Shift-B
关键点:
Product是 Xcode 的构建相关菜单。Analyze会启动静态分析器。Command-Shift-B是同一个动作的快捷键。- 分析范围是当前 active scheme 里所有 target 的源文件。
这一步类似 build(构建),但目标不同。构建检查代码能否编译,静态分析器检查代码路径里是否存在潜在 bug。
读取分析报告的事件链(02:41)
分析器发现问题后,可以在 issue navigator 展开报告。报告会列出导致 bug 的事件顺序,并在编辑器里用箭头标出这些事件。
演讲里的核心路径可以整理成下面的最小 Objective-C 示例。它对应逐字稿里的事实:positionAtDate 声明返回 non-null,但默认分支让 position 保持 nil。
typedef NS_ENUM(NSInteger, BodyShape) {
BodyShapeSpherical,
BodyShapeIrregular,
BodyShapeUnknown,
};
- (nonnull Position *)positionAtDate:(NSDate *)date {
Position *position = nil;
switch (self.shape) {
case BodyShapeSpherical:
position = [self regularPositionAtDate:date];
break;
case BodyShapeIrregular:
position = [self irregularPositionAtDate:date];
break;
default:
break;
}
return position;
}
关键点:
BodyShapeUnknown表示没有被前两个case覆盖的形状。position初始值是nil。BodyShapeSpherical分支会调用regularPositionAtDate:初始化position。BodyShapeIrregular分支会调用另一个方法初始化position。default分支直接break,position仍然是nil。- 方法返回类型标注为
nonnull,返回nil会破坏这个约定。
修复方式是让默认分支也初始化 position。演讲里采用的修复是让 default case 和 spherical case 一样。
- (nonnull Position *)positionAtDate:(NSDate *)date {
Position *position = nil;
switch (self.shape) {
case BodyShapeSpherical:
default:
position = [self regularPositionAtDate:date];
break;
case BodyShapeIrregular:
position = [self irregularPositionAtDate:date];
break;
}
return position;
}
关键点:
default和BodyShapeSpherical共用同一段逻辑。- 进入默认分支时,
regularPositionAtDate:会被调用。 position不再沿着默认分支保持nil。- 修复后重新运行 Analyze,用同一个工具确认问题消失。
检查断言里的副作用(04:49)
Xcode 13 新增了断言副作用检查。逐字稿里的例子是遍历天体数组,统计有卫星的对象数量,再用 NSAssert 验证数量不超过行星数量。
错误写法是把计数更新放进断言表达式。
NSUInteger objectsWithMoons = 0;
for (AstronomicalObject *object in objects) {
if (object.moonCount > 0) {
NSAssert(++objectsWithMoons <= planetCount,
@"Objects with moons must not exceed planets");
}
}
关键点:
objectsWithMoons记录有卫星的对象数量。for循环遍历天体数组。object.moonCount > 0表示当前对象有卫星。++objectsWithMoons会修改计数器,这就是副作用。- 这个副作用发生在
NSAssert表达式里。 - release build(发布构建)里断言可能被禁用,计数器更新可能丢失。
修复方式是把副作用移到断言外面。
NSUInteger objectsWithMoons = 0;
for (AstronomicalObject *object in objects) {
if (object.moonCount > 0) {
objectsWithMoons += 1;
NSAssert(objectsWithMoons <= planetCount,
@"Objects with moons must not exceed planets");
}
}
关键点:
objectsWithMoons += 1独立执行,不依赖断言是否启用。NSAssert只检查条件。- debug build(调试构建)和 release build 对计数器的更新保持一致。
- 同类检查也适用于 C 和 C++ 的
assert。
检查死循环(06:02)
Xcode 13 还可以发现死循环。逐字稿里的例子是填充二维网格时,内层循环错误地增加了 value,没有增加真正的循环计数器 column。
int value = 0;
for (int row = 0; row < rowCount; row++) {
for (int column = 0; column < columnCount; value++) {
grid[row][column] = value;
}
}
关键点:
- 外层循环用
row遍历行。 - 内层循环条件依赖
column < columnCount。 - 内层循环体写入
grid[row][column]。 - 更新表达式却是
value++。 column没有变化,循环条件可能一直成立。- 分析器会报告这类错误,并解释是哪一个变量没有按预期更新。
修复方式是递增 column。
int value = 0;
for (int row = 0; row < rowCount; row++) {
for (int column = 0; column < columnCount; column++) {
grid[row][column] = value;
value++;
}
}
关键点:
column++让内层循环向终止条件前进。grid[row][column]仍然使用当前行列写入。value++移到循环体里,用来生成下一个写入值。- 控制变量和业务变量分开,错误更容易被人和工具发现。
在构建中自动运行(06:47)
Xcode 允许把分析器接入构建流程。进入 Build Settings,用 analysis 搜索相关设置,打开 Analyze During ‘Build’。
Build Settings
└── Search: analysis
└── Analyze During 'Build' = Yes
关键点:
Build Settings是项目构建设置入口。- 搜索
analysis可以只看静态分析相关选项。 Analyze During 'Build'打开后,每次构建会触发分析。- 分析过程类似增量构建,只处理修改过的文件。
- 对构建时间敏感的项目,可以选择 shallow mode。
如果只想检查一个文件,可以从 Product > Perform Action 菜单选择单文件分析。
Xcode
└── Product
└── Perform Action
└── Analyze <Current File>
关键点:
- 单文件分析适合刚改完某个实现文件时快速检查。
- 修改 header file(头文件)时尤其有用。
- 它可以避免重新分析所有 import 这个头文件的文件。
核心启发
-
做什么:给 Objective-C 与 Swift 混编边界做 non-null 返回值巡检。 为什么值得做:演讲里的 bug 就发生在 Objective-C
nonnull方法返回nil,Swift 调用后可能出现运行时异常。 怎么开始:从 Swift 会调用的 Objective-C API 入手,运行 Product > Analyze,优先处理返回值为空、接收对象为空的报告。 -
做什么:把断言清理成“只检查,不修改状态”。 为什么值得做:Xcode 13 可以发现
NSAssert、Cassert、C++assert中的副作用,release build 里这些副作用可能消失。 怎么开始:搜索NSAssert和assert,把++、赋值、写内存等操作移到断言前一行。 -
做什么:为复杂循环增加一次静态分析检查。 为什么值得做:二维网格、分页遍历、状态机循环常把控制变量和业务变量混在一起,Xcode 13 可以发现循环变量不前进导致的死循环。 怎么开始:改动循环逻辑后,对当前文件运行 Product > Perform Action 的单文件分析,先处理 infinite loop 报告。
-
做什么:在核心模块构建时自动运行 Analyze。 为什么值得做:静态分析不需要测试覆盖少见路径,适合尽早发现安全问题、逻辑问题和 API 误用。 怎么开始:在 Build Settings 搜索
analysis,打开 Analyze During ‘Build’,再按模块决定使用 deep mode 还是 shallow mode。 -
做什么:给安全敏感代码打开专门的安全检查。 为什么值得做:演讲明确提到分析器可以检查 security issues,适合处理登录、支付、本地文件和网络输入等代码。 怎么开始:在 Build Settings 里选择性启用安全相关 analyzer checks,对噪音较大的检查单独关闭。
关联 Session
- Detect and diagnose memory issues — 使用 Xcode、XCTest 和 MetricKit 定位内存增长与回归。
- Triage TestFlight crashes in Xcode Organizer — 把已经发生在 TestFlight 用户侧的崩溃快速聚类和定位。
- Symbolication: Beyond the basics — 理解调试工具如何把运行时地址还原到函数与源码位置。
评论
GitHub Issues · utterances