一道題考你對(duì)__autoreleasing和__block的理解

考慮下面的代碼,有哪些問(wèn)題,如何把他改成正確的形式?

@interface TestObj : NSObject
@end
@implementation TestObj

- (void)methodWillSetError:(NSError **)error group:(dispatch_group_t)group {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        *error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
        dispatch_group_leave(group);

    });
}
@end
void testBlockAndAutoReleasePool() {
    NSLog(@"Hello, World!");
    NSError *error;
    TestObj *testObj = [TestObj new];
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    [testObj methodWillSetError:&error group:group];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"error is %@", error);
    });
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        testBlockAndAutoReleasePool();
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop run];
    }
    return 0;
}

methodWillSetError會(huì)去異步設(shè)置error的值,然后另外一個(gè)地方在error設(shè)置后去訪問(wèn)error的值。

實(shí)際上現(xiàn)在新版的Xcode已經(jīng)會(huì)對(duì)
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
進(jìn)行警告
Block captures an autoreleasing out-parameter, which may result in use-after-free bugs
那么這個(gè)警告是什么意思呢?
實(shí)際上這個(gè)方法
- (void)methodWillSetError:(NSError * *)error group:(dispatch_group_t)group
的error,雖然沒(méi)有明確指定內(nèi)存的修飾符(strong, weak, autoreleasing,但是如果你直接定義NSError **error的臨時(shí)變量,在arc下xcode會(huì)編譯失敗,要求你明確指定內(nèi)存關(guān)系)但是編譯器會(huì)默認(rèn)轉(zhuǎn)成NSError * __autoreleasing*,而在block中捕獲一個(gè)__autoreleasing的out-parameter是很容易造成錯(cuò)誤的。
為什么這么說(shuō)呢?

void testAutoReleaseError(NSError **error) {
    [@[@1, @2] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (idx == 0) {
            *error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil];
        }
    }];
    NSLog(@"error:%@" , *error);

}

我們用個(gè)簡(jiǎn)化例子來(lái)看一下,這個(gè)是很容易隨手寫下的代碼。打開(kāi)Xcode的Zombie,會(huì)發(fā)現(xiàn)在
NSLog(@"error:%@" , *error);
那一行crash掉,訪問(wèn)了個(gè)已經(jīng)釋放的對(duì)象,error已經(jīng)被dealloc掉了。
為什么呢?前面說(shuō)了error默認(rèn)是NSError *__autoreleasing *,也就是說(shuō)*error指向的對(duì)象是個(gè)__autoreleasing對(duì)象,所以
*error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil];
的賦值在arc下會(huì)加個(gè)autorelease的調(diào)用變成
*error = [[[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil] autorelease];

而eumerateXXX這一系列的容器接口,里面的實(shí)現(xiàn)是包了一層Autorelease Pool的,所以在block運(yùn)行完后Autorelease Pool被釋放了附帶著把*error指向的對(duì)象給釋放了,*error就指向了個(gè)野指針,考慮到block運(yùn)行時(shí)候外層存在是否包裹著一層Autorelease Pool的不確定性,所以clang直接把在block里捕獲__autoreleasing的out-parameter給警告了。解決這個(gè)問(wèn)題有兩種方案,一種是指定error類型為NSError *__strong *。

把一開(kāi)始的案例中的__autoreleasing修改成__strong后,會(huì)發(fā)現(xiàn)error打出來(lái)還是空的,這是為什么呢?
因?yàn)閎lock捕獲變量的時(shí)候是捕獲變量的當(dāng)前值,你對(duì)變量之后的重新賦值對(duì)block里的變量不會(huì)有影響。而在block里面也不能對(duì)變量做修改(block里的error實(shí)際上是個(gè)拷貝了當(dāng)前block定義時(shí)候error的值的和block綁定的同名變量)
實(shí)際上除了error打出來(lái)是空的問(wèn)題,這里還有個(gè)嚴(yán)重的可能會(huì)導(dǎo)致各種異常情況的bug,普通debug可能看不出來(lái),但是打開(kāi)Xcode的Address Sanitizer 的 Detect use of stack after return,就會(huì)發(fā)現(xiàn)在
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
賦值這里會(huì)提示說(shuō)Use of stack memory after return,因?yàn)樵?/p>

void testBlockAndAutoReleasePool() {
    NSLog(@"Hello, World!");
    NSError *error;
    TestObj *testObj = [TestObj new];
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    [testObj methodWillSetError:&error group:group];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"error is %@", error);
    });
}

這一層的error是個(gè)棧變量,對(duì)其取地址得到是棧上的空間,等到dispatch_after去設(shè)置error的值的時(shí)候,棧空間由于函數(shù)已經(jīng)返回了已經(jīng)被銷毀了,這里對(duì)error的寫入會(huì)導(dǎo)致棧的破壞(可能某個(gè)棧變量的值就被覆蓋了),導(dǎo)致各種奇怪crash你還無(wú)從定位。

這里就需要__block,__block的修飾可以將變量從??臻g的作用域提升到堆上。這里還有個(gè)小知識(shí)點(diǎn),如果你直接加__block會(huì)發(fā)現(xiàn)還是在同樣的地方會(huì)報(bào)出Use of stack memory after return,因?yàn)殡m然__block可以將變量從棧空間的作用域提升到堆上,但它這個(gè)時(shí)機(jī)是在block被copy的時(shí)候才發(fā)生的,在我們的代碼里,是先調(diào)用了methodWillSetError再調(diào)用dispatch_async,在methodWillSetError對(duì)error取地址的時(shí)候變量還在棧上,所以需要將async和methodWillSetError交換一下順序才能保證代碼正常運(yùn)行。

回過(guò)頭來(lái)再來(lái)看下,__autoreleasing到底是什么,為什么clang要把__autoreleasing作為默認(rèn)選項(xiàng),它和__strong的區(qū)別是什么?起初我們定義NSError *error的時(shí)候這里arc下不是默認(rèn)是NSError * __strong error嗎,把它的地址傳遞給個(gè)NSError * __autoreleasing *會(huì)發(fā)生什么?
https://clang.llvm.org/docs/AutomaticReferenceCounting.html
雖然clang的這篇文章是最權(quán)威也最全的文檔,但是里面介紹還是很繞口的。所以這里就再說(shuō)明一下。

把一個(gè)NSError *__strong*傳遞給一個(gè)接收NSError *__autoreleasing*參數(shù)的方法的時(shí)候,clang采用了一個(gè)pass-by-writeback的策略。
具體說(shuō)來(lái)就是,在這一步驟
[testObj methodWillSetError:&error group:group];
clang 會(huì)改寫成

NSError *__autoreleasing temp_error = error;
[testObj methodWillSetError:&temp_error group:group];
error = temp_error;

所以即使error的作用域已經(jīng)被提升到了堆空間,但是如果error的修飾符是`NSError *__autoreleasing*,就會(huì)被轉(zhuǎn)成一個(gè)在棧上的臨時(shí)變量,傳遞到方法里異步去設(shè)置error的時(shí)候還是會(huì)造成棧的地址破壞。
那么為什么默認(rèn)是__autoreleasing呢?在大部分的代碼中其實(shí)往往就是一個(gè)局部變量(默認(rèn)__strong類型),傳遞給一個(gè)out-parameter變量,這樣就要經(jīng)歷這個(gè)__autoreleasing的轉(zhuǎn)來(lái)轉(zhuǎn)去的過(guò)程。
其實(shí)沒(méi)啥特殊的原因,主要就是慣例,就和alloc,copy,mutableCopy和new家族的方法默認(rèn)返回的是Retained return values,而其他函數(shù)通常返回的就是個(gè)Unretained return values一樣。在Cocoa編程中,out-parameter返回的就是個(gè)autoreleasing的對(duì)象。(所以如果你在mrc下寫一個(gè)傳出out-parameter的方法,要確保這個(gè)out-parameter在離開(kāi)這個(gè)方法的時(shí)候是個(gè)autoreleasing的狀態(tài),如果是個(gè)+1所有權(quán)的對(duì)象,那么就會(huì)有內(nèi)存泄漏風(fēng)險(xiǎn))。對(duì)比如果要把所有這種out-parameter的方法的參數(shù)加上個(gè)__autoreleasing的修飾,還不如直接所有的out-parameter默認(rèn)就是__autoreleasing。所以這個(gè)只是一個(gè)最不壞的方案。

最后,給讀者出個(gè)小問(wèn)題,前面說(shuō)了解決Block captures an autoreleasing out-parameter有兩個(gè)辦法,在我們的方法中,由于是要去異步設(shè)置error的值,所以語(yǔ)義上就應(yīng)該是個(gè)__strong的修飾符,這是方法一,那么如果只是同步方法,又想要在block里設(shè)置這個(gè)out-parameter,應(yīng)該要怎么做呢,這個(gè)就留給讀者思考了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容