简体   繁体   English

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

[英]ARC `BAD_ACCESS` when using category method of NSString

I call a utility method of mine like so: 我这样称之为我的实用方法:

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 ];

This is the NSString category method 这是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;
    }
}

No on the following line I had to add a copy statement otherwise there would be a Zombie with the dateString : 在以下行中没有我必须添加一个copy语句,否则会有一个带有dateString的Zombie:

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

To be more specific the Zombie Report told me this: 更具体地说, 僵尸报告告诉我:

 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

Although the copy statement seems to fix the problem I would like to understand what is not ARC-complient with the code so that the BAD_ACCESS would occur without the copy for the value string. 虽然copy语句似乎解决了这个问题,但我想了解什么不是ARC兼容代码,这样BAD_ACCESS就会在没有值字符串的copy情况下发生。

As others have stated the problem lies in the way you retrieve objects from the variable argument list which might not be compatible with ARC. 正如其他人所说,问题在于从可变参数列表中检索可能与ARC不兼容的对象的方式。

va_arg has a funny way of how to return a value of a specific type which ARC is probably not aware of. va_arg有一种有趣的方法,可以返回ARC可能不知道的特定类型的值。 I'm not sure if this is a bug in clang or if it is the intended behavior for ARC. 我不确定这是否是clang中的错误,或者它是否是ARC的预期行为。 I'll clarify this issue and will update the post accordingly. 我将澄清这个问题并相应地更新帖子。

As a workaround just avoid the problem by using void pointers in the argument handling and convert them to objects properly in an ARC safe way: 作为一种解决方法,只需通过在参数处理中使用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];
}

Edit: The __bridge cast tells ARC to not do something about ownership. 编辑: __bridge演员告诉ARC不要对所有权做些什么。 It just expects the object to be alive and does not transfer or give up ownership. 它只是期望对象存活,不转移或放弃所有权。 Nevertheless the key and value variables maintain strong references to the objects while in use. 然而, keyvalue变量在使用时保持对对象的强引用。

Second Edit: It seems that clang/ARC should be aware of the type in va_arg and either warn or just do the right thing ( see this, for example ). 第二次编辑:似乎clang / ARC应该知道va_arg中的类型,并警告或只做正确的事情( 例如,请参阅此内容 )。

I tried to reproduce your problem without success. 我试图重现你的问题没有成功。 Everything works for me on: 一切都适合我:

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

Which Xcode version do you use? 你使用哪个Xcode版本?

It's a bug in clang. 这是铿锵的错误。

It should work 它应该工作

ARC is compatible with variable argument lists. ARC与可变参数列表兼容 If it wasn't you would get an error from the compiler. 如果不是,您将从编译器中收到错误。

The variable value is a strong reference whereas the result of va_arg(_arguments, NSString *) is an unsafe unretained reference: you may write va_arg(_arguments, __unsafed_unretained NSString *) and get the exact same compiled assembly but trying with another ownership-qualifier will get you a compiler error as it's not supported. 变量value是一个强引用,而va_arg(_arguments, NSString *)是一个不安全的未返回引用:您可以编写va_arg(_arguments, __unsafed_unretained NSString *)并获得完全相同的编译程序集但尝试使用另一个所有权限定符将因为它不受支持而得到编译器错误。

So, when storing the value in value and assuming the variable is actually used, the compiler should emit a call to objc_retain and balance it with a call to objc_release when the variable is destructed. 所以,存储在所述值时value并假设该变量被实际使用时,编译器应该发出一个呼叫到objc_retain和与呼叫平衡它objc_release当变量被破坏。 According to the report, the second call is emitted whereas the first one is missing. 根据该报告,第二个呼叫被发出,而第一个呼叫丢失。

This lead to a crash with the string returned by stringWithDate: (and only this one) because it's the only string that isn't constant. 这会导致stringWithDate:返回的字符串崩溃stringWithDate:只有这一个),因为它是唯一不是常量的字符串。 All the other parameters are constant strings generated by the compiler, which simply ignore any memory management method, because they persist in memory as long as the executable is loaded. 所有其他参数都是由编译器生成的常量字符串,它只是忽略任何内存管理方法,因为只要加载了可执行文件,它们就会持久存储在内存中。

So, we need to understand why the compiler emit a release without the corresponding retain. 因此,我们需要理解为什么编译器发出没有相应保留的版本。 As you don't perform any manual memory management and don't trick the ownership rules by casting with __bridge_transfer or __bridge_retained , we can assume that the issue comes from the compiler. 由于您不执行任何手动内存管理并且不通过使用__bridge_transfer__bridge_retained进行转换来欺骗所有权规则,因此我们可以假设问题来自编译器。

Undefined behavior isn't the reason 未定义的行为不是原因

There are two reasons that may cause the compiler to emit invalid assembly. 有两个原因可能导致编译器发出无效的程序集。 Either you code contains an undefined behavior either there is a bug in the compiler. 您的代码包含未定义的行为,或者编译器中存在错误。

Undefined behavior occurs when your code try to perform something that is not defined by the C Standard: when the compiler meets an undefined behavior, it is entitled to do whatever it wants . 未定义行为当编译器遇到一个未定义的行为,它有权为所欲为 :当你的代码试图执行不是由C标准定义的东西出现。 Undefined behaviors result to programs that may or may not crash. 未定义的行为会导致可能会或可能不会崩溃的程序。 Most of the time the issue occurs at the same place as the undefined behavior but sometimes it may seem unrelated because the compilers expect undefined behaviors not to occur and rely on that expectation to perform some optimizations. 大多数情况下,问题出现在与未定义行为相同的位置,但有时可能看起来不相关,因为编译器期望未定义的行为不会发生,并依赖于期望来执行某些优化。

So let's see if your code contains an undefined behavior. 因此,让我们看看您的代码是否包含未定义的行为。 Yes it does, as the method replaceTokensWithStrings: may call va_start and return without va_end being called ( return nil inside the for loop). 是的,因为方法replaceTokensWithStrings:可以调用va_start并返回而不调用va_end (在for循环中return nil )。 The C Standard explicitly states (in section 7.15.1.3) that doing so is undefined behavior. C标准明确指出(在第7.15.1.3节中)这样做是未定义的行为。

If we replace return nil with break , your code is now valid. 如果我们用break替换return nil ,那么你的代码现在是有效的。 However that doesn't solve the problem. 然而,这并没有解决问题。

Blame the compiler 责备编译器

Now that we eliminated all the other possible reasons, we need to face the reality. 现在我们已经消除了所有其他可能的原因,我们需要面对现实。 There is a bug in clang. clang中有一个bug。 We can see this by performing many subtle changes that produce valid compiled assembly: 我们可以通过执行许多产生有效编译汇编的细微更改来看到这一点:

  • If we compile with -O0 instead of -Os , it works. 如果我们使用-O0而不是-Os ,则可以正常工作。
  • If we compile with clang 4.1, it works. 如果我们用clang 4.1编译,它就可以了。
  • If we send a message to the object before the if condition, it works. 如果我们在if条件之前向对象发送消息,它就可以工作。
  • If we replace va_arg(_arguments, NSString *) with va_arg(_arguments, id) , it works. 如果我们用va_arg(_arguments, id)替换va_arg(_arguments, NSString *) va_arg(_arguments, id) ,它就可以了。 I suggest you use this workaround. 我建议你使用这个解决方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM