首先我們先看個好玩的事情~
#import "ViewController2.h"
@interface ViewController2 () {
__weak id tracePtr;
}
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"viewWillAppear tracePtr: %@", tracePtr);
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(@"viewDidAppear tracePtr: %@", tracePtr);
}
@end
看到上面的代碼,猜測一下輸出會是什么呢仰挣?我最開始的想法應該都是null豆茫,因為tracePtr是弱指針馋劈,str在viewDidLoad結(jié)束以后就沒有引用計數(shù)了,應該被回收掉偶洋,所以在viewWillAppear和viewDidAppear中再打印的時候應該就空啦熟吏。
但是實際上打印了什么嘞?
Example1[10896:167819] viewWillAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[10896:167819] viewDidAppear tracePtr: (null)
是不是灰常神奇玄窝,在viewWillAppear的時候str的內(nèi)存仍舊沒有被清空牵寺。這是為什么呢?
autorelease
上面的問題一會兒再解決恩脂,我們先了解一下autorelease相關(guān)的方法哈帽氓。在MRC時代我們需要自己手動管理內(nèi)存,當對象不用了以后俩块,需要調(diào)用[obj release]來釋放內(nèi)存黎休,但有的時候我們不希望它馬上釋放,需要它等一會兒在釋放玉凯,例如作為函數(shù)返回值:
- (Person *)createPerson {
return [[[Person alloc] init] autorelease];
}
如果不加autorelease势腮,直接返回一個新的person,那么由于alloc init會加一次引用計數(shù)漫仆,無論怎么也無法抵消捎拯,除非alloc后調(diào)用release或者讓外部release兩次,但依賴調(diào)用者release是很不容錯的盲厌;而如果馬上release署照,外部調(diào)用這個方法的拿到的就是nil了祸泪,所以這里用autorelease。
autorelease會將對象放到一個自動釋放池中藤树,當自動釋放池被銷毀時浴滴,會對池子里面的所有對象做一次release操作。也就是調(diào)用后不是馬上計數(shù)-1岁钓,而是在自動釋放池銷毀時再-1升略。
這樣的話當外部createPerson以后是可以獲取到一個person的,如果使用了另外的引用指向person屡限,person的引用數(shù)暫時為2品嚣,而自動釋放池銷毀時,會對person執(zhí)行一次release钧大,它的計數(shù)就變?yōu)榱?翰撑,由于仍舊有引用就不會被銷毀;如果外部沒有建新的引用啊央,那么在自動釋放池銷毀時就會銷毀這個對象啦眶诈。
這里的自動釋放池其實是和runloop有關(guān)的,是系統(tǒng)自動創(chuàng)建維護的瓜饥,每次runloop休眠的時候進行清空逝撬,后面的autoreleasepool中會解釋。
我們來看下源碼~
//autorelease方法
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
//檢查是否可以優(yōu)化
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
//放到auto release pool中乓土。
return rootAutorelease2();
}
// rootAutorelease2
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
再看一下AutoreleasePoolPage的autorelease:
public: static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
autorelease方法會把對象存儲到AutoreleasePoolPage的鏈表里*next++ = obj;
宪潮。等到auto release pool被釋放的時候,把鏈表內(nèi)存儲的對象刪除趣苏。所以狡相,AutoreleasePoolPage就是自動釋放池的內(nèi)部實現(xiàn)。
autorelease釋放時機
ARC時代我們是不用自己做對象釋放的處理滴食磕,但ARC其實就是對MRC包了一下尽棕,系統(tǒng)幫我們release和retain,ARC中也是有需要延后銷毀的autorelease對象的彬伦,它們究竟在什么時候銷毀的呢萄金?
其實對象的釋放是由autorelease pool來做的,而這個pool會在RunLoop進入的時候創(chuàng)建媚朦,在它即將進入休眠的時候?qū)ool里面所有的對象做release操作氧敢,最后再創(chuàng)建一個新的pool。(RunLoop可參考:http://www.cocoachina.com/articles/11970)
{
/// 1. 通知Observers询张,即將進入RunLoop
/// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)孙乖。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers唯袄,即將進入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers弯屈,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的恋拷,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了资厉,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
故而蔬顾,在當前RunLoop沒有進入這一輪兒休眠的時候宴偿,對象是暫時不會釋放的,所以如果我們不特殊處理這些autorelease變量诀豁,在他們看起來計數(shù)為0的時候窄刘,可能也不會立刻被釋放,因為其實它的計數(shù)還沒歸零舷胜,當release執(zhí)行后才歸零娩践。
- 那么autorelease Pool是啥呢?
還記得MRC的[obj autorelease]
么烹骨,其實就是將obj放入了自動釋放池的頂部翻伺,這個自動釋放池就是autorelease Pool。
它類似一個棧沮焕,我們可以往里面push一個個新建的變量穆趴,然后在池子銷毀的時候,就會把里面的變量一個個拿出來執(zhí)行release方法遇汞。
@autoreleasepool與AutoreleasePool及原理
我們最經(jīng)常看到的大概就是main()函數(shù)里的autoreleasepool了簿废,如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
這個main()函數(shù)里面的池并非必需空入。因為塊的末尾是應用程序的終止處,即便沒有這個自動釋放池族檬,也會由操作系統(tǒng)來釋放歪赢。但是這些由UIApplicationMain函數(shù)所自動釋放的對象就沒有池可以容納了,系統(tǒng)會發(fā)出警告单料。因此埋凯,這里的池可以理解成最外圍捕捉全部自動釋放對象所用的池。
@autoreleasepool{}其實就相當于:
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
AutoreleasePoolPage是啥類扫尖?上面也出現(xiàn)過它的身影白对,那么它的定義是:
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
這里的parent和child其實就是鏈表的上一個和下一個,也就是說其實自動釋放池AutoreleasePool里面有很多AutoreleasePoolPage换怖,page形成一個鏈表結(jié)構(gòu)甩恼,就像下圖一樣:
自動釋放池AutoreleasePool是以一個個AutoreleasePoolPage組成,而AutoreleasePoolPage以雙鏈表形成的自動釋放池。
AutoreleasePoolPage中的每個對象都會開辟出虛擬內(nèi)存一頁的大刑趺(也就是4096個字節(jié))悦污,除了實例變量占據(jù)空間,其他的空間都用來存儲autorelease對象的地址钉蒲。
id * next指向的是棧頂對象的下一個位置切端,這樣再放入新的對象的時候就知道放到哪個地址了,放入以后會更新next指向顷啼,讓它指到新的空位踏枣。如果AutoreleasePoolPage空間被占滿時,會創(chuàng)建一個AutoreleasePoolPage連接鏈表线梗,后來的對象也會在新的page加入椰于。
單向鏈表適用于節(jié)點的增加刪除,雙向鏈表適用于需要雙向查找節(jié)點值的情況仪搔。這即是AutoreleasePoolPage以雙鏈表的方式組合的原因瘾婿。缺點就是空間占用較單鏈表大。
-
假設當前線程只有一個AutoreleasePoolPage對象烤咧,對象的內(nèi)存地址如下圖:
AutoreleasePoolPage開始 然后當一個對象發(fā)送了autorelease消息偏陪,就是將當前這個對象加入到AutoreleasePoolPage的棧頂next指向的位置。
-
每進行一次objc_autoreleasePoolPush調(diào)用時煮嫌,runtime就會將當前的AutoreleasePoolPage加入一個哨兵對象笛谦,就會變成下面結(jié)構(gòu):
objc_autoreleasePoolPush objc_autoreleasePoolPop的時候,根據(jù)傳入的哨兵位置找到哨兵所對應的page
將晚于哨兵對象插入的autorelease對象都發(fā)送一個release消息昌阿,并移動next指針到正確的位置饥脑。
objc_autoreleasePoolPush返回值也就是哨兵對象的地址,被objc_autoreleasePoolPop作為參數(shù)懦冰。
@autoreleasepool{} 就是先push一下得到哨兵地址灶轰,然后把包裹的創(chuàng)建的變量一個個放入AutoreleasePoolPage,最后pop將哨兵地址之后的變量都拿出來一個個執(zhí)行release刷钢。所以@autoreleasepool和AutoreleasePool不是一個含義哦笋颤!
ARC與MRC下如何創(chuàng)建自動釋放池
NSAutoreleasePool(只能在MRC下使用)
@autoreleasepool {}代碼塊(ARC和MRC下均可以使用)
// MRC
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];
// ARC
@autoreleasepool {
id obj = [NSObject alloc] init];
}
@autoreleasepool應用場景
- 循環(huán)優(yōu)化
如果你嘗試跑下面的代碼,你的內(nèi)存會持續(xù)性的增加内地,幾乎電腦容量就爆了伴澄。。畢竟100000000非常大阱缓,所以如果想嘗試建議改小哈非凌。
for (int i = 0; i < 100000000; i++) {
UIImage *image = [UIImage imageNamed:@"logo"];
}
這個內(nèi)存爆的原因其實就是image作為局部變量,在不特殊處理的時候會在runLoop休眠時再被銷毀荆针,不會立即銷毀清焕。
所以如果想解決這個問題應該改為:
for (int i = 0; i < 100000000; i++) {
@autoreleasepool{
UIImage *image = [UIImage imageNamed:@"logo"];
}
}
- 如果你的應用程序或者線程是要長期運行的并蝗,或者長期在后臺中運行的任務,因為任務運行中runloop是不會休眠的秸妥,如果產(chǎn)生大量需要autorelease的對象滚停,需要手動@autoreleasepool,否則不會立刻釋放導致內(nèi)存增加
子線程中Autorelease的釋放
子線程在使用autorelease對象時粥惧,如果沒有autoreleasepool會在autoreleaseNoPage中懶加載一個出來键畴。
在runloop的run:beforeDate,以及一些source的callback中突雪,有autoreleasepool的push和pop操作起惕,總結(jié)就是系統(tǒng)在很多地方都有autorelease的管理操作。
就算插入沒有pop也沒關(guān)系咏删,在線程exit的時候會釋放資源惹想。
最后解答一下最開始的問題:
通常非alloc、new督函、copy嘀粱、mutableCopy出來的對象都是autorelease的,比如[UIImage imageNamed:]辰狡、[NSString stringWithFormat]锋叨、[NSMutableArray array]等。(會加入到最近的autorelease pool哈)
也就是說 [NSString stringWithFormat:@"%@", @"ss"]方法內(nèi)部類似于:
+(NSString *) stringWithFormat {
NSString *str = [[NSString alloc] initWithXXX];
return [str autorelease];
}
因為alloc init已經(jīng)對引用+1了宛篇,然后NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
再次增加了引用娃磺,作用域結(jié)束的時候只是release了一次,這個變量在stringWithFormat內(nèi)部放入了自動釋放池叫倍,于是要在pool pop的時候才會再次release偷卧,真正的進行內(nèi)存釋放。
所以哦吆倦,不是autoreleasepool可以自動監(jiān)測對象的創(chuàng)建听诸,而是你對象創(chuàng)建的時候被ARC默認加了
return [obj autorelease]
,就被放進AutoReleasePage啦
下面測試一下alloc之類的會怎樣:
- 如果替換為mutableCopy逼庞,則在離開作用域的時候馬上就銷毀了:
NSMutableString *str = [@"a string object" mutableCopy];
輸出:
Example1[41407:454952] viewWillAppear tracePtr: (null)
Example1[41407:454952] viewDidAppear tracePtr: (null)
- 如果替換為NSArray的alloc init方法也是會立刻release:
NSArray *arr = [[NSArray alloc] initWithObjects:@(1), nil];
tracePtr = arr;
輸出:
Example1[41494:457063] viewWillAppear tracePtr: (null)
Example1[41494:457063] viewDidAppear tracePtr: (null)
- 如果替換為NSString的alloc init方法比較特殊,是不會release的:
NSString *str = [[NSString alloc] initWithString:@"a string object"];
//等同于NSString *str = @"a string object";
tracePtr = str;
輸出:
Example1[41494:457063] viewWillAppear tracePtr: tracePtr: a string object
Example1[41494:457063] viewDidAppear tracePtr: tracePtr: a string object
這個我猜測大概是類似java里面的常量池瞻赶,由系統(tǒng)來管理字符串字面量的釋放之類的赛糟,和Array不太一樣。
加入@autoreleasepool再測一下~
- stringWithFormat返回的autorelease對象會被加入到最近的autorelease pool也就是@autoreleasepool {}所在的page砸逊,在@autoreleasepool {}執(zhí)行到結(jié)束的時候璧南,就會把它包裹的新對象都從page拿出來執(zhí)行一遍release,所以當運行到
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
的時候师逸,str對象已經(jīng)release過了司倚。
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
輸出:
Example1[42032:469774] viewDidLoad tracePtr: (null)
Example1[42032:469774] viewWillAppear tracePtr: (null)
Example1[42032:469774] viewDidAppear tracePtr: (null)
- 在@autoreleasepool聲明變量:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = nil;
@autoreleasepool {
str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
輸出:
Example1[42055:470561] viewDidLoad tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42055:470561] viewWillAppear tracePtr: (null)
Example1[42055:470561] viewDidAppear tracePtr: (null)
雖然str加入了autorelease pool,也就是在運行到@autoreleasepool結(jié)尾的時候會對str做release操作,相當于stringWithFormat的autorelease剛把對象放到自動釋放池动知,自動釋放池就做了pop操作執(zhí)行了release皿伺,相當于抵消了stringWithFormat的autorelease。
但是str即使做了release計數(shù)-1盒粮,外面還有一個引用鸵鸥,所以引用數(shù)仍舊不為0,故而不會立刻釋放丹皱,當運行完viewDidLoad的時候它的計數(shù)-1妒穴,會立刻進行釋放。
6.我們再來最后試一下字面量的@autoreleasepool:
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = @"ssuuuuuuuuuuuuuuuuuuuu";
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
輸出:
Example1[42074:471180] viewDidLoad tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42074:471180] viewWillAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42074:471180] viewDidAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
看起來字面量好像即使用了@autoreleasepool也不會釋放了摊崭,它大概是由系統(tǒng)管理吧讼油,string和number應該是比較特殊的兩種,但不用擔心這種的內(nèi)存問題呢簸,畢竟系統(tǒng)肯定會把這種管理好矮台。
最后有個小問題:子線程默認沒有runloop,而autoreleasepool依賴于runloop阔墩,那么子線程沒有autoreleasepool么嘿架?它的變量如何釋放呢?
可以參考下下面的文章啸箫,總的而言就是最好自己創(chuàng)建一個
http://www.reibang.com/p/90d08a99da20
參考:
http://www.reibang.com/p/8133439812d4
原理寫的比較好:http://www.reibang.com/p/d0558e4b0d21
http://www.reibang.com/p/30c4725e142a
http://www.reibang.com/p/5559bc15490d
http://www.reibang.com/p/505ae4c41f31