当使用NSString的类别方法时,ARC`BAD_ACCESS`

时间:2012-07-20 09:09:10

标签: iphone objective-c ios cocoa-touch exc-bad-access

我这样称之为我的实用方法:

NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"dd.MM.yy HH:mm"];
NSString *dateString = [dateFormat stringFromDate:[NSDate date]];

return [[Environment sharedInstance].versionLabelFormat replaceTokensWithStrings:
     @"VERSION", APP_VERSION, 
     @"BUILD", APP_BULD_NUMBER, 
     @"DATETIME" , dateString, 
     nil ];

这是NSString类别方法

-(NSString *)replaceTokensWithStrings:(NSString *)firstKey, ... NS_REQUIRES_NIL_TERMINATION{

    NSString *result = self;

        va_list _arguments;
        va_start(_arguments, firstKey);

        for (NSString *key = firstKey; key != nil; key = va_arg(_arguments, NSString*)) {

            // The value has to be copied to prevent crashes
            NSString *value = [(NSString *)(va_arg(_arguments, NSString*))copy];

            if(!value){
                // Every key has to have a value pair otherwise the replacement is invalid and nil is returned

                NSLog(@"Premature occurence of nil. Each token must be accompanied by a value: %@", result);
                return nil;
            }

            result = [result replaceToken:key withString:value];
        }
        va_end(_arguments);

    // Check if there are any tokens which were not yet replaced (for example if one value was nil)

    if([result rangeOfString:@"{"].location == NSNotFound){
        return result;
    } else {
        NSLog(@"Failed to replace tokens failed string still contains tokens: %@", result);
        return nil;
    }
}

在以下行中没有我必须添加copy语句,否则会有dateString的Zombie:

NSString *value = [(NSString *)(va_arg(_arguments, NSString*))copy];

更具体地说, Zombie Report 告诉我:

 1 Malloc       NSDateFormatter stringForObjectValue:
   Autorelease  NSDateFormatter stringForObjectValue:
 2 CFRetain     MyClass versionString:
 3 CFRetain     replaceToken:withString:
 2 CFRelease    replaceToken:withString:
 1 CFRelease    replaceTokensWithStrings:   ( One release too much!)
 0 CFRelease    MyClass versionString:
-1 Zombie       GSEventRunModal

尽管copy语句似乎解决了这个问题,但我想了解哪些不符合ARC的代码,以便BAD_ACCESS在没有值copy的情况下发生字符串。

2 个答案:

答案 0 :(得分:5)

正如其他人所说,问题在于您从变量参数列表中检索可能与ARC不兼容的对象的方式。

va_arg有一个很有趣的方法,可以返回ARC可能不知道的特定类型的值。我不确定这是否是clang中的错误,或者它是否是ARC的预期行为。我将澄清这个问题并相应地更新帖子。

作为一种解决方法,只需在参数处理中使用void指针避免问题,并以ARC安全的方式将它们正确转换为对象:

for (NSString *key = firstKey; key != nil; key = (__bridge NSString *)va_arg(_arguments, void *)) {
    NSString *value = (__bridge NSString *)va_arg(_arguments, void *);
    NSAssert(value != NULL, @"Premature occurence of nil.");
    result = [result stringByReplacingToken:key
                                 withString:value];
}

编辑: __bridge演员告诉ARC不要对所有权做些什么。它只是期望对象存活,不转移或放弃所有权。然而,keyvalue变量在使用时保持对对象的强引用。

第二次编辑:似乎clang / ARC应该知道va_arg中的类型,并警告或只做正确的事情(see this, for example)。

我试图重现你的问题而没有成功。一切都适合我:

$ clang --version
> Apple clang version 4.0 (tags/Apple/clang-421.10.48) (based on LLVM 3.1svn)

你使用哪个Xcode版本?

答案 1 :(得分:2)

这是铿锵的错误。

它应该可以工作

ARC 兼容和可变参数列表。如果不是,你会从编译器中得到错误。

变量value是一个强引用,而va_arg(_arguments, NSString *)的结果是一个不安全的未提交引用:您可以编写va_arg(_arguments, __unsafed_unretained NSString *)并获得完全相同的已编译程序集但尝试使用其他所有权 - 限定符将为您提供编译器错误,因为它不受支持。

因此,当将值存储在value中并假设实际使用该变量时,编译器应发出对objc_retain的调用,并在变量调用objc_release时进行平衡被毁坏了。根据该报告,第二个呼叫被发出,而第一个呼叫被丢失。

这会导致stringWithDate:返回的字符串崩溃(只有这一个),因为它是唯一不常量的字符串。所有其他参数都是由编译器生成的常量字符串,它只是忽略任何内存管理方法,因为只要加载了可执行文件,它们就会持久存储在内存中。

因此,我们需要理解为什么编译器在没有相应保留的情况下发出一个版本。由于您不执行任何手动内存管理,并且不通过使用__bridge_transfer__bridge_retained进行转换来欺骗所有权规则,因此我们可以假设问题来自编译器。

未定义的行为不是

的原因

有两个原因可能导致编译器发出无效的程序集。您的代码中包含未定义的行为,或者编译器中存在错误。

当您的代码尝试执行未由C标准定义的内容时,会发生

Undefined behavior:当编译器遇到未定义的行为时,它有权执行它想要的任何内容。未定义的行为会导致可能会或可能不会崩溃的程序。大多数情况下,问题出现在与未定义行为相同的位置,但有时可能看起来无关,因为编译器期望未定义的行为不会发生,并依赖于期望来执行某些优化。

因此,让我们看看您的代码是否包含未定义的行为。是的,因为方法replaceTokensWithStrings:可以调用va_start并在va_end循环内调用return nil时返回for。 C标准明确指出(在第7.15.1.3节中)这样做是未定义的行为。

如果我们将return nil替换为break,您的代码现在有效。然而,这并没有解决问题。

责备编译器

现在我们已经消除了所有其他可能的原因,我们需要面对现实。 clang中有一个bug。我们可以通过执行许多产生有效编译汇编的细微更改来看到这一点:

  • 如果我们使用-O0代替-Os进行编译,则可以正常使用。
  • 如果我们使用clang 4.1进行编译,则可以正常工作。
  • 如果我们在if条件之前向对象发送消息,则可以正常工作。
  • 如果我们将va_arg(_arguments, NSString *)替换为va_arg(_arguments, id),则可行。我建议你使用这个解决方法。