Hello!小夥伴們,大家好呀!歡迎來到暴走…………(sorry,拿錯劇本了)。咳咳,大家好呀!今天呢,我要來介紹一個非常古老的東西——Objective-C中的自動釋放池。
我們拿起Xcode默認生成的HelloWorld代碼我們就能夠看到:
int main(int argc, const char *argv[]) {
@autoreleasepool {
// start your code here...
}
return 0;
}
對於初學者來說,這個@autoreleasepool看起來實在是很費解啊。在實際的代碼編寫中,貌似無視掉(甚至去掉)這個@autoreleasepool好像也沒什麼影響嘛。那麼這玩意到底是個神馬?
這個問題呢,需要追溯到iOS 4以前,那個時候我們在使用Objective-C編寫iOS和Mac OS X程序時,都是手動的進行內存管理,我們稱其爲:MRC(Mannul Reference Counting),內存管理機制爲引用計數器,舉一個例子(由於本文的重點不在於介紹MRC,因此只做概述不進行詳細講解):
@import Cocoa;
@interface Test : NSObject
@end
@implementation Test
- (void)dealloc {
NSLog(@"析構");
[super dealloc];
}
- (instancetype)init {
if (self = [super init]) {
NSLog(@"構造");
}
return self;
}
@end
int main(int argc, const char * argv[]) {
Test *t = [[Test alloc] init]; // 構造,引用計數+1
[t retain]; // 引用計數+1
[t release]; // 引用計數-1
[t release]; // 引用計數-1,歸零,析構
return 0;
}
如果創建的對象比較多,而它們又需要在一個統一的時間進行大量的release時,用這種方法管理內存就會使得代碼過於冗長,降低可讀性,因此,我們使用自動釋放池來統一管理。int main(int argc, const char * argv[]) {
@autoreleasepool {
Test *t1 = [[[Test alloc] init] autorelease]; // 構造,引用計數+1
Test *t2 = [[[Test alloc] init] autorelease]; // 構造,引用計數+1
} // 將所有調用過autorelease方法的對象全部釋放,t1和t2析構
return 0;
}
這裏強調一個特殊情況,如果一個沒有被調用autorelease方法的對象中途沒有了指向的指針,則該對象在程序運行結束之前無法被釋放,這樣就會出現內存泄露。int main(int argc, const char * argv[]) {
Test *t1 = [[Test alloc] init];
t1 = nil; // 指針脫離對象,對象無法再次被指向,因此無法被釋放,產生內存泄露
[[Test alloc] init]; // 同樣產生內存泄露
return 0;
}
但是,如果在指針脫離前,該對象處於自動釋放池中,並且調用過autorelease方法,則不會出現這樣的問題@autoreleasepool {
id t = [[[Test alloc] init] autorelease];
t = nil;
//......
} // 在這裏仍然會釋放對象
這裏搞明白以後我們再來看看ARC的情況。ARC把指向對象的指針分成了三類,分別是:strong,weak,autoreleasing。接下來將逐一介紹。
如果一個指針被聲明爲strong(__strong關鍵字爲默認,可以省略),那麼當這個這個指針指向某一對象時,該對象的引用計數器會+1,而當一個strong指針不再指向這個對象時,該對象的引用計數器會自動-1,請仔細觀察以下的情況:
int main(int argc, const char * argv[]) {
Test __strong *t1 = [[Test alloc] init]; // 創建對象,同時有strong指針指向對象,引用計數+1
{
Test __strong *t2 = t1; // 又有一個strong指針指向了對象,引用計數器+1
} // 代碼塊結束,局部變量的生命週期結束,t2被銷燬,對象少了一個strong指針去指向,引用計數-1
t1 = [[Test alloc] init]; // t1指向了新的對象,原對象少了一個strong指針指向,引用計數-1,由於引用計數歸零,對象析構
t1 = nil; // t1清空,後創建的對象引用計數器-1,也被銷燬
return 0;
}
如果一個指針被聲明爲weak,那麼該指針的指向不會影響對象的引用計數器,當weak指針指向的對象被釋放時,該指針自動清空。這種類型的指針的出現主要是爲了解決循環引用的問題(這裏不多做闡述)int main(int argc, const char * argv[]) {
Test __strong *t1 = [[Test alloc] init]; // 創建對象,同時有strong指針指向對象,引用計數+1
Test __weak *t2 = t1; // weak指針指向對象,引用計數器不變
t1 = [[Test alloc] init]; // t1指向新對象,原對象引用計數器-1,歸零,釋放,這時,t2指針將會自動被賦值爲nil,ARC的這種處理機制可以避免野指針(也叫垂懸指針)
[t2 discardEditing]; // 向一個空指針(注意這裏不是野指針)發送消息,不會產生錯誤,也不會得到迴應
return 0;
}
如果一個指針被聲明爲autoreleasing,那麼該指針指向的對象將會被註冊到當前的自動釋放池中(如果當前不在自動釋放池中,則相當於一個普通指針,這種情況下通常會產生內存泄露),當自動釋放池結束時,會對所有註冊到該池的對象進行引用計數器-1的操作int main(int argc, const char * argv[]) {
id __weak t1; // 定義了一個weak指針
id t = [[Test alloc] init]; // 定義個一個strong指針,並指向一個新創建的對象,引用計數器+1
@autoreleasepool { // 定義了一個自動釋放池
id __autoreleasing t2 = t; // 定義了一個autoreleasing指針,並指向對象,引用計數器+1,並將對象註冊到自動釋放池中
t1 = t2; // 用一個weak指針指向對象,引用計數器不變
t2 = nil; // 這裏將autoreleasing指針清空,對原對象的引用計數器沒有影響,但是由於該對象已經被註冊到自動釋放池中,因此不會產生內存泄露
t = nil; // strong指針清空,對象的引用計數器-1
} // 退出自動釋放池,對象的引用計數器-1,歸零,釋放,t1指針自動清空
[t1 discardEditing]; // 此時的t1成爲了nil,因此向其發送消息不會得到回覆,也不會產生錯誤
return 0;
}
相信看到這裏,你應該明白這個神奇的@autoreleasepool是什麼東西了吧。值得一提的是,雖然ARC機制極力避免出現內存泄露以及野指針的情況,但在某些情況下還是會出現,例如: id __autoreleasing t;
@autoreleasepool {
t = [[Test alloc] init];
} // 對象在這裏釋放
[t show]; // 由於t並不是weak型,因此會成爲野指針,發送消息會引發致命錯誤
int main(int argc, const char * argv[]) {
id __autoreleasing t = [[Test alloc] init]; // 由於t沒有處於任何的自動釋放池,因此該對象無法被釋放,會產生內存泄露
return 0;
}
最後要說的一點是,自動釋放池本身也是一個代碼塊,其中定義的變量(包括指針)如果沒有用static關鍵字修飾的話,都會在代碼塊結束時被釋放,我們在使用時要小心int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 5;
}
NSLog(@"%d", a); // 這裏會報錯,因爲自動釋放池中定義的a在這裏沒有作用域
return 0;
}
關於ARC中的@autoreleasepool今天就講到這裏啦。其實我本人前陣子也是一直在爲它發愁,查了很多資料都沒有正面闡述在ARC中自動釋放池的作用,網上搜出來的也總是千篇一律,不解決問題,所以只能自己慢慢嘗試,慢慢研究。今天把研究的成功寫成博客,希望能夠幫到更多像我一樣的人。