由于Objective-C是基于C語言的魁淳,在了解Objective-C內(nèi)存管理前應(yīng)該先了解下C語言的內(nèi)存模型贝润。
簡單回顧下C程序的占用空間的幾個區(qū)域:
- 程序代碼區(qū):存放程序執(zhí)行代碼的區(qū)域
- 靜態(tài)數(shù)據(jù)區(qū):也稱全局?jǐn)?shù)據(jù)區(qū),存放程序中的全局變量完疫。例如:全局變量扔涧,靜態(tài)變量,一般常量旨椒,字符串常量。靜態(tài)數(shù)據(jù)區(qū)的內(nèi)存是由程序終止時由系統(tǒng)自動釋放堵漱。其中靜態(tài)數(shù)據(jù)區(qū)具體又分為兩塊區(qū)域:
- BSS段(Block Started by Symbol):未初始化的全局變量
- 數(shù)據(jù)段(data segment):已初始化的全局變量
- 堆區(qū):由程序員手動管理分配和釋放综慎。通過malloc()、calloc怔锌、free()等函數(shù)操作的就是堆區(qū)的內(nèi)存。
- 棧區(qū):函數(shù)的參數(shù),局部變量等存放在棧區(qū)埃元。棧區(qū)的內(nèi)存由系統(tǒng)自動分配和釋放涝涤。
在Objective-C中創(chuàng)建的對象都分配在堆區(qū),內(nèi)存管理針對的也是這塊區(qū)域岛杀。
Objective-C內(nèi)存管理的核心其實引用計數(shù)阔拳。系統(tǒng)通過對一個對象引用計數(shù)的計算來確認(rèn)是否要釋放對象回收內(nèi)存。Objective-C有兩種內(nèi)存管理機制:手動管理(MRC)和自動管理(ARC)类嗤。ARC的原理其實跟MRC是一致的糊肠,只是系統(tǒng)自動幫我們在合適的地方鍵入了內(nèi)存管理的方法,避免了手動管理帶來了麻煩和失誤遗锣。目前基本上開發(fā)用的都是ARC货裹。最開始學(xué)習(xí)iOS的時候也用過MRC,先介紹下MRC的機制精偿。
MRC
操作對象的四種方式:
- 生成并持有對象:alloc/new/copy/mutableCopy等弧圆, retainCount :+1
- 持有對象:retain,retainCount :+1
- 釋放對象:release笔咽,retainCount :-1
- 廢棄對象:dealloc搔预, 自動釋放內(nèi)存
內(nèi)存管理的四個法則:
- 自己生成的對象,自己持有
- 非自己生成的對象叶组,自己也能持有
- 不再需要自己持有對象的時候釋放對象
- 非自己持有的對象無法釋放
示例代碼:
自己生成的對象拯田,自己持有:
/**
* 以 alloc/new/copy/mutableCopy 等方法創(chuàng)建的對象歸調(diào)用者持有
*/
id obj = [[NSObject alloc] init]; //創(chuàng)建一個NSObject對象返回給變量obj, 并且歸調(diào)用者持有
非自己生成的對象,自己也能持有:
/**
* alloc/new/copy/mutableCopy 等方法以外的方式創(chuàng)建的對象不歸調(diào)用者持有
*/
id obj = [NSMutableArray array]; // 非自己生成的對象甩十,該對象存在船庇,但不歸調(diào)用者持有
[obj retain]; // 如果想持有該對象,需要執(zhí)行retain方法
非自己生成的對象枣氧,且該對象存在是通過autorelease來實現(xiàn)的溢十。autorelease提供了一種使得對象在超出生命周期后能正確的被釋放(通過調(diào)用release方法)機制,以便于將對象返回給調(diào)用者达吞,讓調(diào)用者持有后再釋放對象张弛。否則對象還沒來得及被調(diào)用者持有就被系統(tǒng)釋放了。調(diào)用autorelease后對象不會立刻被釋放酪劫,而是被注冊到autoreleasepool中吞鸭,然后當(dāng)autoreleasepool結(jié)束被銷毀的時候,才會調(diào)用對象的release方法釋放對象覆糟。
不再需要自己持有對象的時候釋放對象:
id obj = [[NSObject alloc] init];
[obj release]; // 釋放自己生成并持有的對象
非自己持有的對象無法釋放:
id obj = [NSMutableArray array];
[obj release]; //由于當(dāng)前的調(diào)用者并不持有改對象刻剥,不能進行釋放操作,否則導(dǎo)致程序崩潰滩字。如果要釋放該對象造虏,需要先對對象進行retain操作御吞。
/**
以上方法在Xcode9中經(jīng)測試發(fā)現(xiàn)如果返回給obj的是NSMutableArray對象,會導(dǎo)致程序崩潰漓藕,但是如果是NSArray就不會陶珠。
*/
MRC下要注意屬性的引用計數(shù)情況。雖然retainCount在獲取引用計數(shù)的時候有時候不準(zhǔn)確享钞,但是也可以用來調(diào)試參考揍诽。例如我們給一個屬性賦值如下:
@interface MemoryRefenceVC ()
@property (nonatomic, copy) NSArray *array;
@end
@implementation MemoryRefenceVC
- (void)viewDidLoad {
[super viewDidLoad];
self.array = [[NSArray alloc] initWithObjects:@1, nil];
NSLog(@"array.retainCount = %ld", _array.retainCount);
}
@end
打印如下:
2018-12-12 17:25:57.607777+0800 XXX[9889:341414] array.retainCount = 2
我們創(chuàng)建了一個對象并且返回給調(diào)用者持有,為什么此時對象的引用計數(shù)是2呢栗竖?
因為在屬性的賦值setter方法中暑脆,會對當(dāng)前的對象多進行一次引用。
- (void)setArray:(NSArray *)array {
[array retain]; //進行了一次retain操作
_array = array;
}
所以此時對象的內(nèi)存引用情況是:alloc創(chuàng)建時retainCount為1狐肢,setter方法中retain了一次引用計數(shù)加1添吗,所以此時retainCount變?yōu)榱?。
類似于如下操作:
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 引用計數(shù)+1
self.array = temp; 引用計數(shù)+1
所以一般在使用屬性賦值的時候一般這么寫:
self.array = [[[NSArray alloc] initWithObjects:@2, nil] autorelease]; //用autorelease抵消一次retain操作
或者:
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil];
self.array = temp;
[temp release];
ARC
ARC是蘋果引入的一種自動管理內(nèi)存的機制处坪,實現(xiàn)的方式就是在編譯的時候在代碼合適的位置自動鍵入內(nèi)存管理的代碼根资。
ARC下內(nèi)存管理思想跟MRC一樣,同樣遵守上面的四個法則同窘。只是ARC下已經(jīng)沒有了上面的retain玄帕、release、autorelease等直接操作對象內(nèi)存管理的方法想邦。ARC下Objective-C采用所有權(quán)修飾符來管理對對象的引用情況裤纹。
- __strong :變量的默認(rèn)修飾符,默認(rèn)不指定的話就是__strong丧没。__strong表明了一種強引用的關(guān)系鹰椒,表示當(dāng)前修飾的變量持有對象,類似于MRC下的retain呕童。
- __weak:與__strong相反漆际,__weak表明一種弱引用的關(guān)系,表示當(dāng)前修飾的變量并不會持有該對象夺饲,當(dāng)對象被系統(tǒng)釋放后奸汇,__weak變量會自動置為nil,比較安全往声,常用于解決循環(huán)引用的情況擂找。
- __unsafe_unretained:同__weak一樣,該修飾符同樣不會持有對象浩销,但是不同的是贯涎,當(dāng)變量指向的對象被系統(tǒng)釋放后,變量不會自動置為nil慢洋,該指針會變?yōu)橐爸羔樚瘤ǎ绻俅卧L問該變量陆盘,會導(dǎo)致野指針訪問錯誤。現(xiàn)在很少會用到該修飾符败明。
- __autoreleasing:用于修飾引用傳值的參數(shù)
(id *, NSObject **)
礁遣,類似于調(diào)用autorelease
方法,在函數(shù)返回該值時會被自動釋放掉肩刃。常見于NSError的傳遞中:例如:error:(NSError *__autoreleasing *)error
,傳遞error
變量的引用杏头,這樣的話才可以在函數(shù)內(nèi)部對error
進行重新賦值然后返回給調(diào)用者盈包,同時將內(nèi)部的創(chuàng)建的error
對象注冊到Autorelease Pool
中稍后釋放。
具體用法就不舉例了醇王,平時寫代碼用的都是這些修飾符呢燥,不過大部分情況用的是__strong
,默認(rèn)省略了這個修飾符而已寓娩。
Autorelease Pool
MRC下叛氨,我們要使用自動釋放池需要手動創(chuàng)建NSAutoreleasepool
,并且要執(zhí)行對象的autorelease
方法和NSAutoreleasepool
的 drain
方法銷毀自動釋放池棘伴。ARC下我們只需要使用@autoreleasepool
語法就可以代替MRC下的NSAutoreleasepool
寞埠。Autorelease Pool
就是提供了一種延遲給對象發(fā)送release消息的機制。當(dāng)你想放棄一個對象的所有權(quán)焊夸,但是又不想這個對象立刻被釋放掉仁连,就可以使用Autorelease Pool
。
ARC下使用Autorelease Pool的場景:當(dāng)在循環(huán)遍歷中創(chuàng)建大量臨時對象的時候阱穗,為了避免內(nèi)存峰值可以使用Autorelease Pool來避免饭冬。例如:
for (int i = 0; i < 100; i++) {
@autoreleasepool {
NSData *data = UIImageJPEGRepresentation(image, 0.7f);
UIImage *image = [UIImage imageWithData:data];
}
}
如果不使用@autoreleasepool
,for循環(huán)內(nèi)部創(chuàng)建出的大量UIImage
對象需要等到循環(huán)結(jié)束時才能釋放揪阶,這樣會導(dǎo)致內(nèi)存暴漲昌抠。當(dāng)指定了@autoreleasepool
后,每次循環(huán)結(jié)束的時候?qū)ο缶蜁会尫诺袈沉牛苊饬藘?nèi)存峰值炊苫。
或者在方法中執(zhí)行一段非常消耗資源的操作時,可以用@autoreleasepool及時釋放掉資源蕴茴。例如SDWebImage中對圖像進行的解碼預(yù)渲染操作劝评。
//摘自SDWebImage
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
循環(huán)引用
循環(huán)引用是指幾個對象(至少兩個對象)之間互相持有強引用形成了一個閉環(huán),導(dǎo)致在超出對象的生命周期后誰都釋放不掉的情況倦淀。
導(dǎo)致循環(huán)引用的可能情況:
- 使用Block互相持有
- NSTimer強引用Target目標(biāo)對象
- 使用delegate
解決循環(huán)引用的方法:
- 使用弱引用weak(__weak)
- 當(dāng)持有的實例完成任務(wù)后賦值為nil
僵尸對象
最近又看了下《Effective Objective-C 2.0》蒋畜,關(guān)于僵尸對象的具體實現(xiàn)仔細(xì)研究了下,做個筆記記錄下撞叽。
僵尸對象是iOS開發(fā)中常用的內(nèi)存管理調(diào)試功能姻成。當(dāng)我們給一個已經(jīng)釋放的對象發(fā)送消息的時候插龄,通過僵尸對象能夠很方便的了解到這個消息的相關(guān)信息,包括當(dāng)前調(diào)用對象所屬的類科展,消息名稱等信息均牢,便于我們查找問題的根源。
僵尸對象的原理:在runtime期間當(dāng)一個對象被釋放后才睹,它不會真正的被系統(tǒng)回收徘跪,而是被轉(zhuǎn)化成一個特殊的僵尸對象。這個對象所占用的內(nèi)存不會被釋放琅攘,當(dāng)再次給這個對象發(fā)送消息時垮庐,僵尸對象會拋出異常,并且描述出當(dāng)前消息的相關(guān)內(nèi)容坞琴。例如:
*** -[__NSArrayI indexOfObject:]: message sent to deallocated instance 0x60000022b500
僵尸對象是如何實現(xiàn)的呢哨查?具體來講分以下幾個步驟:首先runtime會替換掉基類NSObject的dealloc方法,在dealloc方法中進行下面步驟:
獲取當(dāng)前的類名
ClassName
通過拼接
_NSZombie_
前綴創(chuàng)建一個新的僵尸類名:_NSZombie_ClassName
(后綴ClassName為當(dāng)前的類名)通過
_NSZombie_
類拷貝出一個新的類剧辐,并且類名命名為_NSZombie_ClassName
銷毀當(dāng)前的對象寒亥,但是不釋放內(nèi)存(不調(diào)用free()方法)
將當(dāng)前對象的
isa
指針指向新創(chuàng)建的僵尸類(變更對象所屬的類)
_NSZombie_
類以及從其拷貝出來的新的僵尸類都沒有實現(xiàn)任何方法,所以當(dāng)給僵尸對象發(fā)送消息后荧关,會進入消息轉(zhuǎn)發(fā)流程溉奕。___forwarding___
函數(shù)是實現(xiàn)消息轉(zhuǎn)發(fā)流程的核心函數(shù),在這個函數(shù)中先檢測當(dāng)前接收消息的對象所屬的類名忍啤,如果類名的前綴是_NSZombie_
腐宋,表明當(dāng)前的消息發(fā)送的對象是僵尸對象,然后就會做特殊處理:先打印出當(dāng)前消息的相關(guān)信息檀轨,然后終止程序拋出異常胸竞。
偽代碼如下:
dealloc方法中創(chuàng)建僵尸類
- (void)createNSZombie {
//獲取當(dāng)前對象的類名
const char *className = object_getClassName(self);
//創(chuàng)建新的僵尸對象類名
const char *zombieClassName = strcat("_NSZombie_", className);
//根據(jù)僵尸對象類名獲取僵尸對象類(`objc_lookUpClass` 相比 `objc_getClass`,當(dāng)類沒有注冊時不會去調(diào)用類處理回調(diào))
Class zombieClass = objc_lookUpClass(zombieClassName);
//如果不存在参萄,先創(chuàng)建僵尸對象類
if (!zombieClass) {
//獲取_NSZombie_類
Class baseZombieClass = objc_lookUpClass("_NSZombie_");
//這里使用的是`objc_duplicateClass`創(chuàng)建新的類卫枝,`objc_duplicateClass`是直接拷貝目標(biāo)類生成新的類然后賦予新的類名,新的類和_NSZombie_類結(jié)構(gòu)相同讹挎,本類的父類校赤,實例變量和方法都和復(fù)制前一樣。
zombieClass = objc_duplicateClass(baseZombieClass, zombieClassName, 0);
}
//銷毀對象筒溃,但是不釋放對象占用的內(nèi)存
objc_destructInstance(self);
//重新設(shè)置當(dāng)前對象所屬的類马篮,讓其指向新創(chuàng)建的僵尸類
object_setClass(self, zombieClass);
}
消息轉(zhuǎn)發(fā)的實現(xiàn)怜奖,我把整個___forwarding___
函數(shù)的實現(xiàn)都摘錄了浑测,順便回顧下消息轉(zhuǎn)發(fā)的流程
以下代碼摘自:Objective-C 消息發(fā)送與轉(zhuǎn)發(fā)機制原理
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
?
// 調(diào)用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
?
// 僵尸對象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
?
// 調(diào)用 methodSignatureForSelector 獲取方法簽名后再調(diào)用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
?
[receiver forwardInvocation:invocation];
?
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
?
SEL *registeredSel = sel_getUid(selName);
?
// selector 是否已經(jīng)在 Runtime 注冊過
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
?
// The point of no return.
kill(getpid(), 9);
}
?```
這就是整個僵尸對象的實現(xiàn)過程。
OC內(nèi)存管理大概就是這些,要想更深入的理解迁央,可以了解下內(nèi)存管理方法是如何實現(xiàn)的掷匠。下一篇寫下OC的內(nèi)存管理的實現(xiàn)原理。