發(fā)現(xiàn)我從接觸iOS開發(fā)到現(xiàn)在,幾乎都沒有使用過autorelease這個詞。在ARC內存管理方式下,就像不能發(fā)送release
和retain
消息一樣,程序員也不能對某個對象發(fā)送autorelease
消息,讓我?guī)缀跬浟怂拇嬖凇K越裉焱蝗幌肫饋恚瑧摬椴锳RC下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
對象,會得到與預期不符的實驗結果。
-
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
對象,這樣得到的helloWorldString
和newHelloWorldString
到輸出這一步時都不會變成空。 -
使用
initWithString:
既然literal沒法達到預期實驗效果,那用
initWithString:
方法應該能創(chuàng)建出比較正常的NSString
對象了吧?
把helloWorldString
和newHelloWorldString
兩個方法中的語句都替換為:
return [[NSString alloc] initWithString:@"HelloWorld"];
然而運行一下發(fā)現(xiàn),newHelloWorldString
并沒有像預期的一樣被銷毀。
這是什么原因呢?嗯。。如果換一種寫法:
NSString *s1 = @"Hello World";
NSString *s2 = [[NSString alloc] initWithString:s1];
然后運行到這里時輸出s1
和s2
指向的地址,發(fā)現(xiàn)這兩個指針竟然指向同一個地址。看來initWithString:
方法中可能做了某些處理?
-
使用
stringWithFormat:
老板問我用
stringWithFormat:
為什么不能達到預期效果,雖然我當場有點懵逼,但是回頭一想,這不是顯然么。
因為stringWithFormat:
這個方法,不是以alloc/new/copy/mutableCopy開頭的呀,所以它返回的對象已經(jīng)被注冊到了autorelease pool里呀。