手動內(nèi)存管理
在 Xcode4.2 版本以后暇藏,自動引用計數(shù) ARC 已經(jīng)是默認(rèn)有效了洽胶。但是這里還是先分析一下手動內(nèi)存管理 MRC塔橡,方便我們對 iOS 開發(fā)的內(nèi)存管理有更清晰的認(rèn)識。
參考:《Objective-C 高級編程》干貨三部曲(一):引用計數(shù)篇
寫在前面:
- NSObject 已經(jīng)開源启盛,所以 alloc/retain/release/dealloc 的真實實現(xiàn)方案蹦掐,也不用像書籍作者說的那樣參考 GNUstep 源碼去推測:
- 源碼在線地址:NSObject.mm https://opensource.apple.com/source/objc4/objc4-756.2/runtime/NSObject.mm.auto.html
- 下載地址:objt4 源碼 https://opensource.apple.com/tarballs/objc4/
- 可通過下面兩個方法查看匯編輸出:
參考:iOS 獲取匯編輸出方法
- Xcode -> Product -> Perform Action -> Assemble "*.m"即可獲得匯編輸出。
- clang獲取oc代碼匯編輸出僵闯。在文件目錄下卧抗,執(zhí)行命令行:clang -S -fobjc-arc 文件名.m -o output.s
- 或者查看文件轉(zhuǎn)成 C++ 后的源碼。在文件目錄下執(zhí)行命令行:
$ clang -rewrite-objc MyClass.m
然后在同一目錄下會多出一個 MyClass.cpp 文件棍厂,雙擊打開即可颗味。
內(nèi)存管理的思考方式
內(nèi)存管理更加客觀、正確的思考方式是:
- 自己生成的對象牺弹,自己持有。
- 非自己生成的對象时呀,自己也能持有张漂。
- 不再需要自己持有的對象時釋放。
- 非自己持有的對象谨娜,無法釋放航攒。
iOS 內(nèi)存管理經(jīng)常用到的詞有:生成、持有趴梢、釋放漠畜、銷毀。如下:
對象操作 | Objective-C 方法 | 引用計數(shù)變化 |
---|---|---|
生成并持有對象 | alloc/new/copy/mutableCopy 等方法 | +1 |
持有對象 | retain方法 | +1 |
釋放對象 | release方法 | -1 |
廢氣對象 | dealloc方法 | 無 |
借用書中圖坞靶,直觀感受如下:
下面分條列舉說明憔狞。
自己生成的對象,自己所持有
使用以下名稱開頭的方法名彰阴,意味著自己生成的方法瘾敢,只有自己持有:
- alloc
- new
- copy
- mutableCopy
根據(jù)上述 使用以下名稱開頭的方法名,下列名稱也意味著自己生成并持有對象:
- allocMyObject
- newTheObject
- copyThis
- mutableCopyYourObject
但是對于以下名稱,即使使用了 alloc/new/copy/mutableCopy 開頭簇抵,也并不屬于同一類別的方法:
- allocate
- newer
- copying
- mutableCopyyed
這里用 駝峰(CameCase
) 命名法來區(qū)分庆杜。
非自己生成的對象,自己也能持有
在 alloc/new/copy/mutableCopy 方法以外取得的對象碟摆,默認(rèn)并不持有對象晃财,可以通過 retain
來持有對象:
/// 取得非自己生成并持有的對象。此時典蜕,obj 取得對象的存在断盛,但自己并不持有
id obj = [NSMutableArray array];
/// 自己持有對象
[obj retain];
不再需要自己持有的對象時釋放
自己持有的對象,一旦不需要嘉裤,持有者有義務(wù)釋放該對象郑临。釋放使用 release
方法。
/// 自己生成并持有對象
id obj = [[NSObject alloc] init];
/// 釋放對象屑宠。指向該對象的指針仍然保留在 obj 中厢洞,貌似可以訪問。
/// 但對象一經(jīng)釋放典奉,決不可訪問躺翻,否則會發(fā)生崩潰。
[obj release];
通過 retain
持有非自己生成的對象時卫玖,也需要使用 release
釋放:
/// 生成
id obj = [NSMutableArray array];
/// 持有
[obj retain];
/// 釋放
[obj release];
生成并持有對象公你,方法實現(xiàn)如下,注意 allocObject
符合前面生成并持有對象的命名規(guī)范:
-(id)allocObject {
/// 生成并持有對象
id obj = [[NSObject alloc] init];
/// 讓自己持有對象
return obj;
}
/// 此時假瞬,不用調(diào)用 retain陕靠。obj1 已經(jīng)持有該對象
id obj1 = [obj0 allocObject];
生成對象,默認(rèn)不持有脱茉,需要手動持有剪芥,實現(xiàn)如下:
-(id)object {
/// 生成并持有對象
id obj = [[NSObject alloc] init];
/// 取得對象的存在。但是放棄對對象的持有琴许。即加入自動釋放池税肪。
[obj autorelease];
/// 此時,obj 并不能持有該對象了
return obj;
}
/// 獲取對象榜田,并不持有
id obj1 = [obj0 object];
/// 持有對象益兄。即引用計數(shù) +1
[obj1 retain];
使用 autorelease
方法,可以使取得對象的存在箭券,但是自己不持有對象净捅。 autorelease
提供這樣的功能:使對象在超出指定的生存范圍時能夠自動并正確的釋放(調(diào)用 release 方法)。如下圖所示:
無法釋放非自己持有的對象
對于用 alloc/new/copy/mutableCopy 生成并持有的對象邦鲫,或者是 retain 持有的對象灸叼,由于持有者是自己神汹,所以在不需要該對象時自己需要將其釋放。而由此之外得到的對象絕對不能釋放古今,若在程序中釋放了非自己持有的對象屁魏,會引發(fā)崩潰。例如過度釋放對象:
/// 自己生成并持有對象
id obj = [[NSObject alloc] init];
/// 釋放對象
[obj release];
/// 對象已經(jīng)釋放后再次釋放
[obj release];
/// 此時捉腥,程序崩潰氓拼。
/// 崩潰分析:對象已廢棄,訪問廢棄對象時崩潰抵碟。野指針
或者在 取得的對象已存在桃漾,但自己不持有該對象 時釋放,也會引發(fā)崩潰:
id obj = [NSMutableArray array];
[obj release];
///釋放非自己持有的對象拟逮,崩潰
alloc/retain/release/dealloc 實現(xiàn)
NSObject 源碼并沒有公開(已開源)撬统,這里參考開源的 GNUstep 源碼來推測 NSObject 內(nèi)部的實現(xiàn)細節(jié)。主要是要從實現(xiàn)的角度來理解內(nèi)存管理的方式敦迄。為了明確重點恋追,部分引用做了修改。
alloc 方法
id obj = [NSObject alloc];
調(diào)用該方法罚屋,源碼實現(xiàn)如下:
/// GNUstep/modules/core/base/Source/NSObject.m alloc
+(id) alloc {
return [self allocWithZone: NSDefaultMallocZone()];
}
+(id)allocWithZone: (NSZone *)z {
return NSAllocateObject(self, 0, z);
}
通過 allocWithZone 類方法調(diào)用 NSAllocateObject 函數(shù)分配了對象苦囱,下面看下 NSAllocateObject 函數(shù):
/// GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject
struct obj_layout {
NSUInteger retained;
};
inline id
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
int size = 計算容納對象所需要的內(nèi)存大小
id new = NSZoneMalloc(zone, size);
memset(new, 0, size);
new = (id) & ((struct obj_layout *) new)[1];
}
NSAllocateObject 函數(shù)通過調(diào)用 NSZoneMalloc 函數(shù)來分配存放對象所需要的內(nèi)存空間。之后將該空間置為 0脾猛,最后返回作為對象而使用的指針撕彤。
NSZone 是為防止內(nèi)存碎片化而引入的數(shù)據(jù)結(jié)構(gòu)。目前該接口效率低猛拴,且使源代碼更加復(fù)雜羹铅。
下面是去掉 NSZone
之后簡化的源代碼:
struct obj_layout {
NSUInteger retained;
};
+(id) alloc {
int size = sizeof(struct obj_layout) + 對象大小;
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
return (id)(p + 1);
}
alloc 類方法使用 struct obj_layout 中的 retained 整數(shù)來保存引用計數(shù),并將其寫入對象內(nèi)存頭部愉昆,該對象內(nèi)存塊全部置為 0 后返回睦裳。下圖為 GNUstep 實現(xiàn) alloc 類方法返回對象示意圖:
對象的引用計數(shù)可以通過 retainCount 實例方法獲取。源碼實現(xiàn)如下:
/// GNUstep/modules/core/base/Source/NSObject.m retainCount
-(NSUInteger) retainCount {
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject) {
return ((struct obj_layout *)anObject)[-1].retained;
}
可以看到撼唾,給NSExtraRefCount傳入anObject以后,通過訪問對象內(nèi)存頭部的.retained變量哥蔚,來獲取引用計數(shù)倒谷。
retain 方法
/// GNUstep/modules/core/base/Source/NSObject.m retain
-(id) retain {
NSIncrementExtraRefCount(self);
return self;
}
inline void
NSIncrementExtraRefCount(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
[NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained++;
}
可以看出,如果已有的引用計數(shù)過大糙箍,會執(zhí)行異常代碼渤愁。正常情況下,只運行了使 retained 變量加 1 的 retained++ 代碼深夯。
release 方法
/// GNUstep/modules/core/base/Source/NSObject.m release
-(void) release {
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout*)anObject)[-1].retained--;
return NO;
}
}
當(dāng) retained 變量大于 0 時減 1抖格,等于 0 時調(diào)用 dealloc 實例方法诺苹,廢棄對象。
dealloc 方法
/// GNUstep/modules/core/base/Source/NSObject.m dealloc
-(void) dealloc {
NSDeallocateObject(self);
}
inline void
NSDeallocateObject(id anObject) {
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o);
}
上述代碼廢棄由 alloc 分配的內(nèi)存塊雹拄。
以上便是 GNUstep 中 alloc/retain/release/dealloc 的實現(xiàn)收奔,具體總結(jié)如下:
- 在 Objective-C 的對象中存有引用計數(shù)這一整數(shù)值。
- 調(diào)用 alloc 或 retain 方法后滓玖,引用計數(shù) +1坪哄。
- 調(diào)用 release 方法后,引用計數(shù) -1势篡。
- 引用計數(shù)為 0 后調(diào)用 dealloc 方法廢棄對象翩肌。
好了,下面看一下蘋果的實現(xiàn)吧禁悠。
蘋果的實現(xiàn)
因為 NSObject 源碼并沒有公開(已開源)念祭,這里利用 Xcode 的調(diào)試器 lldb 和 iOS 大概追溯其實現(xiàn)過程。在 NSObject 類的 alloc 類方法上設(shè)置斷點碍侦,追蹤程序的執(zhí)行粱坤。以下列出執(zhí)行調(diào)用的方法和函數(shù):
-
alloc 方法
- alloc
- allocWithZone
- class_createInstance
- calloc
-
retainCount 方法
- __CFDoExternRefOperation
- CFBasicHashGetCountOfKey
-
retain 方法
- __CFDoExternRefOperation
- CFBasicHashAddValue
-
release 方法
- __CFDoExternRefOperation
- CFBasicHashRemoveValue
CFBasicHashRemoveValue 返回 0 時,-release 調(diào)用 dealloc
上面頻繁出現(xiàn)的 __CFDoExternRefOperation 是開源代碼 CFRuntime.c 的 __CFDoExternRefOperation 函數(shù)祝钢,
源碼如下:
CF_EXPORT uintptr_t __CFDoExternRefOperation(uintptr_t op, id obj) {
if (nil == obj) HALT;
uintptr_t idx = EXTERN_TABLE_IDX(obj);
uintptr_t disguised = DISGUISE(obj);
CFSpinLock_t *lock = &__NSRetainCounters[idx].lock;
CFBasicHashRef table = __NSRetainCounters[idx].table;
uintptr_t count;
switch (op) {
case 300: // increment
case 350: // increment, no event
__CFSpinLock(lock);
CFBasicHashAddValue(table, disguised, disguised);
__CFSpinUnlock(lock);
if (__CFOASafe && op != 350) __CFRecordAllocationEvent(__kCFObjectRetainedEvent, obj, 0, 0, NULL);
return (uintptr_t)obj;
case 400: // decrement
if (__CFOASafe) __CFRecordAllocationEvent(__kCFObjectReleasedEvent, obj, 0, 0, NULL);
case 450: // decrement, no event
__CFSpinLock(lock);
count = (uintptr_t)CFBasicHashRemoveValue(table, disguised);
__CFSpinUnlock(lock);
return 0 == count;
case 500:
__CFSpinLock(lock);
count = (uintptr_t)CFBasicHashGetCountOfKey(table, disguised);
__CFSpinUnlock(lock);
return count;
}
return 0;
}
下面是其簡化后的代碼實現(xiàn):
/// CF/CFRuntime.c __CFDoExternRefOperation
int ____CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得對象的散列表(obj);
int count;
switch(op) {
case Operation_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case Operation_retain:
count = CFBasicHashAddValue(table, obj);
return count;
case Operation_release:
count = CFBadicHashRemoveValue(table, obj);
return 0 == count;
}
}
__CFDoExternRefOperation 函數(shù)按照 retainCount/retain/release 操作進行分發(fā)比规,調(diào)用不同的函數(shù)。猜測拦英, NSObject 類的 retainCount/retain/release 實例方法也許如下面代碼表示:
-(NSUInteger) retainCount {
return (NSUInteter) __CFDoExternRefOperation(Operation_retainCount, self);
}
-(id)retain {
return (id)__CFDoExternRefOperation(Operation_retain, self);
}
-(void) release {
return __CFExternRefOperation(Operation_release, self);
}
從 __CFDoExternRefOperation 函數(shù)以及由此函數(shù)調(diào)用的各個函數(shù)名看出蜒什,蘋果的實現(xiàn)大概是采用散列表(引用技數(shù)表)來管理引用計數(shù)的,如下圖所示:
GNUstep 將引用計數(shù)保存到對象內(nèi)存塊頭部的變量中疤估,好處如下:
- 少量代碼即可完成灾常。
- 能夠統(tǒng)一管理引用計數(shù)內(nèi)存和對象內(nèi)存塊。
蘋果很可能采用引用計數(shù)表管理引用計數(shù)铃拇,這樣的好處是:
- 對象用內(nèi)存塊的分配無需考慮內(nèi)存塊頭部钞瀑。
- 引用計數(shù)表中存有各個對象的內(nèi)存塊地址,可通過改地址追溯到對象的內(nèi)存塊慷荔。
尤其是第二雕什,在調(diào)試時有著舉足輕重的作用,即使出現(xiàn)故障導(dǎo)致對象占用的內(nèi)存損壞显晶,只要引用計數(shù)表沒壞贷岸,就能夠確認(rèn)各內(nèi)存塊地址。
NSObject.mm 源碼實現(xiàn)
alloc
/// Objc源碼/objc4-756.2/runtime/NSObject.mm
+ (id)alloc {
return _objc_rootAlloc(self);
}
而 _objc_rootAlloc 實現(xiàn)為:
/// Objc源碼/objc4-756.2/runtime/NSObject.mm
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
callAlloc 方法開始實現(xiàn)具體細節(jié):
/// Objc源碼/objc4-756.2/runtime/NSObject.mm
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
其實可以看到磷雇,和上面的猜測大致是一樣的偿警。這里也不再具體擴展,需要時可以從源碼層面看下 release唯笙、dealloc 等的具體實現(xiàn)螟蒸。
autorelease
autorelease 介紹
當(dāng) autorelease 管理的對象超出其作用域后盒使,對象實例的 release 方法會被調(diào)用。autorelease 的具體使用方法如下:
- 生成并持有 NSAutoreleasePool 對象七嫌。
- 調(diào)用已分配對象的 autorelease 方法少办。
- 廢棄 NSAutoreleasePool 對象。
對所有調(diào)用過 autorelease 實例方法的對象抄瑟,在廢棄 NSAutoreleasePool 時凡泣,都將主動調(diào)用 release 方法:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; /// 等同于 [obj release];
程序并非一定要使用 NSAutoreleasePool 對象來工作。但是在大量產(chǎn)生 autorelease 的對象時皮假,只要不廢棄 NSAutoreleasePool 對象鞋拟,即 RunLoop 不進入睡眠狀態(tài),那么生成的對象就不能被釋放惹资,因此有時會產(chǎn)生內(nèi)存不足的現(xiàn)象贺纲。如大量循環(huán)中對圖片做復(fù)雜操作。這種情況下褪测,就會產(chǎn)生大量的 autorelease 對象猴誊,內(nèi)存激增:
for (int i = 0; i < 圖片數(shù); i++) {
/*
* 讀入圖像
* 大量產(chǎn)生 autorelease 對象
* 由于沒有廢棄 NSAutoreleasePool 對象,最終導(dǎo)致內(nèi)存不足侮措。
*/
}
再次情況下懈叹,有必要在適當(dāng)?shù)牡胤缴伞⒊钟蟹衷U棄 NSAutoreleasePool 對象:
for (int i = 0; i < imageArray.count; i++) {
// 臨時 Pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *image = imageArray[i];
[image doSomething];
[pool drain];
}
可能會出的面試題:什么時候會創(chuàng)建自動釋放池澄成?
答:運行循環(huán)檢測到事件并啟動后,就會創(chuàng)建自動釋放池畏吓,而且子線程的 runloop 默認(rèn)是不工作的墨状,無法主動創(chuàng)建,必須手動創(chuàng)建菲饼。
舉個例子:
自定義的 NSOperation 類中的 main 方法里就必須添加自動釋放池肾砂。否則在出了作用域以后,自動釋放對象會因為沒有自動釋放池去處理自己而造成內(nèi)存泄露宏悦。
GNUstep autorelease 實現(xiàn)
同上镐确,先看一下 GNUstep 的源碼實現(xiàn):
/// GNUstep/modules/core/base/Source/NSObject.m autorelease
-(id) autorelease {
[NSAutoreleasePool addObject: self];
}
autorelease 方法的本質(zhì)就是調(diào)用 NSAutoreleasePool 對象的 addObject 類方法。下面是作者假想后簡化的 NSAutoreleasePool 源代碼實現(xiàn):
/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject
+(void) addObjecty: (id)anObj {
NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 對象;
if (pool != nil) {
[pool addObject: anObj];
} else {
NSLog()
}
}
-(void) addObject: (id)anObj {
[array addObject: anObj];
}
addObject 類方法就是調(diào)用正在使用的 NSAutoreleasePool 對象的 addObject 實例方法饼煞,然后這個對象就被追加到正在使用的 NSAutoreleasePool 對象的數(shù)組中辫塌。
下面看一下使用 drain 實例方法廢棄正在使用的 NSAutoreleasePool 對象的過程:
/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m drain
-(void) drain {
[self dealloc];
}
-(void) dealloc {
[self emptyPool];
[array release];
}
-(void) emptyPool {
for (id obj in array) {
[obj release];
}
}
雖然調(diào)用了好幾個方法,可以確定對于數(shù)組中的所有對象都調(diào)用了 release 實例方法派哲。
蘋果的實現(xiàn)
可通過 objc 庫的 https://opensource.apple.com/source/objc4/objc4-750.1/runtime/NSObject.mm.auto.html 來確認(rèn)蘋果中 autorelease 的實現(xiàn)。
/// /objc4/objc4-750.1/runtime/NSObject.mm AutoreleasePoolPage
/// 這里的源碼非常長掺喻。芭届。储矩。感興趣的可以自己去看下。
class AutoreleasePoolPage {
}
下面還是結(jié)合書中總結(jié)的來做分析吧褂乍。核心方法是一樣的:
class AutoreleasePoolPage {
static inline voiod *push() {
// 相當(dāng)于生成或者持有 NSAutoreleasePool 類對象持隧;
}
static inline void *pop(void *token) {
// 相當(dāng)于廢棄 NSAutoreleasePool 類對象;
releaseAll();
}
static inline id autorelease(id obj) {
/*
* 相當(dāng)于 NSAutoreleasePool 類的 addObject 類方法逃片;
* AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 實例屡拨;
* page -> add(obj);
*/
}
id *add(id obj) {
// 將對象追加到內(nèi)部數(shù)組中;
}
void releaseAll() {
// 調(diào)用內(nèi)部數(shù)組中對象的 release 方法
}
};
/// 進棧
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage :: push();
}
/// 出棧
void *objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage :: pop(ctxt)
}
/// 在內(nèi)部釋放
id *objc_autorelease(id obj) {
return AutoreleasePoolPage :: autorelease(obj);
}
下面通過外部調(diào)用來對比分析:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于 objc_autoreleasePoolPush()
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)
[NSAutoreleasePool showPools];
/// 非公開類方法褥实。showPools 會將現(xiàn)在 NSAutoreleasePool 的狀況輸出到控制臺
[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)
可能出的面試題: 蘋果如何實現(xiàn) NSAutoreleasePool 的呀狼? 參考答案: NSAutorelease 以一個隊列數(shù)組的形式實現(xiàn),主要使用三個方法:objc_autoreleasePoolPush(進棧)损离、objc_autoreleasePoolPop(出棧)哥艇、objc_autorelease(釋放內(nèi)部)。
另外僻澎,如果 autorelease NSAutoreleasePool 對象貌踏,回引發(fā)崩潰。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];
因為對于 NSAutoreleasePool 來說窟勃,autorelease 已被重載祖乳。
ARC 自動引用計數(shù)
內(nèi)存管理的思考方式
引用計數(shù)式內(nèi)存管理,就是思考 ARC 所引起的變化秉氧。
- 自己生成的對象眷昆,自己所持有。
- 非自己生成的對象谬运,自己也能持有隙赁。
- 自己持有的對象不再需要時,釋放梆暖。
- 非自己持有的對象無法釋放伞访。
可以看到,思想和 MRC 是一樣的轰驳,區(qū)別主要是我們不需要在顯式的調(diào)用厚掷,在源代碼的記述方法上有所不同。
四種所有權(quán)修飾符
ARC 有效時级解,id 類型或者對象類型必須附加所有權(quán)修飾符冒黑。所有權(quán)修飾符一共有四種,如下:
- __strong:is the default. An object remains “alive” as long as there is a strong pointer to it.
- __weak:specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
- __unsafe_unretained:specifies a reference that does not keep the referenced object alive and is not set to nil when there are no strong references to the object. If the object it references is deallocated, the pointer is left dangling.
- __autoreleasing:is used to denote arguments that are passed by reference (id *) and are autoreleased on return.
下面挨個看一下吧勤哗。
__strong 修飾符
ARC 環(huán)境下抡爹,__strong 是屬性的默認(rèn)修飾符。
__strong 使用方法
id obj = [[NSObject alloc] init];
等同于:
id __strong obj = [[NSObject alloc] init];
__strong 修飾符表示對對象的“強引用”芒划,該對象的持有狀態(tài)如下:
{
// 自己生成并持有對象冬竟。因為強引用欧穴,所以持有。
id __strong obj = [[NSObject alloc] init];
}
/// obj 超出作用域泵殴,強引用失效涮帘。所以自動釋放持有的對象。
對于非自己生成笑诅,并持有的對象调缨,亦是如此:
{
// 取得非自己生成并持有的對象
id __strong obj = [NSMutableArray array];
}
/// 超出作用域,強引用失效
附有 __strong 修飾符的變量之間也可以相互賦值:
// 生成對象A吆你。obj0 持有對象 A 的強引用
id __strong obj0 = [[NSObject alloc] init];
// 生成對象B弦叶。obj1 持有對象 B 的強引用
id __strong obj1 = [[NSObject alloc] init];
// obj2 不持有任何對象
id __strong obj2 = nil;
// obj0 持有對象 B 的強引用。此時對象 A 因為不再被強引用早处,被廢棄湾蔓。
// 此時對象 B 被變量 obj0 和 obj1 共同持有。
obj0 = obj1;
// obj2 持有對象 B 的強引用砌梆。
// 此時對象 B 被變量 obj0默责、obj1贱鼻、obj2 持有根灯。
obj2 = obj0;
// obj1 不再強引用對象 B
obj1 = nil;
// obj0 不再強引用對象 B
obj0 = nil;
// obj2 不再強引用對象 B
obj2 = nil;
// 此時宽堆,對象 B 不再被任何變量強引用蕊程,被廢棄旬渠。
也可以給類的成員變量或方法屬性加上 __strong 修飾符:
@interface Test: NSObject {
id __strong obj_;
}
-(void)setObject: (id __strong)obj;
因為 id 類型和對象類型的所有權(quán)修飾符默認(rèn)為 __strong 修飾符扎即,所以通常不需要寫上 __strong宏胯。
__strong 實現(xiàn)
{
id __strong obj = [[NSObject alloc] init];
}
通過 clang 獲取程序匯編輸出谜酒,或者 cpp 文件坟比,結(jié)合 objc4 庫源碼芦鳍,可以分析程序執(zhí)行的流程。
其實這部分都是模擬代碼葛账,通過總結(jié)分析得出的結(jié)論柠衅。深入解構(gòu)objc_msgSend函數(shù)的實現(xiàn) 這篇文章作者通過把匯編語言轉(zhuǎn)換成 C 語言來具體分析 Objective-C 的消息轉(zhuǎn)發(fā)機制,分析的很深入籍琳。
/// 編譯器模擬代碼
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
作為對比菲宴,看一下轉(zhuǎn)化 C++ 后的執(zhí)行:
id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
可以看出,即使 ARC 不支持 release趋急,實際上編輯器還是自動插入了 release喝峦。通過 objc_msgSend 來傳遞消息。
而使用 alloc/new/copy/mutableCopy 以外方法生成的對象呜达,又有一些不一樣:
{
id __strong obj = [NSMutableArray array];
}
編譯器的模擬代碼:
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj)
objc_retainAutoreleaseReturnValue 函數(shù)主要用于程序最優(yōu)化執(zhí)行谣蠢。該函數(shù)持有的對象應(yīng)該是:注冊到 autoreleasePool 中對象的方法,或者函數(shù)的返回值。
這種 objc_retainAutoreleaseReturnValue 函數(shù)是成對的漩怎,與之相對的函數(shù)是: objc_autoreleaseReturnValue勋颖。來看一下它的使用:
+(id)array {
return [[NSMutableArray alloc] init];
}
編譯器的模擬代碼:
+(id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
objc_autoreleaseReturnValue: 返回注冊到 autoreleasePool 的對象。
書中說勋锤,objc_retainAutoreleaseReturnValue 和 objc_autoreleaseReturnValue 方法配合使用可以不將對象注冊到 autoreleasePool中,如下圖:
__weak 修飾符
當(dāng)帶有 __strong 修飾符的變量在持有對象時侥祭,如果多個對象相互持有叁执,很容易發(fā)生循環(huán)引用。
循環(huán)引用容易發(fā)生內(nèi)存泄漏矮冬。所謂內(nèi)存泄漏就是應(yīng)當(dāng)被廢棄的對象在超出其生存周期后繼續(xù)存在谈宛。
__weak 用法
@interface Test: NSObject {
id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end
@implementation Test
-(id)init {
self = [super init];
return self;
}
-(void)setObject:(id __strong)obj {
obj_ = obj;
}
以下為循環(huán)引用:
{
id test0 = [[Test alloc] init]; // test0 強引用對象 A
id test1 = [[Test alloc] init]; // test1 強引用對象 B
[test0 setObject: test1]; // test0 強引用對象 B
[test1 setObject: test0]; // test1 強引用對象 A
}
或者,對象持有自身時胎署,也會發(fā)生循環(huán)引用:
{
id test = [[Test alloc] init];
[test setObject: test];
}
使用 __weak 修飾符可以避免循環(huán)使用吆录。通過檢查附有 __weak 修飾符的變量是否為 nil,可以判斷被賦值的對象是否已經(jīng)被廢棄琼牧。
__weak 的另一個優(yōu)點就是恢筝,在持有某對象的弱引用時,若該對象被廢棄巨坊,則此弱引用自動失效且被置為 nil 狀態(tài)撬槽。
通過下面代碼看下 __weak 的特性:
id __weak obj = [[NSObject alloc] init];
改代碼會出現(xiàn)編譯警告,因為 __weak 并不直接持有對象趾撵,所以 obj 會被立即釋放掉侄柔。修改如下即可:
id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
所以,針對上面循環(huán)引用的問題占调,作出修改,即可避免循環(huán)引用了:
@interface Test: NSObject {
id __weak obj_;
}
-(void)setObject:(id __strong)obj;
@end
__weak 實現(xiàn)
通過前面說明暂题,可以看到 __weak 有如下魔法般的效果:
- 若附有 __weak 修飾符的變量所引用的對象被廢棄,則該變量被置為 nil究珊。
- 若使用附有 __weak 修飾符的的變量薪者,即是使用注冊到 autoreleasePool 中的對象。
首先苦银,通過研究 __weak 內(nèi)部實現(xiàn)啸胧,再來看下上面提出的的一個問題:
id __weak obj = [[NSObject alloc] init];
編譯器處理該源碼時,模擬如下:
/// 編譯器的模擬代碼
id obj;
id tmp = objc_msgSend(NSObjct, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&obj);
假如 [[NSObject alloc] init]
生成了對象 A幔虏,因為 __weak 不能持有對象纺念,編譯器認(rèn)為對象 A 沒有持有者,就通過 objc_release(tmp);
函數(shù)釋放和廢棄對象 A想括,所以賦值失敗陷谱,編譯警告。
下面,再來通過合理使用 __weak 分析其內(nèi)部的實現(xiàn):
/// 假設(shè) obj 附加了 __strong 修飾符且對象被賦值
{
id __weak obj1 = obj;
}
該源代碼可轉(zhuǎn)換為如下形式:
/// 編譯器的模擬代碼
id obj1;
objc_initWeak(&obj1, obj);
id temp = objc_loadWeakRetained(&obj1);
objc_autorelease(temp);
objc_destroyWeak(&obj1);
- objc_loadWeakRetaine 函數(shù)取出附有 __weak 修飾符的變量所引用的對象烟逊,并 retain渣窜。
- objc_autorelease 函數(shù)將對象注冊到 autoreleasePool 中。
大量的使用 __weak 修飾符修飾的變量宪躯,注冊到 autoreleasepool 的對象也會大量增加乔宿。因此使用附有 __weak 修飾符的變量時,最好先暫時賦值給附有 __strong 修飾符的變量后在使用访雪。如下:
{
id __weak 0 = obj;
NSLog(@"1 %@", o); // o 注冊到 autoreleasepool 1 次
NSLog(@"2 %@", o); // o 注冊到 autoreleasepool 2 次
NSLog(@"3 %@", o); // o 注冊到 autoreleasepool 3 次
NSLog(@"4 %@", o); // o 注冊到 autoreleasepool 4 次
}
即每次使用详瑞,都會注冊到 autoreleasepool 中。如果先賦值給 __strong 變量:
{
id __weak 0 = obj;
id __strong tmp = o; // o 注冊到 autoreleasepool 1 次
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
}
書中原話是: 在 “tmp = o;” 時對象僅登錄到 autoreleasepool 中 1 次臣缀。
這里再看下 Swift 中常用的寫法坝橡,大概也是這個原因:
loginVC.loginSuccess = { [weak self] phoneNum in
guard let strongSelf = self else {
return
}
}
這里重點說兩個方法:
- objc_initWeak(&obj1, obj): 初始化附有 __weak 修飾符的變量,具體通過執(zhí)行
objc_storeWeak(&obj1, obj)
精置,將附有 __weak 修飾符的變量地址注冊到 weak 表中计寇。 - objc_destroyWeak(&obj1):釋放一個 __weak 變量。具體通過執(zhí)行
objc_storeWeak(&obj1, 0)
脂倦,把變量的地址從 weak 表中刪除番宁。
簡單來說,就是 objc_storeWeak(&obj1, obj)
通過第二個參數(shù)決定是注冊變量地址到 weak 表狼讨,還是刪除地址贝淤。
通常面試到 weak 問題時,都會問下 weak 的實現(xiàn)原理政供,主要問的就是對 weak 表的理解播聪。
weak 表與引用計數(shù)表相同,作為散列表被實現(xiàn)布隔。如果使用 weak 表离陶,將廢棄對象的地址作為鍵值進行檢索,就能高速獲取對應(yīng)的附有 __weak 修飾符的變量的地址衅檀。另外招刨,由于一個對象可以賦值給多個附有 __weak 修飾符的變量,所以一個鍵值哀军,可注冊多個變量的地址沉眶。
當(dāng)對象被釋放時,執(zhí)行流程是:
- objc_release
- 因為引用計數(shù)為 0杉适,所以執(zhí)行 dealloc
- _objc_rootDealloc
- object_dispose
- objct_destructInstance
- objc_clear_deallocating
- 從 weak 表中獲取廢棄對象的地址為鍵值的記錄谎倔。
- 將包含在記錄中所有附有 __weak 修飾符變量的地址,賦值為 nil猿推。
- 從 weak 表中刪除該記錄片习。
- 從引用計數(shù)表眾刪除被廢棄對象的地址為鍵值的記錄捌肴。
由上可知,大量使用 __weak 修飾符的變量藕咏,會消耗相應(yīng)的 CPU 資源状知。我們只在需要避免循環(huán)嗎引用時使用 __weak 修飾符即可。
另外孽查,實際上存在著不支持 __weak 修飾符的類饥悴。但是這種類極為罕見。如 NSMachPort
盲再、allowsWeakReference
铺坞、retainWeakReference
等。知道就好了洲胖。
_unsafe_unretained 修飾符
在 iOS4 以及 OSX Snow Leopard 的應(yīng)用程序中代替 __weak 修飾符的。附有 _unsafe_unretained 修飾符的變量不屬于編譯器的內(nèi)存管理對象坯沪。
這里有了解即可绿映,不在細說。
__autoreleasing 修飾符
__autoreleasing 使用方法
在 ARC 有效時腐晾,用 @autoreleasepool 塊代替 MRC 下 NSAutoreleasePool 類叉弦,用附有 __autoreleasing 修飾符的變量代替 MRC 下 autorelease 方法,即對象被注冊到 autoreleasepool藻糖。
通常淹冰,我們并不需要顯式的調(diào)用 __autoreleasing 修飾符。
在訪問附有 __autoreleasing 修飾符的變量時巨柒,實際上必定要訪問注冊到 autoreleasepool 的對象樱拴。
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]); // NSObject
以下源代碼與其相同:
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [tmp class]); // NSObject
為什么在訪問附有 __autoreleasing 修飾符的變量時,必須要訪問注冊到 autoreleasepool 的對象呢洋满?這是因為 __weak 修飾符只持有對象的弱引用晶乔,而在訪問引用對象的過程中,該對象有可能被廢棄牺勾,如果把要訪問的對象注冊到 autoreleasepool 中正罢,那么在 @autoreleasepool 塊結(jié)束之前都能確保該對象存在。因此驻民,使用 __weak 修飾符的變量就要訪問注冊到 autoreleasepool 中的對象翻具。
上面這段話是書中原話,但是結(jié)合上文 __weak 的實現(xiàn)原理回还,感覺這么描述不是很對裆泳。應(yīng)該說,附有 __weak 修飾符的變量持有的對象懦趋,如果該對象不在 autoreleasepool 中晾虑,則編譯器會將該對象注冊到 autoreleasepool 中,并提供給該變量使用。這個只是個人理解帜篇,作為參考糙捺。
__autoreleasing 內(nèi)部實現(xiàn)
將對象賦值給附有 __autoreleasing 修飾符的變量,等同于 ARC 無效時調(diào)用對象的 autorelease 方法笙隙。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
該源碼主要將 NSObject 類對象注冊到 autoreleasepool 中洪灯,可作如下轉(zhuǎn)換:
/// 編譯器的模擬代碼
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
這里能夠看到 pool 入棧、執(zhí)行autorelease竟痰、出棧 三個方法签钩。在 MRC 下有過詳細說明。
在 alloc/new/copy/mutableCopy 方法群之外的方法中使用注冊到 autoreleasepool 中的對象坏快,會有一些區(qū)別:
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
編譯器模擬代碼如下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
注冊到 autoreleasepool 的方法 objc_autorelease 并沒有變铅檩。
引用計數(shù)
參考:iOS ARC下獲取引用計數(shù)(retain count)
在 ARC 有效時,是無法正常查看一個類當(dāng)前的引用計數(shù)的莽鸿。不過昧旨,可以通過下面三個方法來獲取到:
- 使用 KVC
- 使用私有 API
- 使用CFGetRetainCount
實踐如下:
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
// KVC
NSLog(@"count10 = %@",[obj valueForKey:@"retainCount"]); // 1
NSLog(@"count11 = %@",[obj1 valueForKey:@"retainCount"]); // 2
NSLog(@"address0 = %p", obj); // 0x600001757be0
NSLog(@"address1 = %p", obj1); // 0x600001757be0
// 私有API
OBJC_EXTERN int _objc_rootRetainCount(id);
NSLog(@"count20 = %d",_objc_rootRetainCount(obj)); // 1
NSLog(@"count21 = %d",_objc_rootRetainCount(obj1)); // 2
// 使用CFGetRetainCount
NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj))); // 1
NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj1))); // 2
@autoreleasepool {
id anObj = [[NSObject alloc] init];
id __autoreleasing o = anObj;
NSLog(@"count40 = %@",[anObj valueForKey:@"retainCount"]); // 2
NSLog(@"count40 = %@",[o valueForKey:@"retainCount"]); // 2
}
- 由于弱引用不持有對象,附有 __weak 修飾符的變量不會對原對象的引用計數(shù)產(chǎn)生影響祥得。
- __autoreleasing 持有對象兔沃。對引用計數(shù)產(chǎn)生影響。
為什么 weak 變量指向?qū)ο蟮囊糜嫈?shù)改變了级及,其實我不是很確定乒疏,雖然對象地址一樣,但是可能是 weak 表導(dǎo)致的饮焦。這里不是特別清楚怕吴。
ARC 規(guī)則
在 ARC 有效的情況下編譯源代碼,必須遵守一定的規(guī)則:
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObjct
- 必須遵循內(nèi)存管理的方法命名規(guī)則
- 不要顯式調(diào)用 dealloc
- 使用 @autoreleasepool 代替 NSAutoreleasePool
- 不能使用區(qū)域 NSZone
- 對象型變量不能作為 C 語言結(jié)構(gòu)體(struct/union)的成員
- 顯式轉(zhuǎn)換 id 和 void*
1. 不能使用 retain/release/retainCount/autorelease
ARC 有效時追驴,禁止使用 retain/release/retainCount/autorelease械哟。否則會編譯報錯。
2. 不能使用 NSAllocateObject/NSDeallocateObjct
ARC 有效時殿雪,使用 NSAllocateObject/NSDeallocateObjct 會編譯報錯暇咆。
3. 必須遵循內(nèi)存管理的方法命名規(guī)則
對象的生成/持有的方法必須遵循以下命名規(guī)則:
- alloc
- new
- copy
- mutableCopy
- init
前四種方法和 MRC 下一樣。而關(guān)于init方法的要求則更為嚴(yán)格:
- 必須是實例方法
- 必須返回對象
- 返回對象的類型必須是id類型或方法聲明類的對象類型
4. 不要顯式調(diào)用 dealloc
對象被廢棄時丙曙,不論 ARC 是否有效爸业,都會調(diào)用對象的 dealloc 方法。
ARC 無效時:
-(void)dealloc {
[super dealloc];
}
ARC 有效時亏镰,dealloc 無法顯式調(diào)用扯旷,否則編譯報錯。ARC 會自動處理這個方法索抓,因此也不比書寫 [super dealloc]
钧忽。dealloc 中只需書寫廢棄對象時所要做的操作即可:
-(void) {
// 處理
}
5. 使用 @autoreleasepool 代替 NSAutoreleasePool
ARC 有效時毯炮,使用 NSAutoreleasePool 會編譯報錯。
6. 不能使用區(qū)域 NSZone
不管 ARC 是否有效耸黑,區(qū)域 NSZone 在現(xiàn)在的運行時系統(tǒng)(編譯器宏 OBJC2 被設(shè)定的環(huán)境)中已單純地被忽略桃煎。并且 ARC 有效時,使用 NSZone 會編譯報錯大刊。
7. 對象型變量不能作為 C 語言結(jié)構(gòu)體(struct/union)的成員
C語言的結(jié)構(gòu)體如果存在Objective-C對象型變量为迈,便會引起錯誤,因為C語言在規(guī)約上沒有方法來管理結(jié)構(gòu)體成員的生存周期缺菌。
備注:
這里存在疑問葫辐,書中說:
struct Data {
NSMutableArray *array;
}
會存在編譯錯誤,但是我測試時伴郁,并沒有編譯錯誤耿战。不只是版本升級后修改了,還是我理解有問題焊傅。
8. 顯式轉(zhuǎn)換 id 和 void*
非ARC下昆箕,這兩個類型是可以直接賦值的:
id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
但是在ARC下就會引起編譯錯誤。為了避免錯誤租冠,我們需要通過__bridege來轉(zhuǎn)換。
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//顯式轉(zhuǎn)換
id o = (__bridge id)p;//顯式轉(zhuǎn)換
書中用了大量的篇幅介紹橋接薯嗤,這里暫時不做擴展了顽爹。
屬性
屬性的聲明和所有權(quán)修飾符的對應(yīng)關(guān)系:
屬性關(guān)鍵字 | 所有權(quán)修飾符 |
---|---|
assign | __unsafe_unretained 修飾符 |
copy | __strong 修飾符(但是賦值的是被復(fù)制的對象) |
retain | __strong 修飾符 |
strong | __strong 修飾符 |
unsafe_unretained | __unsafe_unretained 修飾符 |
weak | __weak 修飾符 |
數(shù)組
__unsafe_unretained 修飾符以外的 __strong/__weak/__autoreleasing 修飾符保證其指定的變量初始化為 nil。
書中說了一些 C 語言相關(guān)的數(shù)組處理骆姐,不在細說镜粤。
后記
不論是作為面試知識,還是對 Objective-C 有更深入的了解玻褪,引用計數(shù)都值得我們深入學(xué)習(xí)下肉渴。
《Objective-C高級編程》三篇總結(jié)之一:引用計數(shù)篇