寫在前面
最近再看YY大神的YYKit工具,發現在代碼中經常使用@autoreleasepool,特別是在與for循環搭配使用的時候。剛開始很不能理解。
先有個概念:
自己創建的對象:使用 alloc new copy mutablecopy 以及他們的駝峰變形 allocObject newObject copyObject mutablecopyObject。這八種創建的才是自己創建的對象。
不是自己創建的:除去以上八中都不是自己創建的。
autoreleasepool:只有非自己創建的對象才會注冊到離該對象最近的autoreleasepool中去。
之前對iOS的ARC的理解就是自己創建的對象,會在該對象所在是代碼快(當前作用域)結束時候自動釋放。無需程序員自己管理。但是如果仔細研究YYKit的代碼的話,發現如果注釋掉@autoreleasepool,程序一樣可以正常運行,但是觀察當前內存,會比在加上該句代碼時候占用比更大的空間。所以自己百度了一下,這里是sunny(百度一90后iOS程序員)大神寫的關於@autoreleasepool的理解,主要內容其實可以概括為 一句話"Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop",具體我就不抄了,但是有些東西不適合初學者,我自己總結了一些,比較容易理解一點。具體看代碼。
@interface ViewController () @property(strong, nonatomic) NSMutableArray *memoryUsageList; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //Init _memoryUsageList1 = [NSMutableArray new]; //創建一個串行隊列 dispatch_queue_t serialQueue = dispatch_queue_create("test.autoreleasepool", DISPATCH_QUEUE_SERIAL); __block NSString *strTest; dispatch_sync(serialQueue, ^{ for (int i = 0; i < kIterationCount; i++) { @autoreleasepool { NSNumber *num = [NSNumber numberWithInt:i]; // 1 NSString *str = [NSString stringWithFormat:@"%d ", i]; // 2 //Use num and str...whatever... strTest = [NSString stringWithFormat:@"%@%@", num, str]; // 3 if (i % kStep == 0) { [_memoryUsageList1 addObject:@(getMemoryUsage())]; // getM方法是獲取內存的函數 NSLog(@"000----%f", getMemoryUsage()); } } } }); } // 4
double getMemoryUsage(void) { struct task_basic_info info; mach_msg_type_number_t size = sizeof(info); kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size); double memoryUsageInMB = kerr == KERN_SUCCESS ? (info.resident_size / 1024.0 / 1024.0) : 0.0; return memoryUsageInMB; }
讓我們來分析一下當有@autoreleasepool存在的時候,代碼中的1、2、3行中的內存分別是在什么時候釋放。此分析建立在已經有iOS引用計數器概念的基礎上。ARC其實就是編譯器自動幫我們加上了retain,release等,本質還是引用計數器。
1、
1和2是相同的,在堆區分別存在NSNumber的對象和NSString的對象,棧區有對應的指針,num、str指向兩個對象。當一次for循環結束的時候,棧區的兩個指針由系統釋放,此時堆區中的兩個對象的引用計數器為0,所以一次for循環結束的時候,兩個對象釋放。
2、
關鍵是3行,首先分析strTest,每次for循環都會重新覆蓋strTest之前的值,由於strTest是在viewDidLoad函數中,所以當函數結束,也就是如代碼中,4行時候strTest釋放,同時strTest指向的堆中的內存空間也釋放。也就是for循環最后一次的"[NSString stringWithFormat:@"%@%@", num, str]"的值會釋放。
這么看來好像所有的內存都已經釋放了,那么@auto究竟做了什么呢?
重點:注意觀察for循環,每一次for循環,在堆區都會生成一份"[NSString stringWithFormat:@"%@%@", num, str]"的內存,然后最新生成的一份內存會由strTest重新指一次,那么之前那些內存去哪了?我剛開始以為每一次for循環結束時候,之前生成的便會自動釋放。該內存不是通過系統自動創建的,也就是說不是通過 alloc /new/copy/mutablecopy 創建的,所以會注冊到autoreleasepool中 ,作用域無效,系統是無法自動釋放的。這時候是@auto的關鍵了。@autoreleasepool相當於給堆中的內存添加了一個作用域,超過這個作用域堆中的內存就自動釋放。所以每次遍歷完,一份新生成的"[NSString stringWithFormat:@"%@%@", num, str]"作用域結束,自動釋放。
那么,問題又來了。我不加@autoreleasepool,程序一樣運行的很好,沒出現問題。那是因為,系統在每一次的runloop中自動給我們加上了@autoreleasepool,一次runloop結束,系統就會把該循環中的所有需要release的對象清除。之所以在代碼中手動添加一次,是為了防止在某一操作的時候,該操作瞬時占用的內存過大(比如for循環),導致程序瞬時的閃退。
總結:多看大神代碼,那寫你認為很無聊的寫法,可能里面包含這對整個系統更深層次的理解。
但是發現有一些有趣的現象,例如如下的代碼
for (int i = 0; i < 100000; i++) { NSNumber *num = [NSNumber numberWithInt:i]; // 1 NSString *str = [NSString stringWithFormat:@"%d ", i]; //2 NSString *str1 = [NSString stringWithFormat:@"%@", num]; // 3 NSString *str2 = [NSString stringWithFormat:@"%@%@", num, str]; //4 // NSString *str3 = [[NSString alloc] initWithFormat:@"%@%@", num, str]; // ****test**** [NSArray arrayWithObject:@"1234"]; // 5 // if (i % 10000 == 0) { // NSLog(@"000----%f", getMemoryUsage()); // } }
自己個人猜測:
單獨執行代碼1、2,打印的內存不會上升。
執行代碼1和3,內存不會上升
執行代碼 1+2+4 內存飆升。猜測,不同類型合並成string內存管理不一樣。
單獨執行5,內存飆升。
個人理解:[NSNumber numberWithInt:i]和[NSString stringWithFormat:@"%@", num]等,並沒有發送autorelease消息,也就是沒將他們放到autoreleasepool中。所以在代碼快結束的時候他們就已經銷毀了,但是某些特殊的,如[NSArray arrayWithObject:@"1234"]; 會添加到pool中,由於處於autoreleasepool中的對象延遲執行(所在runloop完畢),所以代碼塊結束時候,runloop並未結束,導致無法釋放。
總結:當遇到大量的for循環時候,盡量使用@autoreleasepool來避免內存的延遲釋放導致瞬時內存飆升。蘋果推薦以下三中情況使用@autoreleasepool代碼快
如果你編寫的程序不是基於 UI 框架的,比如說命令行工具;
如果你編寫的循環中創建了大量的臨時對象;(常用)
如果你創建了一個輔助線程。