終于明白那些年知其然而不知其所以然的iOS內(nèi)存管理方式

終于明白那些年知其然而不知其所以然的iOS內(nèi)存管理方式

前言

從我開始學習iOS的時候,身邊的朋友、網(wǎng)上的博客都告訴我iOS的內(nèi)存管理是依靠引用計數(shù)的,然后說引用計數(shù)大于1則對象保存在內(nèi)存的堆中而引用計數(shù)等于0則對象銷毀。然后又說在所謂的ARC時代,強指針指向一個對象,則對象不銷毀;一個對象沒有任何一個強指針指向則銷毀....,最后,我想說這些都很有道理的樣子,但是,我還是不清楚為什么引用計數(shù)器為0為什么會被銷毀,為什么一個對象沒有強指針指向就會銷毀,為什么在@property中一個OC對象要使用strong進行修飾 .... 。所以,在學習 Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理后,讓我明白了很多事情。以下是對于這本書里面知識的總結(jié)性內(nèi)容,如果要詳細了解,請閱讀該書籍。

注意:下面的內(nèi)容是適合于已經(jīng)對于iOS內(nèi)存管理有一定了解的程序員

內(nèi)存管理的思考方式

  • 自己生成的對象,自己持有
  • 非自己生成的對象,自己也能持有
  • 不再需要自己持有對象時釋放
  • 非自己持有的對象無法釋放
  1. 自己生成的對象,自己持有

在iOS內(nèi)存管理中有四個關(guān)鍵字,alloc、new、copy、mutableCopy,自身使用這些關(guān)鍵字產(chǎn)生對象,那么自身就持有了對象


    // 使用了alloc分配了內(nèi)存,obj指向了對象,該對象本身引用計數(shù)為1,不需要retain 
    id obj = [[NSObject alloc] init]; 

    // 使用了new分配了內(nèi)存,objc指向了對象,該對象本身引用計數(shù)為1,不需要retain 
    id obj = [NSObject new]; 

  1. 非自己生成的對象,自己也能持有
    // NSMutableArray通過類方法array產(chǎn)生了對象(并沒有使用alloc、new、copy、mutableCopt來產(chǎn)生對象),因此該對象不屬于obj自身產(chǎn)生的
    // 因此,需要使用retain方法讓對象計數(shù)器+1,從而obj可以持有該對象(盡管該對象不是他產(chǎn)生的)
    id obj = [NSMutableArray array];
    [obj retain];
  1. 不再需要自己持有對象時釋放
    
    id obj = [NSMutableArray array];  
    [obj retain];

    // 當obj不在需要持有的對象,那么,obj應(yīng)該發(fā)送release消息
    [obj release];
  1. 無法釋放非自己持有的對象

    // 1. 釋放一個已經(jīng)釋放的對象
    id obj = [[NSObject alloc] init];

    // 已經(jīng)釋放對象
    [obj release];

    // 釋放了對象還進行釋放
    [obj release];


    // 2. 釋放一個不屬于自己的對象
    id obj1 = [obj object]; 
    
    // obj1沒有進行retain操作而進行release操作,使得obj持有對象釋放,造成了野指針錯誤
    [obj1 release];

如上為iOS進行內(nèi)存管理的四種思考方式(記住不論是ARC還是MRC都遵循該思考方式,只是ARC時代這些工作讓編譯器做了)

引用計數(shù)器討論

蘋果對于引用計數(shù)的管理是通過一張引用計數(shù)表進行管理的

引用計數(shù)表.png

我們平常在操作對象的引用計數(shù)器時,其實就是對這個引用計數(shù)表進行操作,在獲取到該表的地址以及相應(yīng)對象的內(nèi)存地址,就可以通過對象的內(nèi)存從該表中進行索引獲取到相應(yīng)的引用計數(shù)值,然后根據(jù)用戶的操作來返回計時器、計時器加1、計時器減1,下面就深入討論retain、release、alloc、dealloc具體怎么操作該引用計數(shù)表

alloc

當我們調(diào)用alloc函數(shù)時我們進一步會調(diào)用allocWithZone方法


    id obj = [[NSObject alloc] init];


    + (id)alloc {
        return [self allocWithZone:NSDefaultMallocZone()];
    }

    + (id)allocWithZone:(NSZone*)z {
        return NSAllocateObject(self,0,z);
    }

調(diào)用NSAllocateObject函數(shù)對內(nèi)存進行分配

retain、release、retainCount

該書籍對于這三個函數(shù)調(diào)用先是使用GNUstep(一個Cocoa框架的互換框架,功能類似)進行講解,后來又講解了蘋果對于引用計數(shù)的實現(xiàn)。在這里我們就討論蘋果的實現(xiàn)了。

調(diào)用retain、release、retainCount時函數(shù)調(diào)用順序:

retain、retainCount、release函數(shù)調(diào)用順序.png

如下所示,調(diào)用各個函數(shù)時會調(diào)用__CFDoExternRefOperation函數(shù),該函數(shù)包含于CFRuntime.c中,該函數(shù)簡化代碼如下:


- (NSUInteger)retainCount 
{
    return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self);
}

- (id)retain 
{
    return (id)__CFDoExternRefOperation(OPERATION_retain,self);
}

- (void)release 
{
    return __CFDoExternRefOperation(OPERATION_release,self);
}

    int __CFDoExternRefOperation(uintptr_r op,id obj) {
        CFBasicHashRef table = 取得對象對應(yīng)的散列表(obj);
        int count;

        switch(op) {
            case OPERATION_retainCount: 
                count = CFBasicHashGetCountOfKey(table,obj);
                return count; 
            case OPERATION_retain: 
                CFBasicHashAddValue(table,obj);
                return obj; 
            case OPERATION_release: 
                count = CFBasicHashRemoveValue(table,obj):
                return 0 == count;
        }
    }

代碼如上所示,可以想象蘋果就是使用類似于上述的引用計數(shù)表來管理內(nèi)存,也就是說我們在調(diào)用retain、retainCount、release時首先調(diào)用__CFDoExternRefOperation進而獲取到引用技術(shù)表的內(nèi)存地址以及本對象的內(nèi)存地址,然后根據(jù)對象的內(nèi)存地址在表中查詢獲取到引用計數(shù)值。

若是retain就加1
若是retainCount就直接返回值,
若是release則減1而且在CFBasicHashRemoveValue中將引用計數(shù)減少到0時會調(diào)用dealloc,從而調(diào)用NDDeallocateObject函數(shù)、free函數(shù)將對象所在內(nèi)存釋放

以上就是在討論蘋果對于引用計數(shù)的管理方法,對于GNUStep辦法請自行查閱書籍

autorelease

作用:將對象放入自動釋放池中,當自從釋放池銷毀時對自動釋放池中的對象都進行一次release操作
書寫形式:

    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    id obj = [[NSObject alloc] init];

    [obj autorelease];

    [pool drain];   

對于autorelease的實現(xiàn)方式,書籍也對比了GNUSetp與蘋果實現(xiàn)的方式,現(xiàn)在通過GNUStep源代碼來理解蘋果的實現(xiàn)

  1. GNUStep實現(xiàn)
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    - (id)autorelease {
        [NSAutoreleasePool addObject:self];
    }
    + (void)addObject:(id)anObject {
        NSAutoreleasePool *pool = 取得正在使用的Pool對象;  
        if (pool != nil) {
            [pool addObject:anObject];
        }else {
            NSLog(@"NSAutoreleasePool非存在狀態(tài)下使用Pool對象");
        }
    }
    - (void)addObject:(id)anObject {
        [array addObject:anObject];
    }

從上面可以看出,自動釋放池就是通過數(shù)組完成的,我們在調(diào)用autorelease時最終就是將本對象添加到當前自動釋放池的數(shù)組
而針對于自動釋放池銷毀時對數(shù)組中的進行一次release操作,見下面

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    ... 
    // 當自動釋放池銷毀時
    [pool drain];
    - (void)drain {
        [self dealloc];
    }

    - (void)dealloc {
        [self emptyPool];
        [array release];
    }

    - (void)emptyPool {
        for (id obj in array) {
            [obj release];
        }
    }
  1. 蘋果的實現(xiàn)
    class AutoreleasePoolPage 
    {
        static inline void *push() 
        {
            相當于生成或持有NSAutoreleasePool類對象
        }

        static inline void *pop(void *token)
        {
            相當于廢棄NSAutoreleasePool類對象
            releaseAll();
        }

        static inline id autorelease(id obj)
        {
            相當于NSAutoreleasePool類的addObject類方法   
            AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage實例; 
            autoreleasePoolPage->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_autoreleasePoolPage(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }

    id *objc_autorelease(id obj) 
    {
        return AutoreleasePoolPage::autorelease(obj);
    }

如上所示,蘋果內(nèi)部使用了類似于GNUStep中的思想,將對象添加進數(shù)組進行管理

ARC中內(nèi)存管理方式

介紹
關(guān)于這部分的內(nèi)存,作者是分了兩部分進行討論,第一部分介紹ARC管理所需要的關(guān)鍵字__strong 、__weak、__unsafe_unretained、__autoreleasing的作用;第二部分介紹了ARC針對于這些關(guān)
鍵字的具體內(nèi)管管理實現(xiàn)方式。下面我們就綜合兩部分的內(nèi)容進行一次討論

蘋果官方文檔說ARC是有"編譯器自行進行管理",但事實上僅僅是編譯器是不夠,需要滿足下面啷個條件

  • clang(LLVM編譯器)3.0以上
  • objc4 Objective-C運行時庫493.9以上

__strong

作用
    id __strong obj = [[NSObject alloc]init];

如上代碼,表示obj這個強指針指向NSObject對象,且NSObject對象的引用計數(shù)為1

    id __strong obj1 = obj; 

如上代碼,表示obj1這個強指針與obj指針指向同一個NSObject對象,且NSObject對象的引用計數(shù)為2

    id __strong obj = [NSMutableArray array];

如上代碼,表示obj這個強指針指向的NSMutableArray對象的引用計數(shù)為1

綜上所示,當一個對象被強指針指向則引用計數(shù)就加1,否則,該對象沒有一個強指針指向則自動釋放內(nèi)存

那么問題來了,為什么一個對象被強指針指向引用計數(shù)就加1呢? 為什么分配在堆里面的對象內(nèi)存能夠自動釋放內(nèi)存?

原理

第一種情況: 對象是通過alloc、new、copy、multyCopy來分配內(nèi)存的

    id __strong obj = [[NSObject alloc] init];

當使用alloc、new、copy、multyCopt進行對象內(nèi)存分配時,強指針直接指向一個引用計數(shù)為1的對象,在編譯器作用下,上述代碼會轉(zhuǎn)換成以下代碼

    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));

    // 當讓這個代碼會在合適的時候被調(diào)用,不是馬上調(diào)用
    objc_release(obj);

第二種情況: 對象不是自身生成,但是自身持有(一般這樣的對象是通過除alloc、new、copy、multyCopy外方法產(chǎn)生的)

    id __strong obj = [NSMutableArray array];

在這種情況下,obj也指向一個引用計數(shù)為1的對象內(nèi)存,其在編譯器下轉(zhuǎn)換的代碼如下:

    id obj = objc_msgSend(NSMutableArray,@selector(array));

    // 代替我們調(diào)用retain方法,使得obj可以持有該對象
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);

從而使得obj指向了一個引用計數(shù)為1的對象, 不過,objc_retainAutoreleaseReturnValue有一個成對的函數(shù)objc_autoreleaseReturnValue,這兩個函數(shù)可以用于最優(yōu)化程序的運行
如下代碼:

    + (id)array 
    {
        return [[NSMutableArray alloc] init];
    }

代碼轉(zhuǎn)換如下:

    + (id)array 
    {
        id obj = objc_msgSend(NSMutableArray,@selector(alloc));
        objc_msgSend(obj,@selector(init));
        
        // 代替我們調(diào)用了autorelease方法
        return objc_autoreleaseReturnValue(obj);
    }

在轉(zhuǎn)換后的代碼,我們可以看見調(diào)用了objc_autoreleaseReturnValue函數(shù)且這個函數(shù)會返回注冊到自動釋放池的對象,但是,這個函數(shù)有個特點,它會查看調(diào)用方的命令執(zhí)行列表,如果發(fā)現(xiàn)接
下來會調(diào)用objc_retainAutoreleasedReturnValue則不會返回注冊到自動釋放池的對象而僅僅返回一個對象而已。

兩者的關(guān)系圖如下:

關(guān)系圖.png

通過這些,我們就可以通知為什么強指針指向一個對象,這個對象的引用計數(shù)就加1

__weak

作用
    id __weak obj = [[NSObject alloc] init];

根據(jù)我們的知識,可以知道NSObject對象在生成之后立馬就會被釋放,其主要原因是__weak修飾的指針沒有引起對象內(nèi)部的引用計數(shù)器的變化
因此,__weak修飾的指針常用于打破循環(huán)引用或者修飾UI控件,關(guān)于__weak修飾的指針引用場景這里不敘述,下面主要介紹其原理

原理

我們知道弱指針有兩個作用:一. 修飾的指針不會引起指向的對象的引用計數(shù)器變化 二. 當指向的對象被銷毀時,弱指針全部置為nil, 那么除了這些之外,我們還有一個要說的就是,為什么我們
在程序中不能頻繁的使用weak呢?

  1. 為什么弱指針不會引起指向的對象的引用計數(shù)器發(fā)生變化
    id __weak obj = [[NSObject alloc] init];

編譯器轉(zhuǎn)換后的代碼如下:

    id obj;
    id tmp = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(tmp,@selector(init));
    objc_initweak(&obj,tmp);
    objc_release(tmp);
    objc_destroyWeak(&object);

對于__weak內(nèi)存管理也借助了類似于引用計數(shù)表的表,它通過對象的內(nèi)存地址做為key,而對應(yīng)的指針作為value進行管理,在上述代碼中objc_initweak就是完成這部分操作,而objc_destroyWeak
則是銷毀該對象對應(yīng)的value。所以,weak在修飾只是讓weak表增加了記錄沒有引起引用計數(shù)表的變化

  1. 當弱指針指向的對象唄銷毀時,弱指針怎么才能自動置為nil? 為什么我們在程序中不能頻繁使用weak呢

對象通過objc_release釋放對象內(nèi)存的動作如下:

  • objc_release
  • 因為引用計數(shù)為0所以執(zhí)行dealloc
  • _objc_rootDealloc
  • objc_dispose
  • objc_destructInstance
  • objc_clear_deallocating

而在對象被廢棄時最后調(diào)用了objc_clear_deallocating,該函數(shù)的動作如下:

  1. 從weak表中獲取已廢棄對象內(nèi)存地址對應(yīng)的所有記錄
    2)將已廢棄對象內(nèi)存地址對應(yīng)的記錄中所有以weak修飾的變量都置為nil
    3)從weak表刪除已廢棄對象內(nèi)存地址對應(yīng)的記錄
    4)根據(jù)已廢棄對象內(nèi)存地址從引用計數(shù)表中找到對應(yīng)記錄刪除

據(jù)此可以解釋為什么對象被銷毀時對應(yīng)的weak指針變量全部都置為nil,同時,也看出來銷毀weak步驟較多,如果大量使用weak的話會增加CPU的負荷
而不建議大量使用weak,還有一個原因看下面的代碼:

    id __weak obj1 = obj; 
    NSLog(@"obj2-%@",obj1);

編譯器轉(zhuǎn)換上述代碼如下:

    id obj1; 
    objc_initweak(&obj1,obj);

    // 從weak表中獲取附有__weak修飾符變量所引用的對象并retain 
    id tmp = objc_loadWeakRetained(&obj1);

    // 將對象放入自動釋放池
    objc_autorelease(tmp);
    NSLog(@"%@",tmp);
    objc_destroyWeak(&obj1);

據(jù)此當我們訪問weak修飾指針指向的對象時,實際上是訪問注冊到自動釋放池的對象。因此,如果大量使用weak的話,在我們?nèi)ピL問weak修飾的對象時,會有大量對象注冊到自動釋放池,這會影響程
序的性能。推薦方案 : 要訪問weak修飾的變量時,先將其賦給一個strong變量,然后進行訪問

最后一個問題: 為什么訪問weak修飾的對象就會訪問注冊到自動釋放池的對象呢?

  • 因為weak不會引起對象的引用計數(shù)器變化,因此,該對象在運行過程中很有可能會被釋放。所以,需要將對象注冊到自動釋放池中并在自動釋放池銷毀時釋放對象占用的內(nèi)存。

__unsafe_unretained

作用

__unsafe_unretained作用需要和weak進行對比,它也不會引起對象的內(nèi)部引用計數(shù)器的變化,但是,當其指向的對象被銷毀時__unsafr_unretained修飾的指針不會置為nil。而且一般__unsafe_unretained就和它的名字一樣是不安全,它不納入ARC的內(nèi)存管理

__autoreleasing

作用

ARC無效

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];

ARC有效*

    id __autoreleasing obj1 = obj;

如上所示,通過__autoreleasing修飾符就完成了ARC無效時一樣的功能

當然,在某一些情況下我們不通過顯式指定__autoreleasing關(guān)鍵字就可以完成自動注冊到自動釋放池的功能,例如以下情況

第一種:

    @autoeleasepool {
        // 如果看了上面__strong的原理,就知道實際上對象已經(jīng)注冊到自動釋放池里面了 
        id __strong obj = [NSMutableArray array];
    }

第二種:

訪問__weak修飾的對象時,對象就被注冊到了自動釋放池

第三種:

以下形式的默認修飾符是__autorelease

  • id *obj;
  • NSObject **obj;

同時,也引出一個問題: 為什么在@property中OC對象使用strong而基本數(shù)據(jù)類型使用assign?

屬性默認修飾符.png

從表中可以推斷出,在ARC在OC對象的默認修飾符是__strong,因此,在@property中使用strong
而基本數(shù)據(jù)類型是不納入到ARC內(nèi)存管理中的,__unsafe_unretained也不歸ARC管,因此,使用assign對基本數(shù)據(jù)類型進行修飾

原理 ```objc @autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

代碼轉(zhuǎn)換如下:  

```objc
    id pool = objc_autoreleasePoolPush(); 
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);

    @autoreleasepool {
        id __autoreleasing obj = [NSMutableArray array];
    }

代碼轉(zhuǎn)換如下:

    id pool = objc_autoreleasePoolPush();
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_autorelease(obk);
    objc_autoreleasePoolPop(pool);

上述代碼,代表的就是自身生成并持有對象、自身不生成但也持有對象的兩種__autorelease內(nèi)存管理情況

ARC規(guī)則

  • 不能使用retain、release、retainCount、autorelease方法(如果ARC下使用會出現(xiàn)編譯錯誤)

  • 不能使用NSAllocateObject、NSDeallocateObject函數(shù)(如果ARC下使用會出現(xiàn)編譯錯誤)

  • 不要顯式調(diào)用dealloc(ARC下,顯式調(diào)用dealloc并在代碼中書寫[super dealloc]也會出現(xiàn)編譯錯誤)

  • 使用@autoreleasepool塊代替NSAutoreleasePool

    @autoreleasepool{}塊相比較NSAutoreleasePool而言顯得代碼更加整潔、層次性強,而且@autoreleasepool代碼快哉ARC或者非ARC下都是可以使用的
  • 需遵守內(nèi)存管理命名規(guī)則
    1) alloc、new、copy、mutableCopy等以這些名字開頭的方法都應(yīng)當返回調(diào)用方能夠持有的對象  
    2)init開頭的方法必須是實例方法并且要返回對象,返回值要是id或者該方法對應(yīng)類的對象類似或者其超類或者其子類。另外,init開頭的方法也僅僅用作對對象進行初始化操作
  • 不能使用區(qū)域(NSZone)
    區(qū)域是以前為了高效利用內(nèi)存的使用率而設(shè)計的,但是,目前來說ARC下的模式已經(jīng)能夠有效利用內(nèi)存,區(qū)域在ARC下還是非ARC下都已經(jīng)被單純的忽略 
  • 對象型變量不能作為C語言結(jié)構(gòu)體的成員
    OC對象型變量如果成為了C語言結(jié)構(gòu)體的成員,那么,ARC不能掌握該對象的生命周期從而有效管理內(nèi)存,因此,不能這樣使用。 
  • 顯式轉(zhuǎn)換"id" 和 "void*"
    非ARC下:  
    id obj = [[NSObject alloc] init];
    void *p = obj; 
    這樣的代碼是可行的,id和void*可以方便得自由轉(zhuǎn)化 ,但是,在ARC下是不一樣的 

    ARC下id和void*有三個轉(zhuǎn)換的關(guān)鍵字 __bridge、__bridge_retained、__bridge_transfer: 
    id obj = [[NSObject alloc] init]; 
    void *p = (__bridge void*)obj;

    注意: __bridge不會引起對象的引用計數(shù)變化,因此,安全性不太好。相比較,__bridge_retained不僅僅實現(xiàn)了__bridge的功能而且能讓p調(diào)用retain方法使p持有對象。另外,
    __bridge_transfer也是和release方法類似,使用__bridge_transfer進行轉(zhuǎn)化,既讓對象p調(diào)用一次retain方法,而且原來指針obj會調(diào)用一次release方法也非常安全 

最后編輯于
?著作權(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)容