ARC下的autorelease

發(fā)現(xiàn)我從接觸iOS開發(fā)到現(xiàn)在,幾乎都沒有使用過autorelease這個詞。在ARC內存管理方式下,就像不能發(fā)送releaseretain消息一樣,程序員也不能對某個對象發(fā)送autorelease消息,讓我?guī)缀跬浟怂拇嬖凇K越裉焱蝗幌肫饋?,應該查查ARC下autorelease的正確使用姿勢(?_?)


內存管理原則



在內存管理方面,cocoa設立了一些基本原則,其中有這樣一條:

You own any object you create
You create an object using a method whose name begins with “alloc”, “new”, “copy”, or “mutableCopy”.

用以alloc/new/copy/mutableCopy為前綴的方法名創(chuàng)建的對象,是自己創(chuàng)建并持有的。

這條原則在ARC和MRC下都需要遵守。

比如cocoa中有很多工廠方法,它們不以alloc/new/copy/mutableCopy開頭,所以按照約定,它們返回的對象不應該被持有。在MRC下,一種可能的實現(xiàn)方式大概是這樣:

/*
 * MRC,此段代碼摘抄自[《Objective-C 高級編程》](https://book.douban.com/subject/24720270/)
 */

- (id)object
{
    id obj = [[NSObject alloc] init];

    /*
     * 自己持有對象
     */

    [obj autorelease];

    /*
     * 取得對象的存在,但自己不持有對象
     */

    return obj;
}

但是在ARC中,程序員不能對一個對象發(fā)送autorelease消息,那么如何將一個對象注冊到autorelease pool中,又如何遵循以上的這個原則呢?看來,開啟ARC后,編譯器幫助我們做了不少事情。


__autoreleasing修飾符



切換到ARC之后,每個指向OC對象的指針,都被賦上了所有權修飾符。一共有__strong、__weak、__unsafe_unretained__autoreleasing這樣四種所有權修飾符。

當一個對象被賦值給一個使用__autoreleasing修飾符修飾的指針時,相當于這個對象在MRC下被發(fā)送了autorelease消息,也就是說它被注冊到了autorelease pool中。

全局變量和實例變量是無法用__autoreleasing來修飾的,不然編譯器會報錯:

而局部變量用__autoreleasing修飾后,其指向的對象,在當前autorelease pool結束之前不會被回收:

__weak NSObject *weakObj1;
__weak NSObject *weakObj2;

{
    __autoreleasing NSObject *obj1 = [[NSObject alloc] init];
    weakObj1 = obj1; //weakObj1指向的對象已被注冊到autorelease pool
    
    __strong NSObject *obj2 = [[NSObject alloc] init];
    weakObj2 = obj2;//weakObj2指向的對象沒有被注冊到autorelease pool
}
//局部變量obj1和obj2的作用域結束,
//此時weakObj2指向的對象不再被強引用,因此被回收;
//而obj1指向的對象仍然在autorelease pool中

NSLog(@"%@", weakObj1);//輸出<NSObject: 0x100206030>,因為此刻weakObj1在autorelease pool中,不會因為obj1作用域的結束而被回收
NSLog(@"%@", weakObj2);//輸出null

方法名檢查



寫這樣一段代碼:

@interface XSQObject : NSObject

+ (NSString *)newHelloWorldString;
+ (NSString *)helloWorldString;

@end

@implementation XSQObject

+ (NSString *)newHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

+ (NSString *)helloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __weak NSString *helloWorldString = [XSQObject helloWorldString];
        __weak NSString *newHelloWorldString = [XSQObject newHelloWorldString];
        //此處有warning: 
        //assigning retained object to weak variable; 
        //object will be released after assignment 

        NSLog(@"%@", helloWorldString);//輸出HelloWorld
        NSLog(@"%@", newHelloWorldString);//輸出null
        
    }
    return 0;
}

雖然[XSQObject helloWorldString][XSQObject newHelloWorldString]兩個方法的實現(xiàn)完全一樣,但是它們返回的對象被賦值給__weak指針后,前者仍然存在,而后者則被銷毀了。如果再加入@autorelease塊做點實驗,可以發(fā)現(xiàn)helloWorldString指向的對象其實已被注冊到autorelease pool中。

對比內存管理原則,這就像是在MRC下,不以alloc/new/copy/mutableCopy開頭的方法,會對返回的對象發(fā)送autorelease消息一樣。而事實上,在ARC下,編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開頭,如果不是,則自動將返回的對象注冊到autorelease pool中。

更接近底層一點,其實Clang使用了一些標識來決定一個方法的返回值是否應該被持有。比如Clang文檔中有這樣的說明:

Methods in the alloc, copy, init, mutableCopy, and new families are implicitly marked __attribute__((ns_returns_retained)).

alloc/copy/init/mutableCopy/new家族中的方法,會被隱式標記為__attribute__((ns_returns_retained))

在一些特殊的情況下,程序員也可以手動給某些方法加上其他標記,來覆蓋被編譯器隱式加上的標記。


id *obj的傳遞



上面說到,編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開頭,來判斷是否需要將返回的對象注冊到autorelease pool中。

函數(shù)之間如果想要傳遞一個對象,不僅可以通過返回值,也可以通過將一個id指針作為參數(shù)的方式。在cocoa框架中,NSError的對象經(jīng)常通過這種方式來傳遞,比如NSManagedObjectContext中的:

- (BOOL)save:(NSError **)error;

其實,這個(NSError **)error相當于(NSError * __autoreleasing *)error,編譯器默認為其生成了__autoreleasing修飾符。

編譯器默認生成__autoreleasing修飾符的做法,也是在貫徹內存管理原則,即確保只有通過以alloc/new/copy/mutableCopy開頭的方法返回的對象才能被持有。

雖然當我們自己定義id *obj類型的參數(shù)時,也可以顯式指定它的所有權修飾符為其他,并通過編譯,但為了貫徹內存管理原則,還是應該將id *obj類型的參數(shù)的所有權修飾符指定為__autoreleasing。


參考

《Objective-C 高級編程》
Clang
Memory Management Policy
objc arc的簡單探索


2016.03.24更新



老板看了這篇后,問我,為什么在“方法名檢查”中用了initWithCString:encoding:來創(chuàng)建NSString對象?

哈哈,這是因為我試了別的幾種創(chuàng)建NSString對象的方式后,發(fā)現(xiàn)實驗結果和我預期的不符。

所以我來解釋一下為什么用下面幾種方法創(chuàng)建NSString對象,會得到與預期不符的實驗結果。

  1. literal

    literal,就是指用@"xxx"的方式創(chuàng)建一個NSString對象。然而,literal的內存管理方式有些特別。
    官方文檔明確指出了使用literal創(chuàng)建的NSString對象在整個程序的生命周期內都是不會被銷毀的:

    Objective-C string constant is created at compile time and exists throughout your program’s execution. The compiler makes such object constants unique on a per-module basis, and they’re never deallocated.

    所以如果用literal的方式來創(chuàng)建NSString對象,這樣得到的helloWorldStringnewHelloWorldString到輸出這一步時都不會變成空。

  2. 使用initWithString:

    既然literal沒法達到預期實驗效果,那用initWithString:方法應該能創(chuàng)建出比較正常的NSString對象了吧?
    helloWorldStringnewHelloWorldString兩個方法中的語句都替換為:

return [[NSString alloc] initWithString:@"HelloWorld"];

然而運行一下發(fā)現(xiàn),newHelloWorldString并沒有像預期的一樣被銷毀。

這是什么原因呢?嗯。。如果換一種寫法:
NSString *s1 = @"Hello World";
NSString *s2 = [[NSString alloc] initWithString:s1];

然后運行到這里時輸出s1s2指向的地址,發(fā)現(xiàn)這兩個指針竟然指向同一個地址??磥?code>initWithString:方法中可能做了某些處理?

  1. 使用stringWithFormat:

    老板問我用stringWithFormat:為什么不能達到預期效果,雖然我當場有點懵逼,但是回頭一想,這不是顯然么。
    因為stringWithFormat:這個方法,不是以alloc/new/copy/mutableCopy開頭的呀,所以它返回的對象已經(jīng)被注冊到了autorelease pool里呀。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容