7

私は次のように私のユーティリティメソッドを呼び出します:

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ます。

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

より具体的には、ゾンビレポートは私にこれを教えてくれました:

 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のforがないと発生するようにします。copy

4

2 に答える 2

5

他の人が述べているように、問題は、ARCと互換性がない可能性のある変数引数リストからオブジェクトを取得する方法にあります。

va_argARCがおそらく認識していない特定のタイプの値を返す方法の面白い方法があります。これが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に指示します。オブジェクトが生きていることを期待するだけで、所有権を譲渡したり放棄したりすることはありません。それにもかかわらず、keyおよびvalue変数は、使用中にオブジェクトへの強力な参照を維持します。

2番目の編集: clang / ARCはva_argのタイプを認識し、警告するか、正しいことを実行する必要があるようです(たとえば、これを参照してください)。

私はあなたの問題を再現しようとしましたが成功しませんでした。すべてが私のために働きます:

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

どのXcodeバージョンを使用していますか?

于 2012-07-20T13:01:44.753 に答える
2

これはclangのバグです。

動作するはずです

ARCは可変引数リストと互換性があります。そうでない場合は、コンパイラからエラーが発生します。

変数valueは強力な参照ですが、の結果はva_arg(_arguments, NSString *)安全でない保持されていない参照です。va_arg(_arguments, __unsafed_unretained NSString *)まったく同じコンパイル済みアセンブリを記述して取得できますが、別の所有権修飾子を試してみると、サポートされていないためコンパイラエラーが発生します。

したがって、値をに格納valueし、変数が実際に使用されていると想定する場合、コンパイラーは、変数が破棄されたときに、objc_retainへの呼び出しを発行し、への呼び出しとバランスを取る必要があります。objc_releaseレポートによると、最初の呼び出しが欠落しているのに対し、2番目の呼び出しが発行されます。

stringWithDate:これは、定数ではない唯一の文字列であるため、によって返される文字列(およびこれのみ)でクラッシュを引き起こします。他のすべてのパラメーターは、コンパイラーによって生成された定数文字列であり、実行可能ファイルがロードされている限りメモリー内に保持されるため、メモリー管理メソッドを無視します。

したがって、コンパイラが対応する保持なしでリリースを発行する理由を理解する必要があります。__bridge_transfer手動でメモリ管理を実行したり、またはを使用して所有権ルールをだましたりしないため__bridge_retained、問題はコンパイラに起因すると考えられます。

未定義の動作は理由ではありません

コンパイラが無効なアセンブリを発行する原因となる可能性のある2つの理由があります。コードに未定義の動作が含まれているか、コンパイラにバグがあります。

未定義の動作は、コードがC標準で定義されていないことを実行しようとしたときに発生します。コンパイラが未定義の動作を満たしている場合、コンパイラは必要なことを実行する権利があります。未定義の動作は、クラッシュする場合としない場合があるプログラムになります。ほとんどの場合、問題は未定義の動作と同じ場所で発生しますが、コンパイラーは未定義の動作が発生しないことを期待し、その期待に依存していくつかの最適化を実行するため、無関係に見える場合があります。

それでは、コードに未定義の動作が含まれているかどうかを見てみましょう。はい、そうです。メソッドは(ループ内で)呼び出されずに呼び出されて戻るreplaceTokensWithStrings:可能性があるためです。C標準では、(セクション7.15.1.3で)そうすることは未定義の動作であると明示的に述べています。va_startva_endreturn nilfor

に置き換えるreturn nilbreak、コードは有効になります。しかし、それでは問題は解決しません。

コンパイラのせい

他の考えられる理由をすべて排除したので、現実に直面する必要があります。clangにバグがあります。これは、有効なコンパイル済みアセンブリを生成する多くの微妙な変更を実行することで確認できます。

  • -O0の代わりにでコンパイルすると-Os、機能します。
  • clang 4.1でコンパイルすると、動作します。
  • if条件の前にオブジェクトにメッセージを送信すると、機能します。
  • に置き換えるva_arg(_arguments, NSString *)va_arg(_arguments, id)動作します。この回避策を使用することをお勧めします。
于 2012-07-23T10:11:37.783 に答える