Objective-C-(二)內(nèi)存管理

由于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)存管理機(jī)制:手動管理(MRC)和自動管理(ARC)。ARC的原理其實跟MRC是一致的,只是系統(tǒng)自動幫我們在合適的地方鍵入了內(nèi)存管理的方法,避免了手動管理帶來了麻煩和失誤。目前基本上開發(fā)用的都是ARC。最開始學(xué)習(xí)iOS的時候也用過MRC,先介紹下MRC的機(jī)制。

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方法)機(jī)制,以便于將對象返回給調(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)用者并不持有改對象,不能進(jìn)行釋放操作,否則導(dǎo)致程序崩潰。如果要釋放該對象,需要先對對象進(jìn)行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)前的對象多進(jìn)行一次引用。

- (void)setArray:(NSArray *)array {
    [array retain];  //進(jìn)行了一次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)存的機(jī)制,實現(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表明了一種強(qiáng)引用的關(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)橐爸羔?,如果再次訪問該變量,會導(dǎo)致野指針訪問錯誤?,F(xiàn)在很少會用到該修飾符。
  • __autoreleasing:用于修飾引用傳值的參數(shù)(id *, NSObject **),類似于調(diào)用autorelease方法,在函數(shù)返回該值時會被自動釋放掉。常見于NSError的傳遞中:例如:error:(NSError *__autoreleasing *)error,傳遞error變量的引用,這樣的話才可以在函數(shù)內(nèi)部對error進(jìn)行重新賦值然后返回給調(diào)用者,同時將內(nèi)部的創(chuàng)建的error對象注冊到Autorelease Pool中稍后釋放。

具體用法就不舉例了,平時寫代碼用的都是這些修飾符,不過大部分情況用的是__strong,默認(rèn)省略了這個修飾符而已。

Autorelease Pool

MRC下,我們要使用自動釋放池需要手動創(chuàng)建NSAutoreleasepool,并且要執(zhí)行對象的autorelease方法和NSAutoreleasepooldrain方法銷毀自動釋放池。ARC下我們只需要使用@autoreleasepool語法就可以代替MRC下的NSAutoreleasepoolAutorelease Pool就是提供了一種延遲給對象發(fā)送release消息的機(jī)制。當(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中對圖像進(jìn)行的解碼預(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)引用是指幾個對象(至少兩個對象)之間互相持有強(qiáng)引用形成了一個閉環(huán),導(dǎo)致在超出對象的生命周期后誰都釋放不掉的情況。

導(dǎo)致循環(huán)引用的可能情況:

  • 使用Block互相持有
  • NSTimer強(qiáng)引用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方法中進(jìn)行下面步驟:

  • 獲取當(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ā)送消息后,會進(jìn)入消息轉(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ā)機(jī)制原理

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)原理。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容