文章目錄
一.內(nèi)存管理準則
二.屬性內(nèi)存管理修飾符全解析
三.block中的weak和strong
四.weak是怎么實現(xiàn)的
五.autoreleasepool實現(xiàn)方式
一.內(nèi)存管理準則
OC中使用自動引用計數(shù)(ARC)的方式實現(xiàn)內(nèi)存管理扎谎,說是自動引用計數(shù)作儿,其實遵循的還是iOS5以前的手動引用計數(shù)(MRC)的邏輯淑履,不過是編譯器隱式為我們實現(xiàn)了retain量瓜,release岩遗,autorelease那一套東西纳令。我們先引用《iOS與OS X多線程和內(nèi)存管理》中的類比來認識一下什么是自動引用計數(shù):
假設(shè)辦公室里的照明設(shè)備只有一臺香拉,上班進入辦公室的人需要照明穗熬,所以要把燈打開祭务。而對于下班離開辦公室的人來說内狗,已經(jīng)不需要照明了怪嫌,所以要把燈關(guān)掉。若是很多人上下班柳沙,每個人都開燈或是關(guān)燈岩灭,就會造成最早下班的人關(guān)了燈,辦公室里還沒走的人處于一片黑暗之中的情況赂鲤。解決這一問題的辦法是使辦公室在還有至少1人的情況下保持開燈狀態(tài)噪径,在無人時保持關(guān)燈狀態(tài)。為判斷是否還有人在辦公室数初,這里導(dǎo)入計數(shù)功能來計算“需要照明的人數(shù)”:
1.第一個人進入辦公室找爱,“需要照明人數(shù)”加1,計數(shù)值從0變成1泡孩,因此要開燈车摄。
2.之后每當有人進入辦公室,“需要照明的人數(shù)”就加1...
3.每當有人下班離開辦公室仑鸥,“需要照明的人數(shù)”就減1...
4.最后一個人下班離開辦公室時吮播,“需要照明的人數(shù)”減1,計數(shù)值從1變成0眼俊,因此需要關(guān)燈意狠。
這樣就能在不需要照明的時候保持關(guān)燈狀態(tài),辦公室中僅有的照明設(shè)備得到了很好的管理疮胖。那么OC中的對象就好比辦公室的照明設(shè)備环戈,當創(chuàng)建某個對象的時候,其引用計數(shù)由0變1澎灸,當增加強引用指向時谷市,計數(shù)加1;強引用不再指向該對象時击孩,計數(shù)減1迫悠;當引用計數(shù)變?yōu)?時,說明當前對象已經(jīng)沒有人需要了巩梢。那么對象銷毀创泄,系統(tǒng)回收內(nèi)存。
內(nèi)存管理準則總結(jié)起來就下面4條:
- 自己生成的對象括蝠,自己所持有
- 非自己生成的對象鞠抑,自己也能持有
- 不再需要自己持有的對象時釋放
- 非自己持有的對象無法釋放
對象操作與Objective-C方法的對應(yīng)
對象操作 | OC方法 |
---|---|
生成并持有對象 | alloc/new/copy/mutableCopy 方法 |
持有對象 | retain 方法 |
釋放對象 | release 方法 |
這些有關(guān)Objective-C內(nèi)存管理的方法,實際上不包括在該語言中忌警,而是包含在Cocoa框架中用于OS X搁拙,iOS應(yīng)用開發(fā)秒梳。Cocoa框架中Foundation框架類庫的NSObject類擔負內(nèi)存管理的職責。上述的alloc/retain/release/dealloc
方法分別指代NSObject類的alloc
類方法箕速,retain
實例方法酪碘,release
實例方法和dealloc
實例方法。
平時我們使用一個實例對象的時候一般都像這樣:
- (void)test {
//自己生成并持有對象
id obj = [[NSObject alloc] init];
盐茎。兴垦。。
//編譯器自動添加
// [obj release];
}
實際上是編譯器在test方法結(jié)束之前字柠,自動給我們添加了[obj release]
這行代碼探越。其實該方法的實現(xiàn)邏輯就是將obj對象的引用計數(shù)減1,然后檢查引用計數(shù)是否為零窑业,如果為零钦幔,則調(diào)用[obj dealloc]
。關(guān)于retain
常柄,release
,和dealloc
方法的實現(xiàn)鲤氢,后面會具體講到。
非自己生成的對象拐纱,自己也能持有是什么情況呢铜异?比如我們常用的類方法創(chuàng)建實例對象:
- (void)test {
//取得對象的存在,但自己不持有對象
id obj = [NSMutableArray array];
//編譯器自動添加
//自己持有對象
//[obj retain];
...
...
//編譯器自動添加
//釋放對象
//[obj release];
}
使用alloc/retain/release/dealloc
以外的方法獲得的對象秸架,都不是自己持有的揍庄,編譯器會為我們添加retain
方法(引用計數(shù)+1),以持有對象东抹,保證在test
方法范圍內(nèi)該對象一直存在蚂子。最后在test
方法結(jié)束之前,還需要調(diào)用release
釋放該對象缭黔。當然這只是大體的意思食茎,實際編譯器針對成對出現(xiàn)的retain/release
會有優(yōu)化策略,這里先不展開說了馏谨。
其實說到這里别渔,內(nèi)存管理的基本原則大概已經(jīng)說完了,總結(jié)起來就是:當創(chuàng)建一個實例對象的時候?qū)⑵湟糜嫈?shù)初始化為1惧互,如果有其他強引用指向的話(實際調(diào)用了retain
方法)哎媚,引用計數(shù)加1;強引用取消的話(實際調(diào)用release
方法)喊儡,引用計數(shù)減1拨与;每次減少引用計數(shù)都會去檢查該對象的引用計數(shù)是否為零,如果為零艾猜,則內(nèi)部調(diào)用dealloc方法买喧,析構(gòu)對象捻悯,回收內(nèi)存。關(guān)于屬性的內(nèi)存管理淤毛,請看第二部分今缚。
二.屬性內(nèi)存管理修飾符全解析
屬性的修飾符分為內(nèi)存管理(strong/weak/assign/copy),讀寫權(quán)限(readwrite/readonly)钱床,是否原子性(atomic/nonatomic)荚斯,getter方法(getter=method)四類埠居。這一節(jié)主要分析一下內(nèi)存管理語義查牌。
strong
strong修飾符表示指向并持有該對象,即所謂的強引用滥壕,當某個對象有強引用指向時纸颜,其引用計數(shù)加1。一般都是用來修飾對象類型绎橘。
weak
weak 修飾符指向但是并不持有該對象胁孙,即所謂的弱引用,引用計數(shù)也不會加1称鳞。在 Runtime 中對該屬性進行了相關(guān)操作涮较,當指向的對象銷毀時,所有的弱引用可以自動置空(如何實現(xiàn)的請看第五節(jié))冈止。weak用來修飾對象狂票,多用于避免循環(huán)引用的地方,最常見的就是delegate屬性使用該修飾符熙暴。weak 不可以修飾基本數(shù)據(jù)類型闺属。
assign
assign主要用于修飾基本數(shù)據(jù)類型,
例如NSInteger周霉,CGFloat掂器,存儲在棧中,內(nèi)存不用程序員管理俱箱。assign是可以修飾對象的国瓮,跟weak的區(qū)別就是,當指向的對象銷毀時狞谱,assign修飾的指針不會自動置空乃摹,容易引起野指針問題。
copy
copy關(guān)鍵字和 strong類似芋簿,都是強引用指向?qū)ο笙啃浮opy除了用來修飾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
運行打印結(jié)果:
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對象辩蛋,如果該對象后續(xù)有修改,會影響到testString移盆,這可能并不是我想要的結(jié)果悼院。如果換成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
打印結(jié)果:
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關(guān)鍵字的一個作用就是多用于修飾有可變類型的不可變對象叙甸。
三.block中的weak和strong
關(guān)于block中的__weak
和__strong
轉(zhuǎn)換颖医,相信用到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; }
...
}];
其實關(guān)于block的強弱引用轉(zhuǎn)換裆蒸,在我之前的解讀SDWebImage源碼的文章中就提過一次熔萧。不過這次是專門的內(nèi)存管理篇,block的__weak
光戈,__strong
不得不提:
- 1.先weak后strong到底會不會增加引用計數(shù)哪痰?
- 2.如果會增加引用計數(shù),那么跟直接使用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
打印結(jié)果如下:
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
方法中創(chuàng)建的自動變量obj在方法的{}之內(nèi)是有效的,所以第一個打印有值筷弦;出了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
打印結(jié)果:
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>
結(jié)果很好的回答了上面的第一個問題梗夸,先weak后strong引用一個對象,會增加該對象的引用計數(shù)号醉。那么既然轉(zhuǎn)了一圈還是會增加引用計數(shù)反症,為啥還要“多此一舉”呢辛块?其實這就涉及到block的實現(xiàn)原理了,我們知道block會捕獲其定義時使用的自動變量铅碍。如果block定義時直接使用當前對象的話润绵,那么它捕獲的就是默認__strong
修飾的對象,而先將其用__weak
轉(zhuǎn)一下的話胞谈,它捕獲的就是對象的弱引用尘盼,那么這就打破了所謂的引用循環(huán),避免了內(nèi)存泄漏烦绳。
既然弱引避免了內(nèi)存泄漏卿捎,那么block內(nèi)部的__strong
轉(zhuǎn)換又是什么目的呢?其實這樣再轉(zhuǎn)換一次爵嗅,就是為了增加對象的引用計數(shù)娇澎,避免其被提前釋放(尤其在多線程切換時)笨蚁,否則后續(xù)的訪問會出現(xiàn)野指針錯誤睹晒!那么一句話回答上面兩個問題就是:先weak是為了打破引用循環(huán),避免內(nèi)存泄漏括细;后strong是為了保證在block內(nèi)部該對象一直存在伪很,避免野指針錯誤。
四.weak是怎么實現(xiàn)的
前面說到當有強引用指向某對象時奋单,該對象的引用計數(shù)加1锉试,當強引用取消時,引用計數(shù)減1览濒;那么底層是怎么實現(xiàn)計數(shù)的加1減1呢呆盖?還有weak
修飾的屬性,當指向的對象被釋放時贷笛,該指針會自動置空应又,這又是怎么實現(xiàn)的呢?
為了管理所有對象的引用計數(shù)和weak指針乏苦,蘋果創(chuàng)建了一個全局的SideTables,它是一個全局Hash表株扛,里面裝的都是SideTable結(jié)構(gòu)體。其定義在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
百度百科是這么解釋的:“何謂自旋鎖汇荐?它是為實現(xiàn)保護共享資源而提出一種鎖機制洞就。其實,自旋鎖與互斥鎖比較類似掀淘,它們都是為了解決對某項資源的互斥使用旬蟋。無論是互斥鎖,還是自旋鎖革娄,在任何時刻倾贰,最多只能有一個保持者秕狰,也就說,在任何時刻最多只能有一個執(zhí)行單元獲得鎖躁染。但是兩者在調(diào)度機制上略有不同鸣哀。對于互斥鎖,如果資源已經(jīng)被占用吞彤,資源申請者只能進入睡眠狀態(tài)我衬。但是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持饰恕,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖挠羔,"自旋"一詞就是因此而得名÷袂叮”
自旋鎖適用于鎖使用者保持鎖時間比較短的情況破加,對于引用計數(shù)的操作速度其實是非常快的雹嗦,所以這里使用自旋鎖恰到好處范舀。
2.引用計數(shù)器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位操作系統(tǒng)中,它分別占用32和64個bit泊藕。這里使用的是bit mask技術(shù)辅辩。在SideTable結(jié)構(gòu)體定義的上面,定義了這么幾個數(shù):
#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”放到最右側(cè)娃圆,然后左移0位(就是原地不動)玫锋,以32位為例的話就是:0b0000 0000 0000 0000 0000 0000 0000 0001,同理1UL<<1
就是:0b0000 0000 0000 0000 0000 0000 0000 0010讼呢。上面的定義其實可以這樣理解:一個32位的數(shù)撩鹿,其右邊第一位SIDE_TABLE_WEAKLY_REFERENCED表示是否有弱引用指向這個對象,如果為1的話吝岭,在對象釋放的時候需要把所有指向它的弱引用都置為nil三痰;右邊第二位SIDE_TABLE_DEALLOCATING表示對象是否正在釋放,1正在釋放窜管,0沒有散劫;左邊第一位即最高位SIDE_TABLE_RC_PINNED,其實沒有特殊的含義幕帆,就是隨著對象的引用計數(shù)不斷變大获搏,如果這一位都變成1了,表示引用計數(shù)已經(jīng)達到了能夠存儲的最大值。最后SIDE_TABLE_RC_ONE其實定義的就是增加一個引用計數(shù)常熙,size_t實際增加的值纬乍,因為末尾兩位是被占用的,所以引用計數(shù)加1裸卫,size_t實際加的是4仿贬。
3.維護weak指針的結(jié)構(gòu)體weak_table_t weak_table
weak_table_t定義在objc-weak.h文件中:
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
weak_entries是一個數(shù)組,num_entries用來維護數(shù)組始終有一個合適的size墓贿,比如當數(shù)組中的元素數(shù)量超過3/4時茧泪,將數(shù)組大小乘以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_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
}
其中三個成員比較重要:referent聋袋,被指對象的地址队伟;referrers,可變數(shù)組幽勒,里面保存著所有指向這個對象的弱引用的地址嗜侮,如果弱引用指針超過4個的話,將會存在這個數(shù)組中啥容;inline_referrers锈颗,只有4個元素的數(shù)組,默認情況下用它存儲弱引用的指針干毅,超過4個的時候存儲到referrers中宜猜。
先總結(jié)一下SideTables的數(shù)據(jù)結(jié)構(gòu),如下圖所示:
接著再梳理一下流程硝逢,當系統(tǒng)調(diào)用retain
方法時,最終調(diào)用的是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;
}
即取到對應(yīng)SideTable的refcnts绅喉,然后以當前對象地址為key渠鸽,找到real count,將其增加SIDE_TABLE_RC_ONE
,相應(yīng)的引用計數(shù)就加了1柴罐。
當系統(tǒng)調(diào)用release
方法時徽缚,最終調(diào)用的是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多了最終是否需要調(diào)用dealloc的判斷,大概邏輯是1.遍歷變量是否存在革屠,如果不存在就將do_dealloc置為true凿试;2.如果存在再判斷是否小于SIDE_TABLE_DEALLOCATING
,如果小于也將do_dealloc置為true;3.否則就減去前面說過的SIDE_TABLE_RC_ONE
似芝;4.判斷是否需要實際調(diào)用dealloc那婉。
調(diào)用了dealloc
方法后,最終會調(diào)用到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();
}
這里加了遍歷有值 和 存在弱引用 兩個判斷條件党瓮,如果滿足的話就會調(diào)用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);
}
會先判斷最后的遍歷數(shù)組是referrers數(shù)組取還是最大容量為4的inline_referrers數(shù)組,在這一步寞奸,將每一個weak指針置為了nil呛谜。
五.autoreleasepool實現(xiàn)方式
在大部分情況下在跳,我們不需要手動提供autoreleasepool,因為從每個App的入口main函數(shù)可以看到,系統(tǒng)默認用了一個自動釋放池將我們的代碼包含隐岛。即所有在主線程創(chuàng)建的非自己持有的對象都添加到了這個autoreleasepool里面猫妙。但是我們知道主線程是默認開啟runloop的,runloop往簡單了說就是一個do while 循環(huán)聚凹,那么只要這個循環(huán)還在執(zhí)行吐咳,main函數(shù)里面的autoreleasepool就沒辦法走到后面這個花括號}
,那這個自動釋放池到底什么時候釋放呢元践?答案是當前runloop迭代結(jié)束的時候釋放韭脊,因為系統(tǒng)在每個runloop迭代中都加入了autoreleasepool的push
和pop
。具體原理可以深究runloop源碼单旁。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
文章開頭也說了沪羔,在ARC環(huán)境下,以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
方法是非公開的調(diào)試方法篓吁,需要聲明是外部實現(xiàn)的,否則無法使用蚪拦。運行打印的結(jié)果如下:
可以看到autoreleasepool持有了對象TestObject杖剪,這也驗證了生成非自己持有的對象,其真正的持有者是autoreleasepool這一說法驰贷。我們將a = [MyObject testObject];
這行注釋盛嘿,打開下面一行,運行打印結(jié)果如下:
可以看到這一次autoreleasepool并沒有持有TestObject對象括袒,說明以alloc開頭的方法生成的對象是自己持有的次兆。而且,由于a是__weak
修飾的锹锰,返回的對象由于無人持有芥炭,賦值以后立即被釋放掉了;所以in:
后面打印就是null了恃慧。同時編譯器已經(jīng)給出了警告??Assigning retained object to weak variable; object will be released after assignment园蝠。應(yīng)用autoreleasepool這一特性,可以在我們的項目中for in遍歷處理大量對象的時候糕伐,在循環(huán)體內(nèi)部用autoreleasepool將代碼包含砰琢,降低應(yīng)用內(nèi)存峰值,類似這樣:
摘自 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);
}
}
當然,按照sunnyxx這篇文章最后提到的一個知識點陪汽,使用容器的block版本的枚舉器時训唱,內(nèi)部會自動添加一個autoreleasepool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這里被一個局部@autoreleasepool包圍著
}];
由于筆者寫了測試代碼,用“clang -rewrite-objc”命令重寫為C++實現(xiàn)后挚冤,并沒有找到block版本枚舉器內(nèi)部會自動添加autoreleasepool的蛛絲馬跡况增;同時也查看了幫助文檔,這個方法的說明也沒有提到相關(guān)事項训挡。還望知道怎么得出這個結(jié)論的朋友指點一下澳骤。
那么autoreleasepool的內(nèi)部實現(xiàn)是怎么樣的呢?可以隨便寫段測試代碼澜薄,用“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;
};
發(fā)現(xiàn)autoreleasepool最終會變成 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
兩個方法的調(diào)用棋枕,這里顯示這兩個方法是外部定義的,那么我們?nèi)ツ睦镎疫@兩個方法的實現(xiàn)呢妒峦?答案是runtime源碼重斑!
這里說一下怎么下載runtime源碼:先打開這個網(wǎng)址https://opensource.apple.com/,然后選擇你電腦對應(yīng)的macOS版本肯骇,目前我電腦是10.13.6窥浪,然后com+F搜索objc4,我這里搜到的是objc4-723累盗,點擊下載寒矿。打開之后的目錄是這樣的:
我們打開NSObject.mm文件查看,全局搜索objc_autoreleasePoolPush
若债,發(fā)現(xiàn)是這樣:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
那就直接看AutoreleasePoolPage這個類的實現(xiàn):
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++實現(xiàn)的類:
- AutoreleasePool并沒有單獨的結(jié)構(gòu),而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針)
- AutoreleasePool是按線程一一對應(yīng)的(結(jié)構(gòu)中的thread指針指向當前線程)
- AutoreleasePoolPage每個對象會開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大胁鹑凇)蠢琳,除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址
- id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
- 一個AutoreleasePoolPage的空間被占滿時镜豹,會新建一個AutoreleasePoolPage對象傲须,連接鏈表,后來的autorelease對象在新的page加入
我們注意這一行:
# define POOL_BOUNDARY nil
定義了一個POOL_BOUNDARY的宏趟脂,值為nil泰讽,待會會用到。
看看AutoreleasePoolPage的push方法是怎么實現(xiàn)的:
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;
}
由于源碼太多,就不一一貼碼了已卸》鹦總結(jié)流程圖如下:
首先會判斷DebugPoolAllocation標志位,是否需要為每個pool都生成一個新page,為真就走autoreleaseNewPage方法,否則,執(zhí)行autoreleaseFast方法.
在autoreleaseFast方法中,如果存在page且未滿,則直接添加;
如果不存在page,會響應(yīng)autoreleaseNoPage;
如果當前page已滿,則響應(yīng)autoreleaseFullPage方法;
autoreleaseNoPage和autoreleaseFullPage會生成新的page,然后向該page中添加對象.
而autoreleaseNewPage方法,如果當前存在page,則執(zhí)行autoreleaseFullPage方法,否則響應(yīng)autoreleaseNoPage方法,然后就同上了,去執(zhí)行添加方法累澡。那么具體怎么樣添加呢梦抢?
每當進行一次push調(diào)用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象愧哟,即前面說的POOL_BOUNDARY宏奥吩,值為0(也就是個nil),那么這一個page就變成了下面的樣子:
objc_autoreleasePoolPush 的返回值就是這個哨兵對象的地址蕊梧,同時當作 objc_autoreleasePoolPop 的入?yún)ⅲ?/p>
- 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page
- 在當前page中霞赫,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息,并向回移動next指針到正確位置
- kill掉空page
pop之后就變成了這樣:
總結(jié)一下autoreleasepool的用法:在非UI框架肥矢,或者輔助線程中端衰,或者處理大量的臨時變量時篡石,需要使用
@autoreleasepool {}
恕洲。編譯器會將其轉(zhuǎn)為push和pop兩個操作,中間是我們自己的業(yè)務(wù)邏輯盐碱。push時是向AutoReleasePoolPage添加一個值為nil的哨兵對象楼誓,并作為該方法的返回值玉锌,也是pop方法的入?yún)ⅰop時根據(jù)哨兵對象的地址獲取到當前page,然后在當前page中,將晚于哨兵對象添加的對象都發(fā)送一次release命令,并更新next指針位置,最后kill掉空page疟羹。autoreleasepool允許多層嵌套主守,邏輯如上,不過是一個個的套娃榄融,一層層的剝離罷了参淫。
結(jié)語:內(nèi)存管理是iOS開發(fā)或者面試永遠繞不開的一個坎兒,想要完全跨越它愧杯,必須一步一個腳印涎才,慢慢攻克。理解這些原理性的東西力九,實際編程的時候才有理論指導(dǎo)耍铜,不至于兩眼一抹黑。路漫漫其修遠兮跌前,吾將上下而求索棕兼。。抵乓。
參考文章:
《iOS與OS X多線程和內(nèi)存管理》
http://www.cocoachina.com/ios/20170410/19030.html
https://juejin.im/entry/58a178060ce463005644ee4a
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/