《Objective-C 高階程式設計》乾貨三部曲(一):引用計數篇

J_Knight_發表於1970-01-01

總結了Effective Objective-C之後,還想讀一本進階的iOS書,毫不猶豫選中了《Objective-C 高階程式設計》。

這本書有三個章節,我針對每一章節進行總結並加上適當的擴充套件分享給大家。可以從下面這張圖來看一下這三篇的整體結構:

《Objective-C高階程式設計》 乾貨三部曲

注意,這個結構並不和書中的結構一致,而是以書中的結構為參考,稍作了調整。

本篇是第一篇:引用計數,簡單說兩句: Objective-C通過 retainCount 的機制來決定物件是否需要釋放。 每次runloop迭代結束後,都會檢查物件的 retainCount,如果retainCount等於0,就說明該物件沒有地方需要繼續使用它,可以被釋放掉了。無論是手動管理記憶體,還是ARC機制,都是通過對retainCount來進行記憶體管理的。

先看一下手動記憶體管理:

手動記憶體管理

我個人覺得,學習一項新的技術之前,需要先了解一下它的核心思想。理解了核心思想之後,對技術點的把握就會更快一些:

記憶體管理的思想

  • 思想一:自己生成的物件,自己持有。
  • 思想二:非自己生成的物件,自己也能持有。
  • 思想三:不再需要自己持有的物件時釋放物件。
  • 思想四:非自己持有的物件無法釋放。

從上面的思想來看,我們對物件的操作可以分為三種:生成,持有,釋放,再加上廢棄,一共有四種。它們所對應的Objective-C的方法和引用計數的變化是:

物件操作 Objecctive-C方法 引用計數的變化
生成並持有物件 alloc/new/copy/mutableCopy等方法 +1
持有物件 retain方法 +1
釋放物件 release方法 -1
廢棄物件 dealloc方法

用書中的圖來直觀感受一下這四種操作:

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

下面開始逐一解釋上面的四條思想:

思想一:自己生成的物件,自己持有

在生成物件時,使用以下面名稱開頭的方法生成物件以後,就會持有該物件:

  • alloc
  • new
  • copy
  • mutableCopy

舉個?:

id obj = [[NSObject alloc] init];//持有新生成的物件
複製程式碼

這行程式碼過後,指向生成並持有[[NSObject alloc] init]的指標被賦給了obj,也就是說obj這個指標強引用[[NSObject alloc] init]這個物件。

同樣適用於new方法:

id obj = [NSObject new];//持有新生成的物件
複製程式碼

注意: 這種將持有物件的指標賦給指標變數的情況不只侷限於上面這四種方法名稱,還包括以他們開頭的所有方法名稱:

  • allocThisObject
  • newThatObject
  • copyThisObject
  • mutableCopyThatObject

舉個?:

id obj1 = [obj0 allocObject];//符合上述命名規則,生成並持有物件
複製程式碼

它的內部實現:

- (id)allocObject
{
    id obj = [[NSObject alloc] init];//持有新生成的物件
    return obj;
}
複製程式碼

反過來,如果不符合上述的命名規則,那麼就不會持有生成的物件, 看一個不符合上述命名規則的返回物件的createObject方法的內部實現?:

- (id)createObject
{
    id obj = [[NSObject alloc] init];//持有新生成的物件
    [obj autorelease];//取得物件,但自己不持有
    return obj;
}
複製程式碼

經由這個方法返回以後,無法持有這個返回的物件。因為這裡使用了autorelease。autorelease提供了這樣一個功能:在物件超出其指定的生存範圍時能夠自動並正確地釋放(詳細會在後面介紹)。

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

也就是說,生成一個呼叫方不持有的物件是可以通過autorelease來實現的(例如NSMutableArray的array類方法)。

我的個人理解是:通過autorelease方法,使物件的持有權轉移給了自動釋放池。所以實現了:呼叫方拿到了物件,但這個物件還不被呼叫方所持有。

由這個不符合命名規則的例子來引出思想二:

思想二:非自己生成的物件,自己也能持有

我們現在知道,僅僅通過上面那個不符合命名規則的返回物件例項的方法是無法持有物件的。但是我們可以通過某個操作來持有這個返回的物件:這個方法就是通過retain方法來讓指標變數持有這個新生成的物件:

id obj = [NSMutableArray array];//非自己生成並持有的物件
[obj retain];//持有新生成的物件
複製程式碼

注意,這裡[NSMutableArray array]返回的非自己持有的物件正是通過上文介紹過的autorelease方法實現的。所以如果想持有這個物件,需要執行retain方法才可以。

思想三:不再需要自己持有的物件時釋放物件

物件的持有者有義務在不再需要這個物件的時候主動將這個物件釋放。注意,是有義務,而不是有權利,注意兩個詞的不同。

來看一下釋放物件的例子:

id obj = [[NSObject alloc] init];//持有新生成的物件
[obj doSomething];//使用該物件做一些事情
[obj release];//事情做完了,釋放該物件
複製程式碼

同樣適用於非自己生成並持有的物件(參考思想二):

id obj = [NSMutableArray array];//非自己生成並持有的物件
[obj retain];//持有新生成的物件
[obj soSomething];//使用該物件做一些事情
[obj release];//事情做完了,釋放該物件
複製程式碼

可能遇到的面試題:呼叫物件的release方法會銷燬物件嗎? 答案是不會:呼叫物件的release方法只是將物件的引用計數器-1,當物件的引用計數器為0的時候會呼叫了物件的dealloc 方法才能進行釋放物件的記憶體。

思想四:無法釋放非自己持有的物件

在釋放物件的時候,我們只能釋放已經持有的物件,非自己持有的物件是不能被自己釋放的。這很符合常識:就好比你自己才能從你自己的銀行卡里取錢,取別人的卡里的錢是不對的(除非他的錢歸你管。。。只是隨便舉個例子)。

兩種不允許的情況:

1. 釋放一個已經廢棄了的物件

id obj = [[NSObject alloc] init];//持有新生成的物件
[obj doSomething];//使用該物件
[obj release];//釋放該物件,不再持有了
[obj release];//釋放已經廢棄了的物件,崩潰
複製程式碼

2. 釋放自己不持有的物件

id obj = [NSMutableArray array];//非自己生成並持有的物件
[obj release];//釋放了非自己持有的物件
複製程式碼

思考:哪些情況會使物件失去擁有者呢?

  1. 將指向某物件的指標變數指向另一個物件。
  2. 將指向某物件的指標變數設定為nil。
  3. 當程式釋放物件的某個擁有者時。
  4. 從collection類中刪除物件時。

現在知道了引用計數式記憶體管理的四個思想,我們再來看一下四個操作引用計數的方法:

alloc/retain/release/dealloc的實現

某種意義上,GNUstep 和 Foundation 框架的實現是相似的。所以這本書的作者通過GNUstep的原始碼來推測了蘋果Cocoa框架的實現。

下面開始針對每一個方法,同時用GNUstep和蘋果的實現方式(追蹤程式的執行和作者的猜測)來對比一下各自的實現。

GNUstep實現:

alloc方法

//GNUstep/modules/core/base/Source/NSObject.m alloc:

+ (id) alloc
{
    return [self allocWithZone: NSDefaultMallocZone()];
}
 
+ (id) allocWithZone: (NSZone*)z
{
    return NSAllocateObject(self, 0, z);
}
複製程式碼

這裡NSAllocateObject方法分配了物件,看一下它的內部實現:

//GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:

struct obj_layout {
    NSUInteger retained;
};
 
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
    int size = 計算容納物件所需記憶體大小;
    id new = NSZoneMalloc(zone, 1, size);//返回新的例項
    memset (new, 0, size);
    new = (id)&((obj)new)[1];
}
複製程式碼
  1. NSAllocateObject函式通過NSZoneMalloc函式來分配存放物件所需要的記憶體空間。
  2. obj_layout是用來儲存引用計數,並將其寫入物件記憶體頭部。

物件的引用計數可以通過retainCount方法來取得:

GNUstep/modules/core/base/Source/NSObject.m retainCount:

- (NSUInteger) retainCount
{
    return NSExtraRefCount(self) + 1;
}
 
inline NSUInteger
NSExtraRefCount(id anObject)
{
    return ((obj_layout)anObject)[-1].retained;
}
複製程式碼

我們可以看到,給NSExtraRefCount傳入anObject以後,通過訪問物件記憶體頭部的.retained變數,來獲取引用計數。

retain方法

//GNUstep/modules/core/base/Source/NSObject.m retain:

- (id)retain
{
    NSIncrementExtraRefCount(self);
    return self;
}
 
inline void NSIncrementExtraRefCount(id anObject)
{
    //retained變數超出最大值,丟擲異常
    if (((obj)anObject)[-1].retained == UINT_MAX - 1){
        [NSException raise: NSInternalInconsistencyException
        format: @"NSIncrementExtraRefCount() asked to increment too far”];
    }
    
    ((obj_layout)anObject)[-1].retained++;//retained變數+1
}
複製程式碼

release方法

//GNUstep/modules/core/base/Source/NSObject.m release

- (void)release
{
    //如果當前的引用計數 = 0,呼叫dealloc函式
    if (NSDecrementExtraRefCountWasZero(self))
    {
        [self dealloc];
    }
}
 
BOOL NSDecrementExtraRefCountWasZero(id anObject)
{
    //如果當前的retained值 = 0.則返回yes
    if (((obj)anObject)[-1].retained == 0){
        return YES;
    }
    
    //如果大於0,則-1,並返回NO
    ((obj)anObject)[-1].retained--;
    return NO;
}
複製程式碼

dealloc方法

//GNUstep/modules/core/base/Source/NSObject.m dealloc

- (void) dealloc
{
    NSDeallocateObject (self);
}
 
inline void NSDeallocateObject(id anObject)
{
    obj_layout o = &((obj_layout)anObject)[-1];
    free(o);//釋放
}
複製程式碼

總結一下上面的幾個方法:

  • Objective-C物件中儲存著引用計數這一整數值。
  • 呼叫alloc或者retain方法後,引用計數+1。
  • 呼叫release後,引用計數-1。
  • 引用計數為0時,呼叫dealloc方法廢棄物件。

下面看一下蘋果的實現:

蘋果的實現

alloc方法

通過在NSObject類的alloc類方法上設定斷點,我們可以看到執行所呼叫的函式:

  • +alloc
  • +allocWithZone:
  • class_createInstance//生成例項
  • calloc//分配記憶體塊

retainCount:

  • __CFdoExternRefOperation
  • CFBasicHashGetCountOfKey

retain方法

  • __CFdoExternRefOperation
  • CFBasicHashAddValue

release方法

  • __CFdoExternRefOperation
  • CFBasicHashRemoveValue

我們可以看到他們都呼叫了一個共同的 __CFdoExternRefOperation 方法。

看一下它的實現:

int __CFDoExternRefOperation(uintptr_t op, id obj) {

    CFBasicHashRef table = 取得物件的雜湊表(obj);
    int count;
 
    switch (op) {
    case OPERATION_retainCount:
        count = CFBasicHashGetCountOfKey(table, obj);
        return count;
        break;

    case OPERATION_retain:
        count = CFBasicHashAddValue(table, obj);
        return obj;
    
    case OPERATION_release:
        count = CFBasicHashRemoveValue(table, obj);
        return 0 == count;
    }
}
複製程式碼

可以看出,__CFDoExternRefOperation通過switch語句 針對不同的操作來進行具體的方法呼叫,如果 op 是 OPERATION_retain,就去掉用具體實現 retain 的方法,以此類推。

可以猜想上層的retainCount,retain,release方法的實現:

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

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

//這裡返回值應該是id,原書這裡應該是錯了
- (id)release
{
    return (id)____CFDoExternRefOperation(OPERATION_release,self);
}
複製程式碼

我們觀察一下switch裡面每個語句裡的執行函式名稱,似乎和雜湊表(Hash)有關,這說明蘋果對引用計數的管理應該是通過雜湊表來執行的。

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

在這張表裡,key為記憶體塊地址,而對應的值為引用計數。也就是說,它儲存了這樣的資訊:一些被引用的記憶體塊各自對應的引用計數。

那麼使用雜湊表來管理記憶體有什麼好處呢?

因為計數表儲存記憶體塊地址,我們就可以通過這張表來:

  • 確認損壞記憶體塊的位置。
  • 在檢測記憶體洩漏時,可以檢視各物件的持有者是否存在。

autorelease

autorelease 介紹

當物件超出其作用域時,物件例項的release方法就會被呼叫,autorelease的具體使用方法如下:

  1. 生成並持有NSAutoreleasePool物件。
  2. 呼叫已分配物件的autorelease方法。
  3. 廢棄NSAutoreleasePool物件。

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

所有呼叫過autorelease方法的物件,在廢棄NSAutoreleasePool物件時,都將呼叫release方法(引用計數-1):

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];//相當於obj呼叫release方法
複製程式碼

NSRunLoop在每次迴圈過程中,NSAutoreleasePool物件都會被生成或廢棄。 也就是說,如果有大量的autorelease變數,在NSAutoreleasePool物件廢棄之前(一旦監聽到RunLoop即將進入睡眠等待狀態,就釋放NSAutoreleasePool),都不會被銷燬,容易導致記憶體激增的問題:

for (int i = 0; i < imageArray.count; i++)
{
    UIImage *image = imageArray[i];
    [image doSomething];
}
複製程式碼

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

因此,我們有必要在適當的時候再巢狀一個自動釋放池來管理臨時生成的autorelease變數:

for (int i = 0; i < imageArray.count; i++)
{
    //臨時pool
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    UIImage *image = imageArray[i];
    [image doSomething];
    [pool drain];
}
複製程式碼

圖片來自:《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

可能會出的面試題:什麼時候會建立自動釋放池? 答:執行迴圈檢測到事件並啟動後,就會建立自動釋放池,而且子執行緒的 runloop 預設是不工作的,無法主動建立,必須手動建立。 舉個?: 自定義的 NSOperation 類中的 main 方法裡就必須新增自動釋放池。否則在出了作用域以後,自動釋放物件會因為沒有自動釋放池去處理自己而造成記憶體洩露。

autorelease實現

和上文一樣,我們還是通過GNUstep和蘋果的實現來分別看一下。

GNUstep 實現

//GNUstep/modules/core/base/Source/NSObject.m autorelease

- (id)autorelease
{
    [NSAutoreleasePool addObject:self];
}
複製程式碼

如果呼叫NSObject類的autorelease方法,則該物件就會被追加到正在使用的NSAutoreleasePool物件中的陣列裡(作者假想了一個簡化的原始碼):

//GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject

+ (void)addObject:(id)anObj
{
    NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool物件
    if (pool != nil){
        [pool addObject:anObj];
    }else{
        NSLog(@"NSAutoreleasePool物件不存在");
    }
}

- (void)addObject:(id)anObj
{
    [pool.array addObject:anObj];
}
複製程式碼

也就是說,autorelease例項方法的本質就是呼叫NSAutoreleasePool物件的addObject類方法,然後這個物件就被追加到正在使用的NSAutoreleasePool物件中的陣列裡。

再來看一下NSAutoreleasePool的drain方法:

- (void)drain
{
    [self dealloc];
}

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

- (void)emptyPool
{
    for(id obj in array){
        [obj release];
    }
}
複製程式碼

我們可以看到,在emptyPool方法裡,確實是對陣列裡每一個物件進行了release操作。

蘋果的實現

我們可以通過objc4/NSObject.mm來確認蘋果中autorelease的實現:

objc4/NSObject.mm AutoreleasePoolPage
 
class AutoreleasePoolPage
{
    static inline void *push()
    {
        //生成或者持有 NSAutoreleasePool 類物件
    }

    static inline void pop(void *token)
    {
        //廢棄 NSAutoreleasePool 類物件
        releaseAll();
    }
    
    static inline id autorelease(id obj)
    {
        //相當於 NSAutoreleasePool 類的 addObject 類方法
        AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 例項;
       autoreleaesPoolPage->add(obj)
    }

    id *add(id obj)
    {   
        //將物件追加到內部陣列中
    }
    
    void releaseAll()
    {
        //呼叫內部陣列中物件的 release 方法
    }
};

//壓棧
void *objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}
 
//出棧
void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}
複製程式碼

來看一下外部的呼叫:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同於 objc_autoreleasePoolPush
 
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同於 objc_autorelease(obj)
 
[NSAutoreleasePool showPools];
// 檢視 NSAutoreleasePool 狀況
 
[pool drain];
// 等同於 objc_autoreleasePoolPop(pool)
複製程式碼

看函式名就可以知道,對autorelease分別執行push、pop操作。銷燬物件時執行release操作。

可能出現的面試題:蘋果是如何實現autoreleasepool的? autoreleasepool以一個佇列陣列的形式實現,主要通過下列三個函式完成. • objc_autoreleasepoolPush(壓入) • objc_autoreleasepoolPop(彈出) • objc_autorelease(釋放內部)

ARC記憶體管理

記憶體管理的思想

上面學習了非ARC機制下的手動管理記憶體思想,針對引用計數的操作和自動釋放池的相關內容。現在學習一下在ARC機制下的相關知識。

ARC和非ARC機制下的記憶體管理思想是一致的:

  • 自己生成的物件,自己持有。
  • 非自己生成的物件,自己也能持有。
  • 不再需要自己持有的物件時釋放物件。
  • 非自己持有的物件無法釋放。

在ARC機制下,編譯器就可以自動進行記憶體管理,減少了開發的工作量。但我們有時仍需要四種所有權修飾符來配合ARC來進行記憶體管理

四種所有權修飾符

但是,在ARC機制下我們有的時候需要追加所有權宣告(以下內容摘自官方文件):

  • __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修飾符

__strong修飾符 是id型別和物件型別預設的所有權修飾符:

__strong使用方法:

id obj = [NSObject alloc] init];
複製程式碼

等同於:

id __strong obj = [NSObject alloc] init];
複製程式碼

看一下記憶體管理的過程:

{
    id __strong obj = [NSObject alloc] init];//obj持有物件
}
//obj超出其作用域,強引用失效
複製程式碼

__strong修飾符表示對物件的強引用。持有強引用的變數在超出其作用域時被廢棄。

在__strong修飾符修飾的變數之間相互賦值的情況:

id __strong obj0 = [[NSObject alloc] init];//obj0 持有物件A
id __strong obj1 = [[NSObject alloc] init];//obj1 持有物件B
id __strong obj2 = nil;//ojb2不持有任何物件
obj0 = obj1;//obj0強引用物件B;而物件A不再被ojb0引用,被廢棄
obj2 = obj0;//obj2強引用物件B(現在obj0,ojb1,obj2都強引用物件B)
obj1 = nil;//obj1不再強引用物件B
obj0 = nil;//obj0不再強引用物件B
obj2 = nil;//obj2不再強引用物件B,不再有任何強引用引用物件B,物件B被廢棄
複製程式碼

而且,__strong可以使一個變數初始化為nil:id __strong obj0; 同樣適用於:id __weak obj1; id __autoreleasing obj2;

做個總結:被__strong修飾後,相當於強引用某個物件。物件一旦有一個強引用引用自己,引用計數就會+1,就不會被系統廢棄。而這個物件如果不再被強引用的話,就會被系統廢棄。

__strong內部實現:

生成並持有物件:

{
    id __strong obj = [NSObject alloc] init];//obj持有物件
}
複製程式碼

編譯器的模擬程式碼:

id obj = objc_mesgSend(NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);//超出作用域,釋放物件
複製程式碼

再看一下使用命名規則以外的構造方法:

{
    id __strong obj = [NSMutableArray array];
}
複製程式碼

編譯器的模擬程式碼:

id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
複製程式碼

objc_retainAutoreleasedReturnValue的作用:持有物件,將物件註冊到autoreleasepool並返回。

同樣也有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的物件。

__weak修飾符

__weak使用方法:

__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;
}
@end
複製程式碼
{
    id test0 = [[Test alloc] init];//test0強引用物件A
    id test1 = [[Test alloc] init];//test1強引用物件B
    [test0 setObject:test1];//test0強引用物件B
    [test1 setObject:test0];//test1強引用物件A
}
複製程式碼

因為生成物件(第一,第二行)和set方法(第三,第四行)都是強引用,所以會造成兩個物件互相強引用對方的情況:

《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

所以,我們需要打破其中一種強引用:

@interface Test:NSObject
{
    id __weak obj_;//由__strong變成了__weak
}

- (void)setObject:(id __strong)obj;
@end
複製程式碼

這樣一來,二者就只是弱引用對方了:

《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

__weak內部實現

{
    id __weak obj1 = obj;
}
複製程式碼

編譯器的模擬程式碼:

id obj1;
objc_initWeak(&obj1,obj);//初始化附有__weak的變數
id tmp = objc_loadWeakRetained(&obj1);//取出附有__weak修飾符變數所引用的物件並retain
objc_autorelease(tmp);//將物件註冊到autoreleasepool中
objc_destroyWeak(&obj1);//釋放附有__weak的變數
複製程式碼

這確認了__weak的一個功能:使用附有__weak修飾符的變數,即是使用註冊到autoreleasepool中的物件。

這裡需要著重講解一下objc_initWeak方法和objc_destroyWeak方法:

  • objc_initWeak:初始化附有__weak的變數,具體通過執行objc_strongWeak(&obj1, obj)方法,將obj物件以&obj1作為key放入一個weak表(Hash)中。
  • objc_destroyWeak:釋放附有__weak的變數。具體通過執行objc_storeWeak(&obj1,0)方法,在weak表中查詢&obj1這個鍵,將這個鍵從weak表中刪除。

注意:因為同一個物件可以賦值給多個附有__weak的變數中,所以對於同一個鍵值,可以註冊多個變數的地址。

當一個物件不再被任何人持有,則需要釋放它,過程為:

  • objc_dealloc
  • dealloc
  • _objc_rootDealloc
  • objc_dispose
  • objc_destructInstance
  • objc_clear_deallocating
    • 從weak表中獲取廢棄物件的地址
    • 將包含在記錄中的所有附有__weak修飾符變數的地址賦值為nil
    • 從weak表中刪除該記錄
    • 從引用計數表中刪除廢棄物件的地址

__autoreleasing修飾符

__autoreleasing使用方法

ARC下,可以用@autoreleasepool來替代NSAutoreleasePool類物件,用__autoreleasing修飾符修飾變數來替代ARC無效時呼叫物件的autorelease方法(物件被註冊到autoreleasepool)。

《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》

說到__autoreleasing修飾符,就不得不提__weak:

id  __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
複製程式碼

等同於:

id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//實際訪問的是註冊到自動個釋放池的物件
複製程式碼

注意一下兩段等效的程式碼裡,NSLog語句裡面訪問的物件是不一樣的,它說明:在訪問__weak修飾符的變數(obj1)時必須訪問註冊到autoreleasepool的物件(tmp)。為什麼呢?

因為__weak修飾符只持有物件的弱引用,也就是說在將來訪問這個物件的時候,無法保證它是否還沒有被廢棄。因此,如果把這個物件註冊到autoreleasepool中,那麼在@autoreleasepool塊結束之前都能確保該物件存在。

__autoreleasing內部實現

將物件賦值給附有__autoreleasing修飾符的變數等同於ARC無效時呼叫物件的autorelease方法。

@autoreleasepool{
    id __autoreleasing obj = [[NSObject alloc] init];
}
複製程式碼

編譯器的模擬程式碼:

id pool = objc_autoreleasePoolPush();//pool入棧
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);//pool出棧
複製程式碼

在這裡我們可以看到pool入棧,執行autorelease,出棧的三個方法。

ARC下的規則

我們知道了在ARC機制下編譯器會幫助我們管理記憶體,但是在編譯期,我們還是要遵守一些規則,作者為我們列出了以下的規則:

  1. 不能使用retain/release/retainCount/autorelease
  2. 不能使用NSAllocateObject/NSDeallocateObject
  3. 必須遵守記憶體管理的方法名規則
  4. 不要顯式呼叫dealloc
  5. 使用@autorelease塊代替NSAutoreleasePool
  6. 不能使用區域(NSZone)
  7. 物件型變數不能作為C語言結構體的成員
  8. 顯式轉換id和void*

1. 不能使用retain/release/retainCount/autorelease

在ARC機制下使用retain/release/retainCount/autorelease方法,會導致編譯器報錯。

2. 不能使用NSAllocateObject/NSDeallocateObject

在ARC機制下使用NSAllocateObject/NSDeallocateObject方法,會導致編譯器報錯。

3. 必須遵守記憶體管理的方法名規則

物件的生成/持有的方法必須遵循以下命名規則:

  • alloc
  • new
  • copy
  • mutableCopy
  • init

前四種方法已經介紹完。而關於init方法的要求則更為嚴格:

  • 必須是例項方法
  • 必須返回物件
  • 返回物件的型別必須是id型別或方法宣告類的物件型別

4. 不要顯式呼叫dealloc

物件被廢棄時,無論ARC是否有效,系統都會呼叫物件的dealloc方法。

我們只能在dealloc方法裡寫一些物件被廢棄時需要進行的操作(例如移除已經註冊的觀察者物件)但是不能手動呼叫dealloc方法。

注意在ARC無效的時候,還需要呼叫[super dealloc]:

- (void)dealloc
{
    //該物件的處理
    [super dealloc];
}
複製程式碼

5. 使用@autorelease塊代替NSAutoreleasePool

ARC下須使用使用@autorelease塊代替NSAutoreleasePool。

6. 不能使用區域(NSZone)

NSZone已經在目前的執行時系統(__OBC2__被設定的環境)被忽略了。

7. 物件型變數不能作為C語言結構體的成員

C語言的結構體如果存在Objective-C物件型變數,便會引起錯誤,因為C語言在規約上沒有方法來管理結構體成員的生存週期 。

8. 顯式轉換id和void*

非ARC下,這兩個型別是可以直接賦值的

id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
複製程式碼

但是在ARC下就會引起編譯錯誤。為了避免錯誤,我們需要通過__bridege來轉換。

id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//顯式轉換
id o = (__bridge id)p;//顯式轉換
複製程式碼

屬性

來看一下屬性的宣告與所有權修飾符的關係

屬性關鍵字 所有權 修飾符
assign __unsafe_unretained
copy __strong
retain __strong
strong __strong
__unsafe_unretained __unsafe_unretained
weak __weak

說一下__unsafe_unretained: __unsafe_unretained表示存取方法會直接為例項變數賦值。

這裡的“unsafe”是相對於weak而言的。我們知道weak指向的物件被銷燬時,指標會自動設定為nil。而__unsafe_unretained卻不會,而是成為空指標。需要注意的是:當處理非物件屬性的時候就不會出現空指標的問題。

這樣第一章就介紹完了,第二篇會在下週一發布^^

擴充套件文獻:

  1. Apple:Transitioning to ARC Release Notes
  2. 蚊香醬:可能是史上最全面的記憶體管理文章
  3. 微笑和飛飛:可能碰到的iOS筆試面試題(6)--記憶體管理
  4. 《iOS程式設計(第4版)》

本文已經同步到個人部落格:傳送門

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章