2.7. 常见误报的常见问题解答和处理方法

2.7.1. 自定义断言

问:如何告诉分析器我不想在此处报告错误,因为我的自定义错误处理程序将在错误到达之前安全地结束执行?

您可以通过向分析器讲解您的 自定义断言处理程序 来告诉它该路径是不可到达的。例如,您可以按如下方式修改代码段

void customAssert() __attribute__((analyzer_noreturn));
int foo(int *b) {
  if (!b)
    customAssert();
  return *b;
}

2.7.2. 空指针解引用

问:分析器报告空指针解引用,但我确信指针永远不会为空。如何告诉分析器指针永远不会为空?

分析器通常认为指针可能为空的原因是,前面的代码检查了它与空的比较。如果您绝对确定它不会为空,请删除前面的检查,最好也添加一个断言。例如

void usePointer(int *b);
int foo(int *b) {
  usePointer(b);
  return *b;
}

2.7.3. 死存储

问:如何告诉静态分析器我不关心某个特定的死存储?

当分析器看到存储到变量中的值从未使用过时,它将生成类似于以下内容的消息

Value stored to 'x' is never read

您可以使用 (void)x; 惯用法来确认代码中存在死存储,但您不希望它在将来被报告。

2.7.4. 未使用实例变量

问:如何告诉静态分析器我不关心 Objective-C 中某个特定的未使用实例变量?

当分析器看到存储到变量中的值从未使用过时,它将生成类似于以下内容的消息

Instance variable 'commonName' in class 'HappyBird' is never used by the methods in its @implementation

您可以将 __attribute__((unused)) 添加到实例变量声明中以抑制警告。

2.7.5. 未本地化的字符串

问:如何告诉静态分析器我不关心某个特定的未本地化字符串?

当分析器看到未本地化的字符串被传递给将向用户呈现该字符串的方法时,它将生成类似于以下内容的消息

User-facing text should use localized string macro

如果您的项目故意使用未本地化的面向用户的字符串(例如,在从未向用户显示的调试 UI 中),您可以通过一个函数来抑制分析器警告(并记录您的意图),该函数只是返回其输入,但被注释为返回本地化的字符串

__attribute__((annotate("returns_localized_nsstring")))
static inline NSString *LocalizationNotNeeded(NSString *s) {
  return s;
}

然后,您可以在创建调试 UI 时调用此函数

[field setStringValue:LocalizationNotNeeded(@"Debug")];

某些项目也可能会发现使用 NSLocalizedString 但在字符串内容中添加 “DNL” 或 “Do Not Localize” 作为约定很有用

UILabel *testLabel = [[UILabel alloc] init];
NSString *s = NSLocalizedString(@"Hello <Do Not Localize>", @"For debug purposes");
[testLabel setText:s];

2.7.6. 手动保留/释放中的释放

问:如何告诉分析器我的实例变量不需要在手动保留/释放下的 -dealloc 中释放?

如果您的类只在生命周期的部分时间内使用实例变量,它可能会维护一个不变式,保证实例变量始终在 -dealloc 之前被释放。在这种情况下,您可以通过在 -dealloc 中添加 assert(_ivar == nil) 或显式释放 [_ivar release](当变量为 nil 时将为无操作)来消除有关缺少释放的警告。

2.7.7. 决定可空性

问:如何决定方法的返回类型应该是 _Nullable 还是 _Nonnull?

根据方法的实现,这会让您处于五种情况之一

  1. 您实际上从未返回 nil。

  2. 您有时会返回 nil,并且调用者应该处理它。这包括您的方法在文档中说明在给定特定输入的情况下返回 nil 的情况。

  3. 您根据某些外部条件(例如内存不足错误)返回 nil,但客户端也对此无能为力。

  4. 您只有在调用者传递的输入在文档中说明为无效时才会返回 nil。这意味着这是客户端的错误。

  5. 您在某些完全未记录的情况下返回 nil。

在 (1) 中,您应该将方法注释为返回 _Nonnull 对象。

在 (2) 中,方法应标记为 _Nullable

在 (3) 中,您应该将方法注释为 _Nonnull。为什么?因为鉴于调用者对此无能为力且不知道发生了什么错误,所以没有调用者会真正检查 nil。此时,事情已经变得如此糟糕,基本上无法恢复。

最不令人满意的案例是 (4),因为生成的程序几乎肯定会崩溃或只是默默地执行错误的操作。如果这是一个新方法或您控制调用者,您可以使用 NSParameterAssert()(或等效项)来检查先决条件并删除 nil 返回。但是,如果您不控制调用者并且他们依赖于此行为,您应该将方法标记为 _Nonnull 并返回强制转换为 _Nonnull 的 nil。

如果您处于 (5),请记录它,然后确定您现在是处于 (2)、(3) 还是 (4)。

2.7.8. 故意违反可空性

问:如何告诉分析器我故意违反了可空性?

在某些情况下,方法故意违反可空性可能是有意义的。例如,您的方法可能 - 出于向后兼容性的原因 - 选择在返回类型为非空的非空方法中返回 nil 并记录错误消息,当客户端违反了已记录的先决条件时,而不是使用 NSAssert() 检查先决条件。在这些情况下,您可以使用强制转换来抑制分析器警告

return (id _Nonnull)nil;

请注意,此强制转换不会影响代码生成。

2.7.9. 确保循环体执行

问:分析器假设循环体永远不会进入。如何告诉它循环体至少会进入一次?

如果您知道循环至少会进入一次,可以使用断言来通知分析器。例如

int foo(int length) {
  int x = 0;
  assert(length > 0);
  for (int i = 0; i < length; i++)
    x += 1;
  return length/x;
}

通过在函数开头添加 assert(length > 0),您可以告诉分析器您的代码从未期望为零或负值,因此它不需要测试这些路径的正确性。

2.7.10. 抑制特定警告

问:如何抑制特定的分析器警告?

当您遇到分析器错误/误报时,请检查它是否是上面讨论的问题之一,或者分析器 注释 是否可以通过帮助静态分析器更好地理解代码来解决问题。其次,请 报告它 以帮助我们改善用户体验。

有时,真的没有 “好” 的方法来消除问题。在这种情况下,您可以通过使用 Clang 属性 ‘suppress’ 来注释有问题的代码行来直接 “静默” 它

int foo() {
  int *x = nullptr;
  ...
  [[clang::suppress]] {
    // all warnings in this scope are suppressed
    int y = *x;
  }

  // null pointer dereference warning suppressed on the next line
  [[clang::suppress]]
  return *x
}

int bar(bool coin_flip) {
  // suppress all memory leak warnings about this allocation
  [[clang::suppress]]
  int *result = (int *)malloc(sizeof(int));

  if (coin_flip)
    return 0;      // including this leak path

  return *result;  // as well as this leak path
}

2.7.11. 从分析中排除代码

问:如何有选择地排除分析器检查的代码?

当静态分析器使用 clang 解析源文件时,它会隐式定义预处理器宏 __clang_analyzer__。可以使用此宏有选择地排除分析器检查的代码。这是一个例子

#ifndef __clang_analyzer__
// Code not to be analyzed
#endif

不建议使用这种用法,因为它会使代码从此以后对分析器失效。相反,我们希望用户在分析器标记误报时向分析器提交错误。