13

私はARCを使用していて、ループ内の文字列を変更するときに奇妙な動作が見られます。

私の状況では、NSXMLParserデリゲートコールバックを使用してループしていますが、デモプロジェクトといくつかのNSStringオブジェクトを変更するだけのサンプルコードを使用すると、まったく同じ動作と症状が見られます。

デモプロジェクトはGitHubからダウンロードできます。メインビューコントローラーのviewDidLoadメソッドで、4つのメソッド呼び出しの1つをコメント解除するだけで、さまざまな動作をテストできます。

簡単にするために、これは私が空のシングルビューアプリケーションに固執した単純なループです。viewDidLoadこのコードをメソッドに直接貼り付けました。ビューが表示される前に実行されるため、ループが終了するまで画面は黒くなります。

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    NSString *newText = [text stringByAppendingString:@" Hello"];

    if (text) {
        text = newText;
    }else{
        text = @"";
    }
}

次のコードも、ループが完了するまでメモリを消費し続けます。

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

割り当てツールを実行した状態で、これら2つのループがInstrumentsでどのようにループするかを次に示します。

文字列操作を繰り返す楽器プロファイリング

見る?大量のメモリ警告が表示されてからアプリが自然に停止するまで、徐々に安定したメモリ使用量。

次に、少し違うことを試してみました。私は次のようにのインスタンスを使用しましたNSMutableString

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

このコードのパフォーマンスははるかに優れているようですが、それでもクラッシュします。これは次のようになります。

代わりにプロファイルであるNSMutableStrings

次に、これを小さなデータセットで試し、どちらかのループがビルドアップを完了するのに十分な時間生き残ることができるかどうかを確認しました。NSStringバージョンは次のとおりです。

NSString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

同様にクラッシュし、結果のメモリグラフは、次のコードを使用して生成された最初のグラフのようになります。

NSStringが再びクラッシュする

を使用するNSMutableStringと、同じ百万回の反復ループが成功するだけでなく、はるかに短い時間で実行されます。コードは次のとおりです。

NSMutableString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

そして、メモリ使用量のグラフを見てください。

NSMutableStringsは、より小さなデータセットで機能するようです

最初の短いスパイクは、ループによって発生したメモリ使用量です。viewDidLoadで実行しているため、ループの処理中に画面が黒くなるという一見無関係な事実に気付いたときのことを覚えていますか?そのスパイクの直後に、ビューが表示されます。したがって、このシナリオではNSMutableStringsがメモリをより効率的に処理するだけでなく、はるかに高速であるように見えます。魅力的な。

ここで、実際のシナリオに戻ります... NSXMLParserAPI呼び出しの結果を解析するために使用しています。XML応答構造に一致するObjective-Cオブジェクトを作成しました。したがって、たとえば、次のようなXML応答について考えてみます。

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>

私のオブジェクトは次のようになります。

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

@end

これで、NSXMLParserデリゲートで、先に進んでXMLをループし、現在の要素を追跡します(データはかなりフラットなので、完全な階層表現は必要ありません。これは、 XMLとしてのMSSQLデータベース)そして、foundCharactersメソッドで、次のようなものを実行します。

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}

このコードは最初のコードによく似ています。を使用してXMLを効果的にループしているNSXMLParserので、すべてのメソッド呼び出しをログに記録すると、次のようになります。

parserDidStartDocument:parser:didStartElement:namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser:didStartElement:namespaceURI:qualifiedName:parser:didStartElement:namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser:didStartElement:namespaceURI:qualifiedName:parser:did namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser:didStartElement:namespaceURI:qualifiedName:parserDidEndDocument:

パターンを見ますか?それはループです。を複数回連続して呼び出すことも可能であることに注意してください。そのためparser:foundCharacters:、プロパティを前の値に追加します。

まとめると、ここには2つの問題があります。まず第一に、あらゆる種類のループでメモリが蓄積すると、アプリがクラッシュするようです。第二に、NSMutableStringプロパティでの使用はそれほどエレガントではなく、意図したとおりに機能するかどうかさえわかりません。

一般に、ARCを使用して文字列をループしているときに、このメモリの蓄積を克服する方法はありますか?NSXMLParserに固有のことができることはありますか?

編集:

最初のテストでは、1秒を使用しても@autoreleasepool{...}問題は解決しないようです。

オブジェクトは、thwyが存在する間、メモリ内のどこかに移動する必要があり、自動解放プールが空になる可能性があるrunloopの終わりまでそこにあります。

これは、NSXMLParserに関する限り、文字列の状況では何も修正されません。ループがメソッド呼び出し全体に分散しているため、修正される可能性があります。さらにテストする必要があります。

(理論的には、ARCはメモリがピークアウトするまでではなく、ある時点でメモリをクリーンアップするため、これをメモリピークと呼んでいることに注意してください。実際には何もリークしていませんが、同じ効果があります。)

編集2:

自動解放プールをループ内に固定すると、いくつかの興味深い効果があります。NSStringオブジェクトに追加するときの蓄積をほぼ軽減するようです。

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                text = [text stringByAppendingString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

割り当てトレースは次のようになります。

ここに画像の説明を入力してください

時間の経過とともにメモリが徐々に蓄積されることに気づきましたが、それは約150キロバイトの調整であり、以前に見られた350メガバイトではありません。ただし、このコードを使用NSMutableStringすると、自動解放プールを使用しない場合と同じように動作します。

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                [text appendString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

そして、割り当てトレース:

NSMutableStringは、自動解放プールの影響を受けないようです

NSMutableStringは明らかに自動解放プールの影響を受けないように見えます。理由はわかりませんが、最初は、これを以前に見たものと結び付けます。これはNSMutableString、それ自体で約100万回の反復を処理できますが、処理NSStringできません。

それで、これを解決する正しい方法は何ですか?

4

2 に答える 2

11

大量の自動解放されたオブジェクトで自動解放プールを汚染しています。

ループの内部を自動解放プールで囲みます。

for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}
于 2012-08-09T13:57:51.137 に答える
1

メモリ関連のバグを探している間、@""と@"Hello"は不滅のオブジェクトになることに注意してください。あなたはそれを定数として考えることができますが、オブジェクトのためです。このオブジェクトのインスタンスは、常に1つだけメモリに存在します。

@bbumが指摘し、検証したように、@autoreleasepoolはループでこれを処理する正しい方法です。

@autoreleasepoolとNSMutableStringを使用した例では、プールは実際にはあまり機能しません。ループ内の唯一の致命的なオブジェクトは@""のmutableCopyですが、これは1回だけ使用されます。もう1つのケースは、永続オブジェクト(NSMutableString)へのobjc_msgSendであり、これは不滅のオブジェクトとセレクターのみを参照します。

私は、メモリの蓄積がAppleのNSMutableStringの実装内にあると推測することしかできませんが、それがない場合ではなく、@autoreleasepool内に表示されるのはなぜだろうと思います。

于 2012-08-09T16:25:23.670 に答える