iOS 開發刷題系列三:NSString 引用計數

weixin_33907511發表於2018-07-03

下面的程式會輸出什麼?

    NSMutableArray *ary = [[NSMutableArray array] retain];
    NSString *str = [NSString stringWithFormat:@"123456789"];
    NSString *longStr = [NSString stringWithFormat:@"1234567890"];
    
    [str retain];
    [longStr retain];
    [ary addObject:str];
    [ary addObject:longStr];
    
    NSLog(@"str = %ld", (unsigned long)[str retainCount]);
    NSLog(@"longStr = %ld", (unsigned long)[longStr retainCount]);
    
    [str retain];
    [str release];
    [str release];
    [longStr retain];
    [longStr release];
    [longStr release];
    
    NSLog(@"str = %ld", (unsigned long)[str retainCount]);
    NSLog(@"longStr = %ld", (unsigned long)[longStr retainCount]);
    [ary removeAllObjects];
    NSLog(@"str = %ld", (unsigned long)[str retainCount]);
    NSLog(@"longStr = %ld", (unsigned long)[longStr retainCount]);

輸出結果

2018-07-03 13:54:59.951143+0800 BlockTestDemo[13502:2107264] str = -1
2018-07-03 13:54:59.951374+0800 BlockTestDemo[13502:2107264] longStr = 3
2018-07-03 13:54:59.951613+0800 BlockTestDemo[13502:2107264] str = -1
2018-07-03 13:54:59.951717+0800 BlockTestDemo[13502:2107264] longStr = 2
2018-07-03 13:54:59.951956+0800 BlockTestDemo[13502:2107264] str = -1
2018-07-03 13:54:59.952044+0800 BlockTestDemo[13502:2107264] longStr = 1

在網上搜尋了一下,一般人給出的答案是:當字串長度小於10時,字串是儲存在常量區,沒有引用計數。如果長度大於等於10呢,就會被複制到堆去,有引用計數。

後來又出現了一個詞:Tagged Pointer 具體瞭解一下。 嘗試著輸出字串的class,發現兩者的類名是不同的:

NSString *str = [NSString stringWithFormat:@"123456789"];
NSString *longStr = [NSString stringWithFormat:@"1234567890"];
NSLog(@"str %s %p", object_getClassName(str), str);
NSLog(@"longStr %s %p", object_getClassName(longStr), longStr);
2018-07-03 13:54:59.950804+0800 BlockTestDemo[13502:2107264] str NSTaggedPointerString 0xa1ea1f72bb30ab19
2018-07-03 13:54:59.950979+0800 BlockTestDemo[13502:2107264] longStr __NSCFString 0x60c000224f20

Tagged Pointer專門用來儲存小的物件,例如NSNumber和NSDate
Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。

這應該也是上面的NSString在長度小於10的時候,沒有引用計數的原因了。

引申

這種情況引申出另外一道題:

@property (nonatomic, strong) NSString *strongStr;

dispatch_queue_t queue = dispatch_queue_create("strongStr", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 100000; i++) {
        dispatch_async(queue, ^{
            self.strongStr = [NSString stringWithFormat:@"ab %d", i];
        });
    }

如果將dispatch_async 裡面的內容改成:

self.strongStr = [NSString stringWithFormat:@"abcdefghijklmn %d", i];

會如何?
前者不會crash, 而後者會crash。
我們來看一下strongStr的setter方法:

- (void)setStrongStr:(NSString *)strongStr {
    if (strongStr == _strongStr) return;
    id pre = _strongStr;
    [strongStr retain];//1.先保留新值
    _strongStr = strongStr;//2.再進行賦值
    [pre release];//3.釋放舊值
}

結合上面的Tagged Pointer的解釋,呼叫retain or release時strongStr的引用計數一直都是-1;
而對於後者,strongStr實際上是一個物件,retain會使引用計數+1,release會使引用計數 -1;
而對於多執行緒非同步並行執行setStrongStr方法,可能會出現這種情況:多個執行緒拿到同一個舊值,然後給strongStr賦值不同的新值,然後在對舊值的release時候,出現多次release,程式crash;

相關文章