前言
最近有時間把ios的基礎知識整理一下主穗,淺談一下對ios內存管理機制的理解,以前也只是會用腮敌,知其然但不知其所以然竖瘾。本文的ARC專指ObjectiveC的ARC鸣剪,不會設計到其他語言的內存管理组底,在此需要有一點的ios的內存管理機制的理解。
什么是ARC?
ARC的全稱是Auto Reference Counting也就是自動引用計數(shù)筐骇。
Objective C的引用計數(shù)理解起來很容易债鸡,當一個對象被持有的時候計數(shù)加一,不再被持有的時候引用計數(shù)減一铛纬,當引用計數(shù)為零的時候厌均,說明這個對象已經無用了,則將其釋放告唆。
引用計數(shù)分為兩種:
- 手動引用計數(shù)(MRC)
- 自動引用計數(shù)(ARC)
內存管理的思考方式:
- 自己生成的對象棺弊,自己持有
- 非自己生成的對象晶密,自己也能持有
- 不再需要自己持有對象時釋放
- 非自己持有的對象無法釋放
(1) 自己生成的對象,自己持有
在iOS內存管理中有四個關鍵字模她,alloc稻艰、new、copy侈净、mutableCopy尊勿,自身使用這些關鍵字產生對象,那么自身就持有了對象
// 使用了alloc分配了內存,obj指向了對象畜侦,該對象本身引用計數(shù)為1,不需要retain
id obj = [[NSObject alloc] init];
// 使用了new分配了內存,objc指向了對象元扔,該對象本身引用計數(shù)為1,不需要retain
id obj = [NSObject new];
(2) 非自己生成的對象夏伊,自己也能持有
// NSMutableArray通過類方法array產生了對象(并沒有使用alloc摇展、new吻氧、copy溺忧、mutableCopt來產生對象),因此該對象不屬于obj自身產生的
// 因此,需要使用retain方法讓對象計數(shù)器+1,從而obj可以持有該對象(盡管該對象不是他產生的)
id obj = [NSMutableArray array];
[obj retain];
(3) 不再需要自己持有對象時釋放
id obj = [NSMutableArray array];
[obj retain];
// 當obj不在需要持有的對象盯孙,那么鲁森,obj應該發(fā)送release消息
[obj release];
(4) 無法釋放非自己持有的對象
// 1. 釋放一個已經釋放的對象
id obj = [[NSObject alloc] init];
// 已經釋放對象
[obj release];
// 釋放了對象還進行釋放
[obj release]; //
//2. 釋放一個不屬于自己的對象
id obj1 = [obj object];
// obj1沒有進行retain操作而進行release操作,使得obj持有對象釋放振惰,造成了野指針錯誤
[obj1 release];
如上為iOS進行內存管理的四種思考方式(記住不論是ARC還是MRC都遵循該思考方式歌溉,只是ARC時代這些工作讓編譯器做了)
上述是我很好理解ios內存管理的的方法,在此我們做一下小的對比
在iOS開發(fā)早期骑晶,編寫代碼是采用MRC的
// MRC代碼
NSObject * obj = [[NSObject alloc] init]; //引用計數(shù)為1
//不需要的時候
[obj release] //引用計數(shù)減1
//持有這個對象
[obj retain] //引用計數(shù)加1
//放到AutoReleasePool
[obj autorelease]//在auto release pool釋放的時候痛垛,引用計數(shù)減1
雖說這種方式提供了面向對象的內存管理接口,但是開發(fā)者不得不花大量的時間在內存管理上桶蛔,并且容易出現(xiàn)內存泄漏或者release一個已被提前釋放的對象匙头,導致crash。
再后來仔雷,Apple對ios/Mac OS開發(fā)引入了ARC蹂析。使用ARC,開發(fā)者不再需要手動的retain/release/autorelease. 編譯器會自動插入對應的代碼碟婆,再結合Objective C的runtime电抚,實現(xiàn)自動引用計數(shù)。
比如如下ARC代碼:
NSObject * obj;
{
obj = [[NSObject alloc] init]; //引用計數(shù)為1
}
NSLog(@"%@",obj);
等同于如下MRC代碼
NSObject * obj;
{
obj = [[NSObject alloc] init]; //引用計數(shù)為1
[obj relrease]
}
NSLog(@"%@",obj);
在Objective C中竖共,有三種類型是ARC適用的:
- block
- objective 對象蝙叛,id, Class, NSError*等
- 由attribute((NSObject))標記的類型。
- 像double *,CFStringRef等不是ARC適用的公给,仍然需要手動管理內存借帘。
- 以CF開頭的(Core Foundation)的對象往往需要手動管理內存锻煌。
屬性所有權
我們在看看ARC中常見的所有權關鍵字,
- assign對應關鍵字__unsafe_unretained, 顧名思義姻蚓,就是指向的對象被釋放的時候宋梧,仍然指向之前的地址,容易引起野指針狰挡。
- copy對應關鍵字__strong,只不過在賦值的時候捂龄,調用copy方法。
- retain對應__strong
- strong對應__strong
- unsafe_unretained對應__unsafe_unretained
- weak對應__weak加叁。
其中倦沧,__weak和__strong是本文要講解的核心內容。
ARC的內部實現(xiàn)
ARC背后的引用計數(shù)主要依賴于這三個方法:
- retain 增加引用計數(shù)
- release 降低引用計數(shù)它匕,引用計數(shù)為0的時候展融,釋放對象。
- autorelease 在當前的auto release pool結束后豫柬,降低引用計數(shù)告希。
在Cocoa Touch中,NSObject協(xié)議中定義了這三個方法烧给,由于Cocoa Touch中燕偶,絕大部分類都繼承自NSObject(NSObject類本身實現(xiàn)了NSObject協(xié)議),所以可以“免費”獲得NSObject提供的運行時和ARC管理方法础嫡,這就是為什么適用OC開發(fā)iOS的時候指么,你的類要繼承自NSObject。
既然ARC是引用計數(shù)榴鼎,那么對應一個對象伯诬,內存中必然會有一個地方來存儲這個對象的引用計數(shù)。iOS的Runtime是開源的巫财,如果你有興趣可以下載全部的代碼盗似,我們通過源代碼一探究竟。
我們從retain入手
- (id)retain {
return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
所以說翁涤,本質上retain就是調用sidetable_retain桥言,再看看sitetable_retain的實現(xiàn):
id objc_object::sidetable_retain()
{
//獲取table
SideTable& table = SideTables()[this];
//加鎖
table.lock();
//獲取引用計數(shù)
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//增加引用計數(shù)
refcntStorage += SIDE_TABLE_RC_ONE;
}
//解鎖
table.unlock();
return (id)this;
}
到這里,retain如何實現(xiàn)就很清楚了葵礼,通過SideTable這個數(shù)據(jù)結構來存儲引用計數(shù)号阿。我們看看這個數(shù)據(jù)結構的實現(xiàn):
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
//省略其他實現(xiàn)...
};
可以看到,這個數(shù)據(jù)結構就是存儲了一個自旋鎖鸳粉,一個引用計數(shù)map扔涧。這個引用計數(shù)的map以對象的地址作為key,引用計數(shù)作為value。到這里枯夜,引用計數(shù)的底層實現(xiàn)我們就很清楚了.
存在全局的map弯汰,這個map以地址作為key,引用計數(shù)的值作為value
再來看看release的實現(xiàn):
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
//找到對應地址的
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) { //找不到的話湖雹,執(zhí)行dellloc
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用計數(shù)小于閾值咏闪,dealloc
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用計數(shù)減去1
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
//執(zhí)行dealloc
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
release的到這里也比較清楚了:查找map,對引用計數(shù)減1摔吏,如果引用計數(shù)小于閾值鸽嫂,則調用SEL_dealloc
Autorelease pool
上文提到了,autorelease方法的作用是把對象放到autorelease pool中征讲,到pool drain的時候据某,會釋放池中的對象。舉個例子
__weak NSObject * obj;
NSObject * temp = [[NSObject alloc] init];
obj = temp;
NSLog(@"%@",obj); //非空
放到auto release pool中诗箍,
__weak NSObject * obj;
@autoreleasepool {
NSObject * temp = [[NSObject alloc] init];
obj = temp;
}
NSLog(@"%@",obj); //null
可以看到癣籽,放到自動釋放池的對象是在超出自動釋放池作用域后立即釋放的。事實上在iOS 程序啟動之后滤祖,主線程會啟動一個Runloop筷狼,這個Runloop在每一次循環(huán)是被自動釋放池包裹的,在合適的時候對池子進行清空氨距。
對于Cocoa框架來說桑逝,提供了兩種方式來把對象顯式的放入AutoReleasePool.
- NSAutoreleasePool(只能在MRC下使用)
- @autoreleasepool {}代碼塊(ARC和MRC下均可以使用)
那么AutoRelease pool又是如何實現(xiàn)的呢?
我們先從autorelease方法源碼入手
//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);
}
可以看到,把一個對象放到auto release pool中茬暇,是調用了AutoreleasePoolPage::autorelease這個方法首昔。
我們繼續(xù)查看對應的實現(xiàn):
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方法的實現(xiàn)就比較清楚了糙俗,
autorelease方法會把對象存儲到AutoreleasePoolPage的鏈表里勒奇。等到auto release pool被釋放的時候,把鏈表內存儲的對象刪除巧骚。所以赊颠,AutoreleasePoolPage就是自動釋放池的內部實現(xiàn)。
__weak與__strong
用過block的同學一定寫過類似的代碼:
__weak typeSelf(self) weakSelf = self;
[object fetchSomeFromRemote:^{
__strong typeSelf(weakSelf) strongSelf = weakSelf;
//從這里開始用strongSelf
}];
那么劈彪,為什么要這么用呢竣蹦?原因是:
- block會捕獲外部變量,用weakSelf保證self不會被block被捕獲沧奴,防止引起循環(huán)引用或者不必要的額外生命周期痘括。
- 用strongSelf則保證在block的執(zhí)行過程中,對象不會被釋放掉。
首先__strong和__weak都是關鍵字纲菌,是給編譯器理解的挠日。為了理解其原理,我們需要查看它們編譯后的代碼翰舌,使用XCode嚣潜,我們可以容易的獲得一個文件的匯編代碼。
比如椅贱,對于Test.m文件郑原,當源代碼如下時:
#import "Test.h"
@implementation Test
- (void)testFunction{
{
__strong NSObject * temp = [[NSObject alloc] init];
}
}
@end
轉換后的匯編代碼如下:
.loc 2 15 37 prologue_end ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:37
ldr x9, [x9]
ldr x1, [x8]
mov x0, x9
bl _objc_msgSend
adrp x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
.loc 2 15 36 is_stmt 0 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
ldr x1, [x8]
.loc 2 15 36 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
bl _objc_msgSend
mov x8, #0
add x9, sp, #8 ; =8
.loc 2 15 29 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:29
str x0, [sp, #8]
Ltmp4:
.loc 2 16 5 is_stmt 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
mov x0, x9
mov x1, x8
bl _objc_storeStrong
.loc 2 17 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
Ltmp5:
即使我們不懂匯編,也能很輕易的獲取到調用順序如下
_objc_msgSend // alloc
_objc_msgSend // init
_objc_storeStrong // 強引用
在結合Runtime的源碼夜涕,我們看看最關鍵的objc_storeStrong的實現(xiàn)
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
id objc_retain(id obj) { return [obj retain]; }
void objc_release(id obj) { [obj release]; }
我們再來看看__weak. 將Test.m修改成為如下代碼犯犁,同樣我們分析其匯編實現(xiàn)
.loc 2 15 35 prologue_end ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:35
ldr x9, [x9]
ldr x1, [x8]
mov x0, x9
bl _objc_msgSend
adrp x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
.loc 2 15 34 is_stmt 0 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
ldr x1, [x8]
.loc 2 15 34 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
bl _objc_msgSend
add x8, sp, #24 ; =24
.loc 2 15 27 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
mov x1, x0
.loc 2 15 27 discriminator 2 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x8
bl _objc_initWeak
.loc 2 15 27 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
ldr x1, [sp, #16] ; 8-byte Folded Reload
.loc 2 15 27 discriminator 3 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
str x0, [sp, #8] ; 8-byte Folded Spill
mov x0, x1
bl _objc_release
add x8, sp, #24
Ltmp4:
.loc 2 16 5 is_stmt 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
mov x0, x8
bl _objc_destroyWeak
.loc 2 17 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
ldp x29, x30, [sp, #48] ; 8-byte Folded Reload
add sp, sp, #64 ; =64
ret
可以看到,__weak本身實現(xiàn)的核心就是以下兩個方法
- _objc_initWeak
- _objc_destroyWeak
我們通過Runtime的源碼分析這兩個方法的實現(xiàn):
id objc_initWeak(id *location, id newObj)
{
//省略....
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
void objc_destroyWeak(id *location)
{
(void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>
(location, nil);
}
所以女器,本質上都是調用了storeWeak函數(shù)酸役,這個函數(shù)內容較多,主要做了以下事情
- 獲取存儲weak對象的map驾胆,這個map的key是對象的地址涣澡,value是weak引用的地址。
- 當對象被釋放的時候丧诺,根據(jù)對象的地址可以找到對應的weak引用的地址入桂,將其置為nil即可。
這就是在weak背后的神秘的手驳阎。