iOS記憶體管理指北
文章目錄
一.記憶體管理準則
二.屬性記憶體管理修飾符全解析
三.block中的weak和strong
四.weak是怎麼實現的
五.autoreleasepool實現方式
一.記憶體管理準則
OC中使用自動引用計數(ARC)的方式實現記憶體管理,說是自動引用計數,其實遵循的還是iOS5以前的手動引用計數(MRC)的邏輯,不過是編譯器隱式為我們實現了retain,release,autorelease那一套東西。我們先引用《iOS與OS X多執行緒和記憶體管理》中的類比來認識一下什麼是自動引用計數:
假設辦公室裡的照明裝置只有一臺,上班進入辦公室的人需要照明,所以要把燈開啟。而對於下班離開辦公室的人來說,已經不需要照明瞭,所以要把燈關掉。若是很多人上下班,每個人都開燈或是關燈,就會造成最早下班的人關了燈,辦公室裡還沒走的人處於一片黑暗之中的情況。解決這一問題的辦法是使辦公室在還有至少1人的情況下保持開燈狀態,在無人時保持關燈狀態。為判斷是否還有人在辦公室,這裡匯入計數功能來計算“需要照明的人數”:
1.第一個人進入辦公室,“需要照明人數”加1,計數值從0變成1,因此要開燈。
2.之後每當有人進入辦公室,“需要照明的人數”就加1...
3.每當有人下班離開辦公室,“需要照明的人數”就減1...
4.最後一個人下班離開辦公室時,“需要照明的人數”減1,計數值從1變成0,因此需要關燈。
這樣就能在不需要照明的時候保持關燈狀態,辦公室中僅有的照明裝置得到了很好的管理。那麼OC中的物件就好比辦公室的照明裝置,當建立某個物件的時候,其引用計數由0變1,當增加強引用指向時,計數加1;強引用不再指向該物件時,計數減1;當引用計數變為0時,說明當前物件已經沒有人需要了。那麼物件銷燬,系統回收記憶體。
記憶體管理準則總結起來就下面4條:
- 自己生成的物件,自己所持有
- 非自己生成的物件,自己也能持有
- 不再需要自己持有的物件時釋放
- 非自己持有的物件無法釋放
物件操作與Objective-C方法的對應
物件操作 | OC方法 |
---|---|
生成並持有物件 | alloc/new/copy/mutableCopy 方法 |
持有物件 | retain 方法 |
釋放物件 | release 方法 |
這些有關Objective-C記憶體管理的方法,實際上不包括在該語言中,而是包含在Cocoa框架中用於OS X,iOS應用開發。Cocoa框架中Foundation框架類庫的NSObject類擔負記憶體管理的職責。上述的 alloc/retain/release/dealloc
方法分別指代NSObject類的 alloc
類方法, retain
例項方法, release
例項方法和 dealloc
例項方法。
平時我們使用一個例項物件的時候一般都像這樣:
- (void)test { //自己生成並持有物件 id obj = [[NSObject alloc] init]; 。。。 //編譯器自動新增 // [obj release]; }
實際上是編譯器在test方法結束之前,自動給我們添加了 [obj release]
這行程式碼。其實該方法的實現邏輯就是將obj物件的引用計數減1,然後檢查引用計數是否為零,如果為零,則呼叫 [obj dealloc]
。關於 retain
, release
,和 dealloc
方法的實現,後面會具體講到。
非自己生成的物件,自己也能持有是什麼情況呢?比如我們常用的類方法建立例項物件:
- (void)test { //取得物件的存在,但自己不持有物件 id obj = [NSMutableArray array]; //編譯器自動新增 //自己持有物件 //[obj retain]; ... ... //編譯器自動新增 //釋放物件 //[obj release]; }
使用 alloc/retain/release/dealloc
以外的方法獲得的物件,都不是自己持有的,編譯器會為我們新增 retain
方法(引用計數+1),以持有物件,保證在 test
方法範圍內該物件一直存在。最後在 test
方法結束之前,還需要呼叫 release
釋放該物件。當然這只是大體的意思,實際編譯器針對成對出現的 retain/release
會有優化策略,這裡先不展開說了。
其實說到這裡,記憶體管理的基本原則大概已經說完了,總結起來就是:當建立一個例項物件的時候將其引用計數初始化為1,如果有其他強引用指向的話(實際呼叫了 retain
方法),引用計數加1;強引用取消的話(實際呼叫 release
方法),引用計數減1;每次減少引用計數都會去檢查該物件的引用計數是否為零,如果為零,則內部呼叫dealloc方法,析構物件,回收記憶體。關於屬性的記憶體管理,請看第二部分。
二.屬性記憶體管理修飾符全解析
屬性的修飾符分為記憶體管理(strong/weak/assign/copy),讀寫許可權(readwrite/readonly),是否原子性(atomic/nonatomic),getter方法(getter=method)四類。這一節主要分析一下記憶體管理語義。
strongstrong修飾符表示指向並持有該物件,即所謂的強引用,當某個物件有強引用指向時,其引用計數加1。一般都是用來修飾物件型別。
weakweak 修飾符指向但是並不持有該物件,即所謂的弱引用,引用計數也不會加1。在 Runtime 中對該屬性進行了相關操作,當指向的物件銷燬時,所有的弱引用可以自動置空(如何實現的請看第五節)。weak用來修飾物件,多用於避免迴圈引用的地方,最常見的就是delegate屬性使用該修飾符。weak 不可以修飾基本資料型別。
assign主要用於修飾基本資料型別,
例如NSInteger,CGFloat,儲存在棧中,記憶體不用程式員管理。assign是可以修飾物件的,跟weak的區別就是,當指向的物件銷燬時,assign修飾的指標不會自動置空,容易引起野指標問題。
copycopy關鍵字和 strong類似,都是強引用指向物件。copy除了用來修飾block外, 多用於修飾有可變型別的不可變物件,如NSString,NSArray,NSDictionary上,保證封裝性。這個問題用測試程式碼比較好說明。
@interface ViewController () @property (nonatomic, strong) NSString *testString; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSMutableString *ms = [NSMutableString stringWithString:@"test"]; self.testString = ms; NSLog(@">>>>>%@",self.testString); 。。。 。。。 [ms appendString:@"hello"]; NSLog(@">>>>>%@",self.testString); } @end
執行列印結果:
2018-11-14 11:44:17.391568+0800 ZZTest[4330:96375] >>>>>test 2018-11-14 11:44:17.391698+0800 ZZTest[4330:96375] >>>>>testhello
如果用strong修飾NSString,賦值的是一個NSMutableString物件,如果該物件後續有修改,會影響到testString,這可能並不是我想要的結果。如果換成copy修飾的話就可以避免這個問題,因為testString指向的是一個全新的副本,原物件的修改對它不會有任何影響,測試程式碼為證。
@interface ViewController () @property (nonatomic, copy) NSString *testString; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSMutableString *ms = [NSMutableString stringWithString:@"test"]; self.testString = ms; NSLog(@">>>>>%@",self.testString); [ms appendString:@"hello"]; NSLog(@">>>>>%@",self.testString); NSLog(@">>>>>%@",ms); } @end
列印結果:
2018-11-14 11:53:29.309521+0800 ZZTest[4510:105814] >>>>>test 2018-11-14 11:53:29.309636+0800 ZZTest[4510:105814] >>>>>test 2018-11-14 11:53:29.309700+0800 ZZTest[4510:105814] >>>>>testhello
所以引申一下copy關鍵字的一個作用就是多用於修飾 有可變型別的不可變物件 。
三.block中的weak和strong
關於block中的 __weak
和 __strong
轉換,相信用到block的地方都少不了要注意他們的使用。就像SDWebImage中隨意截出來的一段程式碼一樣:
//摘自SDWebImage __weak __typeof(self)wself = self; SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { wself.sd_imageProgress.totalUnitCount = expectedSize; wself.sd_imageProgress.completedUnitCount = receivedSize; if (progressBlock) { progressBlock(receivedSize, expectedSize, targetURL); } }; id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { __strong __typeof (wself) sself = wself; if (!sself) { return; } ... }];
其實關於block的強弱引用轉換,在我之前的解讀SDWebImage原始碼的文章中就提過一次。不過這次是專門的記憶體管理篇,block的 __weak
, __strong
不得不提:
- 1.先weak後strong到底會不會增加引用計數?
- 2.如果會增加引用計數,那麼跟直接使用strong有什麼不同?
回答第一個問題之前,我們可以用程式碼測試一下:
@interface ViewController () { __weak typeof(NSObject *) _obj; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self test]; NSLog(@">>>>%@", _obj); } - (void)test { NSObject *obj = [[NSObject alloc] init]; _obj = obj; NSLog(@">>>>%@", _obj); } @end
列印結果如下:
2018-11-12 19:20:55.447117+0800 ZZTest[13659:492278] >>>><NSObject: 0x6000009030d0> 2018-11-12 19:20:55.447220+0800 ZZTest[13659:492278] >>>>(null)
因為 test
方法中建立的自動變數obj在方法的{}之內是有效的,所以第一個列印有值;出了 test
方法後,obj只有弱引用指向,所以被釋放了。第二個列印為null,這個是很好理解的。
接下來將程式碼稍作修改,如下:
@interface ViewController () { __strong typeof(NSObject *) _obj; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self test]; NSLog(@">>>>%@", _obj); } - (void)test { NSObject *obj = [[NSObject alloc] init]; __weak typeof(NSObject *)weakObj = obj; _obj = weakObj; NSLog(@">>>>%@", _obj); } @end
列印結果:
2018-11-12 19:31:06.321660+0800 ZZTest[13856:504099] >>>><NSObject: 0x6000035c4a00> 2018-11-12 19:31:06.321828+0800 ZZTest[13856:504099] >>>><NSObject: 0x6000035c4a00>
結果很好的回答了上面的第一個問題,先weak後strong引用一個物件,會增加該物件的引用計數。那麼既然轉了一圈還是會增加引用計數,為啥還要“多此一舉”呢?其實這就涉及到block的實現原理了,我們知道block會捕獲其定義時使用的自動變數。如果block定義時直接使用當前物件的話,那麼它捕獲的就是預設 __strong
修飾的物件,而先將其用 __weak
轉一下的話,它捕獲的就是物件的弱引用,那麼這就打破了所謂的引用迴圈,避免了記憶體洩漏。
既然弱引避免了記憶體洩漏,那麼block內部的 __strong
轉換又是什麼目的呢?其實這樣再轉換一次,就是為了增加物件的引用計數,避免其被提前釋放(尤其在多執行緒切換時),否則後續的訪問會出現野指標錯誤!那麼一句話回答上面兩個問題就是:先weak是為了打破引用迴圈,避免記憶體洩漏;後strong是為了保證在block內部該物件一直存在,避免野指標錯誤。
四.weak是怎麼實現的
前面說到當有強引用指向某物件時,該物件的引用計數加1,當強引用取消時,引用計數減1;那麼底層是怎麼實現計數的加1減1呢?還有 weak
修飾的屬性,當指向的物件被釋放時,該指標會自動置空,這又是怎麼實現的呢?
為了管理所有物件的引用計數和weak指標,蘋果建立了一個全域性的SideTables,它是一個全域性Hash表,裡面裝的都是SideTable結構體。其定義在NSObject.mm的原始碼中:
struct SideTable { spinlock_t slock; RefcountMap refcnts; weak_table_t weak_table; SideTable() { memset(&weak_table, 0, sizeof(weak_table)); } ~SideTable() { _objc_fatal("Do not delete SideTable."); } void lock() { slock.lock(); } void unlock() { slock.unlock(); } void forceReset() { slock.forceReset(); } // Address-ordered lock discipline for a pair of side tables. template<HaveOld, HaveNew> static void lockTwo(SideTable *lock1, SideTable *lock2); template<HaveOld, HaveNew> static void unlockTwo(SideTable *lock1, SideTable *lock2); };
可以看到SideTable有三個成員變數:
1.一把自旋鎖 spinlock_t slock
百度百科是這麼解釋的:“何謂自旋鎖?它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。”
自旋鎖適用於鎖使用者保持鎖時間比較短的情況,對於引用計數的操作速度其實是非常快的,所以這裡使用自旋鎖恰到好處。
2.引用計數器 RefcountMap refcnts
RefcountMap的定義是這樣的
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
其實就是個C++的Map,那麼這個Map裡面儲存的又是什麼呢?從這裡可以看到:
id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; table.lock(); size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; }
那麼 size_t
的定義是 typedef __darwin_size_t size_t;
,再進一步看它的定義是unsigned long,在32位和64位作業系統中,它分別佔用32和64個bit。這裡使用的是bit mask技術。在SideTable結構體定義的上面,定義了這麼幾個數:
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) #define SIDE_TABLE_DEALLOCATING(1UL<<1)// MSB-ward of weak bit #define SIDE_TABLE_RC_ONE(1UL<<2)// MSB-ward of deallocating bit #define SIDE_TABLE_RC_PINNED(1UL<<(WORD_BITS-1))
1UL<<0
的意思是將“1”放到最右側,然後左移0位(就是原地不動),以32位為例的話就是:0b0000 0000 0000 0000 0000 0000 0000 0001,同理 1UL<<1
就是:0b0000 0000 0000 0000 0000 0000 0000 0010。上面的定義其實可以這樣理解:一個32位的數,其右邊第一位SIDE_TABLE_WEAKLY_REFERENCED表示是否有弱引用指向這個物件,如果為1的話,在物件釋放的時候需要把所有指向它的弱引用都置為nil;右邊第二位SIDE_TABLE_DEALLOCATING表示物件是否正在釋放,1正在釋放,0沒有;左邊第一位即最高位SIDE_TABLE_RC_PINNED,其實沒有特殊的含義,就是隨著物件的引用計數不斷變大,如果這一位都變成1了,表示引用計數已經達到了能夠儲存的最大值。最後SIDE_TABLE_RC_ONE其實定義的就是增加一個引用計數,size_t實際增加的值,因為末尾兩位是被佔用的,所以引用計數加1,size_t實際加的是4。
3.維護weak指標的結構體 weak_table_t weak_table
weak_table_t定義在objc-weak.h檔案中:
struct weak_table_t { weak_entry_t *weak_entries; size_tnum_entries; uintptr_t mask; uintptr_t max_hash_displacement; };
weak_entries是一個數組,num_entries用來維護陣列始終有一個合適的size,比如當陣列中的元素數量超過3/4時,將陣列大小乘以2。
weak_entry_t也定義在objc-weak.h中:
#define WEAK_INLINE_COUNT 4 struct weak_entry_t { DisguisedPtr<objc_object> referent; union { struct { weak_referrer_t *referrers; uintptr_tout_of_line_ness : 2; uintptr_tnum_refs : PTR_MINUS_2; uintptr_tmask; uintptr_tmax_hash_displacement; }; struct { weak_referrer_tinline_referrers[WEAK_INLINE_COUNT]; }; }; }
其中三個成員比較重要:referent,被指物件的地址;referrers,可變陣列,裡面儲存著所有指向這個物件的弱引用的地址,如果弱引用指標超過4個的話,將會存在這個陣列中;inline_referrers,只有4個元素的陣列,預設情況下用它儲存弱引用的指標,超過4個的時候儲存到referrers中。
先總結一下SideTables的資料結構,如下圖所示:

sidetable結構圖解.jpg
接著再梳理一下流程,當系統呼叫 retain
方法時,最終呼叫的是NSObject.mm中的這個方法:
id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; table.lock(); size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; }
即取到對應SideTable的refcnts,然後以當前物件地址為key,找到real count,將其增加 SIDE_TABLE_RC_ONE
,相應的引用計數就加了1。
當系統呼叫 release
方法時,最終呼叫的是NSObject.mm中的這個方法:
uintptr_t objc_object::sidetable_release(bool performDealloc) { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; bool do_dealloc = false; table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it == table.refcnts.end()) { do_dealloc = true; table.refcnts[this] = SIDE_TABLE_DEALLOCATING; } else if (it->second < SIDE_TABLE_DEALLOCATING) { // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it. do_dealloc = true; it->second |= SIDE_TABLE_DEALLOCATING; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second -= SIDE_TABLE_RC_ONE; } table.unlock(); if (do_dealloc&&performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return do_dealloc; }
release相比retain多了最終是否需要呼叫dealloc的判斷,大概邏輯是1.遍歷變數是否存在,如果不存在就將do_dealloc置為true;2.如果存在再判斷是否小於 SIDE_TABLE_DEALLOCATING
,如果小於也將do_dealloc置為true;3.否則就減去前面說過的 SIDE_TABLE_RC_ONE
;4.判斷是否需要實際呼叫dealloc。
呼叫了 dealloc
方法後,最終會呼叫到sidetable_clearDeallocating方法:
void objc_object::sidetable_clearDeallocating() { SideTable& table = SideTables()[this]; // clear any weak table items // clear extra retain count and deallocating bit // (fixme warn or abort if extra retain count == 0 ?) table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it != table.refcnts.end()) { if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { weak_clear_no_lock(&table.weak_table, (id)this); } table.refcnts.erase(it); } table.unlock(); }
這裡加了遍歷有值 和 存在弱引用 兩個判斷條件,如果滿足的話就會呼叫 weak_clear_no_lock
方法,其定義在objc-weak.mm檔案中:
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { objc_object *referent = (objc_object *)referent_id; weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { /// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } // zero out references weak_referrer_t *referrers; size_t count; if (entry->out_of_line()) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); }
會先判斷最後的遍歷陣列是referrers陣列取還是最大容量為4的inline_referrers陣列,在這一步,將每一個weak指標置為了nil。
五.autoreleasepool實現方式
在大部分情況下,我們不需要手動提供autoreleasepool,因為從每個App的入口main函式可以看到,系統預設用了一個自動釋放池將我們的程式碼包含。即所有在主執行緒建立的非自己持有的物件都新增到了這個autoreleasepool裡面。但是我們知道主執行緒是預設開啟runloop的,runloop往簡單了說就是一個do while 迴圈,那麼只要這個迴圈還在執行,main函式裡面的autoreleasepool就沒辦法走到後面這個花括號 }
,那這個自動釋放池到底什麼時候釋放呢?答案是當前runloop迭代結束的時候釋放,因為系統在每個runloop迭代中都加入了autoreleasepool的 push
和 pop
。具體原理可以深究runloop原始碼。
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
文章開頭也說了,在ARC環境下,以alloc/new/copy/mutableCopy開頭的方法的返回值取得的物件是自己持有的,其他情況下便是取得非自己持有的物件,此時物件的持有者就是autoreleasepool。我們可以用以下程式碼來驗證一下:
#import <Foundation/Foundation.h> @interface MyObject : NSObject + (id)testObject; @end @implementation MyObject + (id)testObject { id obj = [[MyObject alloc] init]; return obj; } + (id)allocObject { id obj = [[MyObject alloc] init]; return obj; } @end extern void _objc_autoreleasePoolPrint (); int main(int argc, char * argv[]) { __weak id a; @autoreleasepool { a = [MyObject testObject]; //a = [MyObject allocObject]; _objc_autoreleasePoolPrint(); NSLog(@"in:%@",a); } NSLog(@"out:%@",a); }
需要說明的是,其中的 _objc_autoreleasePoolPrint
方法是非公開的除錯方法,需要宣告是外部實現的,否則無法使用。執行列印的結果如下:

autoreleasepool列印結果1.jpg
可以看到autoreleasepool持有了物件TestObject,這也驗證了生成非自己持有的物件,其真正的持有者是autoreleasepool這一說法。我們將 a = [MyObject testObject];
這行註釋,開啟下面一行,執行列印結果如下:

autoreleasepool列印結果2.jpg
可以看到這一次autoreleasepool並沒有持有TestObject物件,說明以alloc開頭的方法生成的物件是自己持有的。而且,由於a是 __weak
修飾的,返回的物件由於無人持有,賦值以後立即被釋放掉了;所以 in:
後面列印就是null了。同時編譯器已經給出了警告:warning:Assigning retained object to weak variable; object will be released after assignment。應用autoreleasepool這一特性,可以在我們的專案中for in遍歷處理大量物件的時候,在迴圈體內部用autoreleasepool將程式碼包含,降低應用記憶體峰值,類似這樣:
摘自 SDWebImageCoderHelper.m for (size_t i = 0; i < frameCount; i++) { @autoreleasepool { SDWebImageFrame *frame = frames[i]; float frameDuration = frame.duration; CGImageRef frameImageRef = frame.image.CGImage; NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}}; CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); } }
當然,按照 ofollow,noindex">sunnyxx這篇文章 最後提到的一個知識點,使用容器的block版本的列舉器時,內部會自動新增一個autoreleasepool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { // 這裡被一個區域性@autoreleasepool包圍著 }];
由於筆者寫了測試程式碼,用“clang -rewrite-objc”命令重寫為C++實現後,並沒有找到block版本列舉器內部會自動新增autoreleasepool的蛛絲馬跡;同時也查看了幫助文件,這個方法的說明也沒有提到相關事項。還望知道怎麼得出這個結論的朋友指點一下。
那麼autoreleasepool的內部實現是怎麼樣的呢?可以隨便寫段測試程式碼,用“clang -rewrite-objc”命令重寫為C++一探究竟。測試程式碼如下:
@implementation ZZTestObject - (void)test { NSArray *arr = @[@"1", @"one", @"2", @"two", @"three", @"3"]; for (NSString *str in arr) { @autoreleasepool { NSLog(@">>>>>>>>>>%@", str); } } }
終端cd到ZZTestObject.m這一層,執行命令“clang -rewrite-objc ZZTestObject.m”,就會得到一個ZZTestObject.cpp檔案,開啟後全域性搜尋 @implementation ZZTestObject
,可以看到這段程式碼:
// @implementation ZZTestObject static void _I_ZZTestObject_test(ZZTestObject * self, SEL _cmd) { NSArray *arr = ((NSArray *(*)(Class, SEL, ObjectType_Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(6U, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_2, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_3, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_4, (NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_5).arr, 6U); { NSString * str; struct __objcFastEnumerationState enumState = { 0 }; id __rw_items[16]; id l_collection = (id) arr; _WIN_NSUInteger limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend) ((id)l_collection, sel_registerName("countByEnumeratingWithState:objects:count:"), &enumState, (id *)__rw_items, (_WIN_NSUInteger)16); if (limit) { unsigned long startMutations = *enumState.mutationsPtr; do { unsigned long counter = 0; do { if (startMutations != *enumState.mutationsPtr) objc_enumerationMutation(l_collection); str = (NSString *)enumState.itemsPtr[counter++]; { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_6, str); } }; __continue_label_1: ; } while (counter < limit); } while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend) ((id)l_collection, sel_registerName("countByEnumeratingWithState:objects:count:"), &enumState, (id *)__rw_items, (_WIN_NSUInteger)16))); str = ((NSString *)0); __break_label_1: ; } else str = ((NSString *)0); } } // @end
我們注意這一行:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_04__vckj48s04bgf4ttzttv3w2w0000gn_T_ZZTestObject_dc0ae2_mi_6, str); }
我們再全域性搜尋一下 __AtAutoreleasePool
,最終會找到這裡:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void); extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *); struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };
發現autoreleasepool最終會變成 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
兩個方法的呼叫,這裡顯示這兩個方法是外部定義的,那麼我們去哪裡找這兩個方法的實現呢?答案是runtime原始碼!
這裡說一下怎麼下載runtime原始碼:先開啟這個網址 https://opensource.apple.com/ ,然後選擇你電腦對應的macOS版本,目前我電腦是10.13.6,然後com+F搜尋objc4,我這裡搜到的是objc4-723,點選下載。開啟之後的目錄是這樣的:

objc原始碼目錄.jpg
我們開啟NSObject.mm檔案檢視,全域性搜尋 objc_autoreleasePoolPush
,發現是這樣:
void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); }
那就直接看AutoreleasePoolPage這個類的實現:
class AutoreleasePoolPage { // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is // pushed and it has never contained any objects. This saves memory // when the top level (i.e. libdispatch) pushes and pops pools but // never uses them. #define EMPTY_POOL_PLACEHOLDER ((id*)1) #define POOL_BOUNDARY nil static pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3;// 0xA3A3A3A3 after releasing static size_t const SIZE = #if PROTECT_AUTORELEASEPOOL PAGE_MAX_SIZE;// must be multiple of vm page size #else PAGE_MAX_SIZE;// size and alignment, power of 2 #endif static size_t const COUNT = SIZE / sizeof(id); magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; ... }
AutoreleasePoolPage是一個C++實現的類:
- AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)
- AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
- AutoreleasePoolPage每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址
- id *next指標作為遊標指向棧頂最新add進來的autorelease物件的下一個位置
- 一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入
我們注意這一行:
#define POOL_BOUNDARY nil
定義了一個POOL_BOUNDARY的巨集,值為nil,待會會用到。
看看AutoreleasePoolPage的push方法是怎麼實現的:
static inline void *push() { id *dest; if (DebugPoolAllocation) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }
由於原始碼太多,就不一一貼碼了。總結流程圖如下:

autoreleasepoolpage流程圖.jpg
首先會判斷DebugPoolAllocation標誌位,是否需要為每個pool都生成一個新page,為真就走autoreleaseNewPage方法,否則,執行autoreleaseFast方法.
在autoreleaseFast方法中,如果存在page且未滿,則直接新增;
如果不存在page,會響應autoreleaseNoPage;
如果當前page已滿,則響應autoreleaseFullPage方法;
autoreleaseNoPage和autoreleaseFullPage會生成新的page,然後向該page中新增物件.
而autoreleaseNewPage方法,如果當前存在page,則執行autoreleaseFullPage方法,否則響應autoreleaseNoPage方法,然後就同上了,去執行新增方法。那麼具體怎麼樣新增呢?
每當進行一次push呼叫時,runtime向當前的AutoreleasePoolPage中add進一個哨兵物件,即前面說的POOL_BOUNDARY巨集,值為0(也就是個nil),那麼這一個page就變成了下面的樣子:

pooladd.jpg
objc_autoreleasePoolPush 的返回值就是這個哨兵物件的地址,同時當作 objc_autoreleasePoolPop 的入參:
- 根據傳入的哨兵物件地址找到哨兵物件所處的page
- 在當前page中,將晚於哨兵物件插入的所有autorelease物件都發送一次- release訊息,並向回移動next指標到正確位置
- kill掉空page
pop之後就變成了這樣:

pool_pop.jpg
@autoreleasepool {}
。編譯器會將其轉為push和pop兩個操作,中間是我們自己的業務邏輯。push時是向AutoReleasePoolPage新增一個值為nil的哨兵物件,並作為該方法的返回值,也是pop方法的入參。pop時根據哨兵物件的地址獲取到當前page,然後在當前page中,將晚於哨兵物件新增的物件都發送一次release命令,並更新next指標位置,最後kill掉空page。autoreleasepool允許多層巢狀,邏輯如上,不過是一個個的套娃,一層層的剝離罷了。
結語:記憶體管理是iOS開發或者面試永遠繞不開的一個坎兒,想要完全跨越它,必須一步一個腳印,慢慢攻克。理解這些原理性的東西,實際程式設計的時候才有理論指導,不至於兩眼一抹黑。路漫漫其修遠兮,吾將上下而求索。。。

求索.jpg