iOS
ARC

NSDataのbytesプロパティを参照するとautoreleaseプールに登録される

More than 3 years have passed since last update.

チャンク単位でNSDataを作成して処理させたら、autoreleasepoolにオブジェクトが溜まって大変だったのでメモ。

- (void)func
{
    for (int i = 0; i < 1000; ++i)
    {
        char text[] = "sample text";
        NSData * data = [[NSData alloc] initWithBytes:text length:sizeof(text)];
        [self operate:data];
    }
}

- (void)operate:(NSData *)data
{
    const char * chars = (const char *)data.bytes;
    NSLog(@"%02d %02d %02d %02d", chars[0], chars[1], chars[2], chars[3]);
}

こんな感じでNSDataを別の関数に渡して処理させていたら、メモリを圧迫してしまいました。

原因

結局 data.bytes を呼び出すとautoreleasingの参照が増えて、ループが終わってRunLoopに戻るまでオブジェクトが解放されませんでした。

operate:@autoreleasepool{}で囲うことで、メモリの問題は解消されました。

bytesプロパティはconst void*なので油断していたら、ここでも参照管理は(ある程度)されていました。

InstrumentsでAllocationsの様子を見ていると、_NSInlineDataの# Persistentが増え続けて、ループが終わるとごっそり# Transientに移るのが確認できました(ループが終わると消えるのでリークにはならない)。
@autoreleasepoolに入れると# Persistentはほぼ変わらず# Transientのみ増えるのでautoreleaseで解放されているようです。

(_objc_autoreleasePoolPrint()は試したけど何も出力してくれませんでした)

もう少し詳しく

bytesプロパティは @property (readonly) const void *bytes NS_RETURNS_INNER_POINTER;と宣言されていて、NS_RETURNS_INNER_POINTERが記されています。
LLVMのARC資料の7.7 Interior pointersによると、

the object’s lifetime will be extended until at least the earliest of:
- the last use of the returned pointer, or any pointer derived from it, in the calling function or
- the autorelease pool is restored to a previous state.

となっています。

これに従うとcharsのスコープが終わった時点で前者の条件を満たして参照が解放されると思うのですが、実際には、理由は分かりませんが(void *だから?)後者のautoreleaseの処理まで残っていたようです。

参考資料