漫談Block

一、Objective-C發展史

Objective-C從1983年誕生,已經走過了30多年的歷程。隨著時間的推移,Objective-C支持很多特性,下面是幾個重要的發展節點:

  • Object Oriented C —1983
  • Retain and Release
  • Properties —2006(Objective-C2.0發布)
  • Blocks —Mac OS X 10.6(June 8, 2009) & iOS4.0(June 21, 2010)
  • ARC —Mac OS X 10.6&iOS5(2011)

以上發展節點來自這里,時間來自維基百科和其他網站。至于為什么會先說這些,主要我發現很多同學在了解一項技術時,沒有一個立體的概念,經常看后會很快忘記。當我們對一個知識的來龍去脈了解時,這個知識點就不再是個點,而會變成一個面,起到幫助我們記憶的作用。另外我建議大家深入學習某項技術時,不要僅看一些人的分享,現在知識分享已經沒有門檻,經常會看到人云亦云的博客(當然也可能包括我:)),最好的方式就是先從博客上了解知識,然后從官方途徑上去學習驗證。推薦大家幾個網站,第一個當然是蘋果的開發文檔;第二個就是WWDC視頻;如果你還是不盡興,想從源碼方面上了解,可以去看下蘋果開放的源碼-runtimelibDispatchlibclosure,和clang文檔

二、Block的由來

閉包

在講Block之前,我們先了解下閉包的概念:

在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。—維基百科

從上面維基百科的解釋中,我們可以看到,一個閉包即有捕獲自由變量的特性,又具有函數的特性。閉包在我們日常工作中,經常用于回調,尤其在延遲調用(也稱為惰性求值)的情況下非常有用。比如我們經常需要在網絡請求完成時更新UI,網絡請求需要一定時間,在請求結束時再調用更新UI的方法。

Block

先從蘋果官方文檔上,看下關于Block的描述

Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.

簡單來講Block是Apple實現的閉包,它適用于C++,Objective-C,Objective-C++。另外Block需要runtime和clang的支持,下面會講到

三、Block使用

先看一個使用Block的場景,代碼如下:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

Block的使用和函數指針使用非常相似,最大的不同,可能就是函數指針的*變成了^。下面我們看下Block有哪些東西

blocks.jpg

三、Block存儲域

在iOS開發過程中,有三種不同類型的Block,分別是:

  • _NSConcreteStackBlock, 棧上的Block,在出了作用域之后會被釋放
  • _NSConcreteMallocBlock,堆上的Block,在MRR環境需要手動釋放
  • _NSConcreteGlobalBlock,全局的Block,存在data區域,生命周期和應用一樣

在我們初始化Block時只有_NSConcreteStackBlock/_NSConcreteGlobalBlock兩種類型。注意我們是無法生成_NSConcreteMallocBlock的,只有在_NSConcreteStackBlock調用__Block_copy時才會被copy到堆上,生成_NSConcreteMallocBlock。這一點可以從clang文檔中了解到,可能現在看懂這些還比較困難,沒關系,我們下面在Block內部實現會講到。

有兩個問題需要我們注意下:

  • 在初始化Block時,我們怎么判斷是_NSConcreteStackBlock還是_NSConcreteGlobalBlock

    這里有個快速判斷的方法。如果Block的body里使用到了外部的非全局變量和非static靜態變量,那么這個Block就會在棧上創建即_NSConcreteStackBlock。反之如果沒有引用變量或者僅引用了全局變量或者static靜態變量則是全局Block_NSConcreteGlobalBlock,下面有幾個例子:

    // _NSConcreteGlobalBlock
    int multiplier = 7; // 全局變量
    {
        int (^myBlock)(int) = ^(int num) {
          return num * multiplier;
      };
    }
    // _NSConcreteGlobalBlock
    static int multiplier = 7; // 靜態變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteStackBlock MRR環境
    int multiplier = 7; // 局部變量或者實例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteMallocBlock ARC環境
    // 注意這里雖然在打印myBlock時顯示為NSMallocBlock,但是這并不是說我們創建出了_NSConcreteMallocBlock,而是被隱式地調用了copy
    int multiplier = 7; // 局部變量或者實例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
  • Block在MRR環境和ARC環境下使用時,需要注意哪些情況?

    1、在MRR環境下系統提供了兩種方式把Block從棧上copy到堆上,第一種是顯式調用copy,第二種是顯式調用Block_copy宏。當然如果使用屬性的話,則需要用copy標記,如

    @property (copy) int(^Blk)(int)

    而使用retain標記屬性是不行的,大家可以試一下

    2、在ARC環境下,編譯器幫助我們自動插入了copy操作,省去了我們很多的工作量,以下幾種情況編譯器會隱式執行copy操作:

    • 將Block作為函數返回值時
    • 將Block賦值給__strong修改的局部變量,或者標記為strongcopy的屬性時
    • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時

    雖然上面的幾種情況幾乎涵蓋了所有Block使用場景,不過仍然一種情況需要我們考慮到,就是把Block當方法參數傳遞給我們自定義方法時進行傳遞時,系統是不幫我們copy的,在使用時需要我們手動進行copy,如:

    {
        int multiplier = 7; 
        print(^(int num) {
          return num * multiplier;
      };)
    }
    void print(Blk blk) {
        // NSStackBlock
      blk(1);
    }
    

四、Block的內部實現

在看源碼之前,我們先大概捋下如果讓我們自己實現一個閉包該怎么實現,或者需要考慮什么?比如如何捕獲外部變量;如何管理外部變量的內存;調用方式是怎樣的?如果解決了這些問題,我們也能寫一個閉包的實現,無非是沒有編譯器的加持,調用起來不是那么優雅。好了,說了這么多,我們帶著這些疑問去看Block的實現源碼,思路會更加清晰。

我們先準備好clang里的說明文檔(這個文檔解釋Block內部實現)和Block實現源碼

Block的實現主要包括四個文件:Block.hBlock_private.hdata.cruntime.c

  • Block.h

    // Create a heap based copy of a Block or simply add a reference to an existing one.
    // This must be paired with Block_release to recover memory, even when running
    // under Objective-C Garbage Collection.
    BLOCK_EXPORT void *_Block_copy(const void *aBlock);
    
    // Lose the reference, and if heap based and last reference, recover the memory
    BLOCK_EXPORT void _Block_release(const void *aBlock);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_dispose(const void *, const int);
    
    // Used by the compiler. Do not use these variables yourself.
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
    BLOCK_EXPORT void * _NSConcreteStackBlock[32];
    
    #if __cplusplus
    }
    #endif
    
    // Type correct macros
    
    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    #define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
    

    這個文件包括Block向外提供的方法,我們可以看到Block_copy宏實現,其實就是調用了_Block_copy方法,Block_release宏調用了_Block_release方法,所以我們也可以將宏調用改為方法調用。這個文件是暴露出去的,我們可以在工程中看到:路徑在/usr/include/Block.h,這四個文件只有這個文件是暴露的,用來給我們調用

  • data.c

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteAutoBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] = { 0 }; // only used in GC
    

    這個文件定義了6種Block類型。其中三種類型只有GC環境才會出現,因為GC在ARC推出后,已被蘋果廢棄了。所以我們只關心其中三種類型即可,這也印證了我們上面說的三種Block類型

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    
  • Block_private.h

    這個文件就是Block的具體結構

  • runtime.c

    這個文件描述了Block的copy/release和Block持有變量的copy/release操作。

好了,這就是Block的所有文件,Block_private.hruntime.c兩個文件是我們需要重點關注。哦,其實還有一個關聯的文件沒說,先放著不管,在講到runtime.c會引出該文件,這個文件很重要,如果沒有這個文件我們在閱讀到某塊代碼時會有點莫名其妙。

Block的內部結構

首先我們從Block內部結構說起,上面我們已經講到,它在Block_private.h文件中,先貼下Block的結構代碼

#define BLOCK_DESCRIPTOR_1 1
/* Block描述1
 * 如果是_NSConcreteStackBlock時,在copy到堆上時,需要確定Block的大小,用來在堆上分配空間
*/
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

#define BLOCK_DESCRIPTOR_2 1
/* Block描述2
 * 如果是_NSConcreteStackBlock并且捕獲了__block修飾的變量或者(id, NSObject, __attribute__((NSObject)), block, ...)類型的變量,在copy到堆上時需要生成copy方法,這個方法用來解決捕獲的變量在被copy時要執行的動作。
 * 如果是_NSConcreteGlobalBlock則不會生成該結構
 */
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的內部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 這個值主要用來告知系統Block在copy時應該執行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

從上面我們可以了解到,Block由三個struct構成,每個struct我都做了詳細的注釋。Block_layout中的flags在文件中也有描述:

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime  only use in GC
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code  only use in GC
    BLOCK_IS_GC =             (1 << 27), // runtime  only use in GC
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE only use in GC
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler  only use in GC
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler  only use in GC
};

去掉GC環境需要的值

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
};

flags只有這四種值需要我們關注,其中是BLOCK_HAS_COPY_DISPOSEBLOCK_IS_GLOBAL是我們在使用block時由編譯器根據上下文生成的。另外兩個是在Block被copy時,runtime用到和修改的,這里的runtime特指runtime.c文件,在分析runtime.c文件時,再具體說明。

Block在捕獲非全局和非靜態變量時,都是copy的不可變變量,在Block的body里是不能修改這個值的,編譯器會給我們提示如:

(注解:Block的body里使用全局變量或者靜態變量,這些變量并不會進行copy,它只做使用,也不會去管理這些變量的內存,如果Block僅僅使用了全局變量或者靜態變量,Block為_NSConcreteGlobalBlock類型)

block_const_copy.png

如果需要修改變量值,我們可以通過__block修飾變量:

__block int multiplier = 7; // 局部變量或者實例變量
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};
myBlock(1);

__block修飾的變量會被Block_byref這樣的結構包起來,具體如下

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding; // 初始化時會指向自己,當Block被copy時,Block_byref也會被copy到堆上,forwarding會指向堆上的Block_byref
    volatile int32_t flags; // contains ref count
    uint32_t size; // Block_byref大小,用來copy時分配內存
};

// Block_byref被copy到堆上和釋放時需要的操作
struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
};

如果變量屬于id, NSObject, __attribute__((NSObject)), block, …這種類型,則會生成byref_keepbyref_destroy方法,來管理變量的內存,如果是修飾的是自動變量如int,CGPoint,enum類型,則不會生成結構體Block_byref_2

Block如何管理內存

這里引出另外一個文件runtime.c,該文件包含了Block的copy/release、id, NSObject, __attribute__((NSObject)), block, …類型變量的copy/release和__block修飾的變量的copy/release的具體實現。里面有幾個方法是我們需要關注的:

  • Block的copy方法:_Block_copy

    _Block_copy該方法用來確定Block怎樣進行copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

這個方法通過aBlock->flags確定不同類型Block應該怎么copy,flags的值都在Block_private.h文件中,上 面分析這個文件時有講到,如果忘記了,往前翻一下。

flagsBLOCK_IS_GLOBAL即Block的isa_NSConcreteGlobalBlock時直接返回不做任何操作

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    ...
}

當為BLOCK_HAS_COPY_DISPOSE即Block為_NSConcreteStackBlock時,則在堆上生成同樣大小的Block,把堆上的Block的flags改為BLOCK_HAS_COPY_DISPOSE|BLOCK_NEEDS_FREE|2,把isa指向_NSConcreteMallocBlock

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

另外會調用_Block_call_copy_helper方法,這個方法實際上會調用Block_descriptor_2Block_private.h里有講到)的copy方法

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

如果Block已經被copy到堆上,再調用copy方法,則只需要增加Block的引用計數即可,從實現上看,Block在連續調用Block_copy時僅增加了Block的引用計數,并沒有增加對Block持有的變量的引用計數

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    ...
}
  • 上面已經說了,如果捕獲的變量為id, NSObject, __attribute__((NSObject)), block, …類型變量和__block修飾的變量,則需要調用Block_descriptor_2->copy()方法,該方法最終會調用到_Block_object_assign方法(為什么能調用到它,我只能說是編譯器干的,現在不需要關心,等到我們使用clang命令重寫Block的使用時,就會明白了)。

    _Block_object_assign方法用來確定被捕獲的變量怎樣進行copy

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
      default:
        break;
    }
}

該方法根據不同的flags值,執行不同的操作。如果為id, NSObject, __attribute__((NSObject))類型,則flags為BLOCK_FIELD_IS_OBJECT,從上面可以看到調用了_Block_retain_object

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
      ...
    }
}

默認_Block_retain_object方法什么也沒做,如果在MRR環境下,libDispatch會調用_Block_use_RR2方法,_Block_retain_object方法會被賦值為retain操作,具體調用看object.m這個文件的void_os_object_init(void)方法,但是在ARC環境下,該文件是不會編譯的,文件中也做了說明

object.m
#if _OS_OBJECT_OBJC_ARC
#error "Cannot build with ARC"
#endif

_Block_retain_object實現如下

static void _Block_retain_object_default(const void *ptr __unused) { }
// 默認_Block_retain_object被賦值為_Block_retain_object_default,即什么都不做
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
// 上面的注釋其實不是libclosure-65版本的,我從libclosure-63版本摘過來的,不知道蘋果為啥把這個注釋去掉了,讓人讀起源碼來,會感覺莫名其妙
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}

通過上面的分析,可以知道如果在MRR環境下,Block會通過_Block_retain_object方法持有id, NSObject, __attribute__((NSObject))類型變量。而在ARC環境下,_Block_retain_object方法是個空操作,并不會持有該類型變量,那ARC環境下Block怎么樣達到持有對象類型的變量呢?ARC環境有了更完善的內存管理,如果外部變量由__strongcopystrong修飾時,Block會把捕獲的變量用__strong來修飾進而達到持有的目的。

在使用Block時,最大的困擾就是RetainCycle(循環引用),原因很簡單:對象A持有了Block,Block又持有了對象A。因為Block持有了對象A,所以對象A想釋放則必須要先釋放Block,而Block又由于被對象A持有也釋放不了,這就造成了循環引用。解決循環引用有兩種辦法,一種是手動把Block置為nil來釋放Block對對象A的引用;另外一種就是禁止Block強持有對象A,在MRR和ARC環境下禁止Block持有對象A的做法是不一樣,ARC環境下只需要把變量加上__weak修飾就可以避免Block持有變量;而在MRR環境下只能通過避免調用_Block_retain_object方法,怎么避免呢,可以往下繼續看

(這里吐槽下,蘋果的文檔真是亂,什么地方都有,找起來很麻煩不說,libDispatch里面竟然也有Block實現,看文檔的日期,竟然比libclosure-65還新,一度讓我不知道該看哪個,最后我發現,libDispatch里的版本其實不是最新的Block實現,比如libclosure-65版本廢棄了_Block_use_RR方法,徹底使用_Block_use_RR2按照文檔上的解釋,說這種調用方式可以隨意增加方法;()

當變量由__block修飾時,該變量會被打包成Block_byref類型,flags會被標記為BLOCK_FIELD_IS_BYREF

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
      ...
    }
}

我們可以看到實際上是調用了_Block_byref_copy方法

該方法先在堆上生成同樣大小的Block_byref賦值給堆上的Block,并把flags設置為src->flags | BLOCK_BYREF_NEEDS_FREE | 4

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            ...

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    ...
    return src->forwarding;
}

從上面可以看到最終會調用Block_byref的copy方法,該方法又會調用_Block_object_assign方法,如果__block修飾的變量是id, NSObject, __attribute__((NSObject))類型,則flags會被設置成BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT,我們看到僅僅做了賦值操作。所以通過__block修飾可以避免調用到_Block_retain_object方法,也就是在MRR環境下我們可以通過__block來避免Block強持有變量,進而避免循環引用

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
    }
}

當然Block也支持嵌套Block使用,flags會被標記為BLOCK_FIELD_IS_BLOCK,被捕獲的Block被copy就是調用上面的_Block_copy方法

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break; 
      ...
    }
}

回頭看下runtime.c這個文件的幾個方法

_Block_copy方法用來確定_NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock三種類型在Block調用copy時執行的動

_Block_object_assign方法用來確定Block捕獲了id, NSObject, __attribute__((NSObject)), block, …類型變量和__block修飾的變量在_NSConcreteStackBlock類型Block copy到堆上時執行的動作

_Block_byref_copy方法用來確定被__block修飾的變量在_NSConcreteStackBlock類型Block copy到堆上時執行的動作

通過上面的分析,Block在使用不同類型的外部變量時,內存管理有一下幾種情況:

  • 當外部變量是全局變量或者static靜態變量時,只使用且不需要管理內存;
  • 當外部變量是值類型如int、CGPoint時進行值const copy,不需要管理內存;
  • 當外部變量是對象型變量id, NSObject, __attribute__((NSObject))時,進行指針const copy,在Block被copy到堆上時增加引用計數用來持有該變量,在MRR環境通過_Block_retain_object方法持有變量,在ARC環境下通過__strong持有變量。這里是解決循環引用的關鍵
  • 當外部變量被__block修飾時,會使用Block_byrefstruct包裝該變量,在Block被copy到堆上時copyBlock_byref,如果__block修飾的是id, NSObject, __attribute__((NSObject))對象型類型,該變量只做指針copy不會增加變量的引用計數。當__block修飾的是值類型時,做值const copy
  • 當外部變量是block類型時,在Block被copy到堆上時,調用_Block_copy 進行持有,如果外部block類型變量也持有變量,則遞歸進行copy

五、Block使用-續

第三部分講到Block怎么樣使用,第四節講到了Block的內部結構和內存管理。這里大家可能有個疑問,Block在使用時,并沒有聲明那一堆struct啊,而且像什么isa、flags值都是誰設置的呢。

先看下Block使用例子:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

再看下Block_private.h文件中Block的結構:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的內部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 這個值主要用來告知系統Block在copy時應該執行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

正常來講,我們應該先聲明Block_layout對象,然后實現invoke方法,告知系統Block是_NSConcreteStackBlock還是_NSConcreteGlobalBlock,再實現BLOCK_DESCRIPTOR_2的copy方法,管理捕獲的對象的內存。可以看到,這種調用方式太麻煩了,而且需要我們深刻理解Block_layout里每個變量的含義,稍不留神就會出錯。為了讓我們使用更簡單,這一堆變量的設置都交給了編譯器去實現。編譯器可以聰明地把上面的結構轉成下面的結構。這里我們借助編譯器前端clang,使用clang -rewrite-objc xxx.m命令重寫成c++代碼,看下編譯器是怎么轉換的

好了,我們先建個工程,假設叫BlockImpl;然后創建文件,比如也叫BlockImpl,把上面那段代碼copy進來

#import "BlockImpl.h"

@implementation BlockImpl
- (void)test {
    int multiplier = 7; // 局部變量或者實例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    myBlock(1);
}
@end

然后,我們在終端中輸入命令clang -rewrite-objc BlockImpl.m(當然你首先要cd 該文件所在文件夾:)),回車,你會發現在BlockImpl.m同級文件夾中生成了一個名字叫BlockImpl.cpp文件。這個文件就是重寫后的c++文件。打開后如下:

#ifndef __OBJC2__
#define __OBJC2__
#endif
struct objc_selector; struct objc_class;
struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
#ifndef _REWRITER_typedef_Protocol
typedef struct objc_object Protocol;
#define _REWRITER_typedef_Protocol
#endif
#define __OBJC_RW_DLLIMPORT extern
...

幾行代碼被重寫成將近10萬行代碼,沒事兒,不要怕,我們只需要關注跟我們相關的代碼,在這我把相關代碼摘出來,如下:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// @implementation BlockImpl

struct __BlockImpl__test_block_impl_0 {
    struct __block_impl impl;
    struct __BlockImpl__test_block_desc_0* Desc;
    int multiplier;
    __BlockImpl__test_block_impl_0(void *fp, struct __BlockImpl__test_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static int __BlockImpl__test_block_func_0(struct __BlockImpl__test_block_impl_0 *__cself, int num) {
    int multiplier = __cself->multiplier; // bound by copy
    return num * multiplier;
}

static struct __BlockImpl__test_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __BlockImpl__test_block_desc_0_DATA = { 0, sizeof(struct __BlockImpl__test_block_impl_0)};

static void _I_BlockImpl_test(BlockImpl * self, SEL _cmd) {
    int multiplier = 7;
    int (*myBlock)(int) = ((int (*)(int))&__BlockImpl__test_block_impl_0((void *)__BlockImpl__test_block_func_0, &__BlockImpl__test_block_desc_0_DATA, multiplier));
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}
// @end

摘完后,代碼是不是清爽了許多,好了,下一步我們就嘗試對應下,看看重寫后的代碼是不是和Block_private.h文件中定義的結構一樣:

1、__BlockImpl__test_block_impl_0__block_impl加起來對應上了Block_layout。這里有個知識點注意下,struct并沒有改變變量在內存的位置,所以這兩個是可以劃等號的

2、__BlockImpl__test_block_desc_0Block_descriptor_1對應

3、Block的body被轉換成了函數指針__BlockImpl__test_block_func_0

從上面的分析,可以看到它們倆是完全可以對應上的。另外我們也可以看到系統為做了一些自動化的工作,比如為isa賦值&_NSConcreteStackBlockmyBlock(1)調用轉換為函數調用((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);;局部變量multiplier被copy到了__BlockImpl__test_block_impl_0上,函數調用時也是使用的__BlockImpl__test_block_impl_0multiplier值,而不是那個局部變量,這也說明了Block不能隨意地修改外部局部變量(當然添加__block修飾符才可以,第三、四部分有講到原因)。

clang -rewrite-objc BlockImpl.m以MRR方式重寫,如果要想要以ARC方式,加上-fobjc-arc-fobjc-runtime=macosx-10.7,即clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,通過-rewrite-object命令重寫下你的應用吧,結合第四部分,你會深刻了解Block的工作方式和循環引用產生的原因和解決方案(第四部分已經加粗說明)

循環引用

很多人搞不明白,ARC的循環引用是怎么引起的,總以為和MRR是一樣的,其實第四部分已經說明過一次了,在這里單拎出來強調下。MRR環境下是_Block_retain_object實現強引用外部變量的,這個可以自己寫下代碼用-rewrite-objc命令重寫下,很容易理解。在ARC環境_Block_retain_object其實是個空操作,在第四部分已經說明。ARC是通過__strong實現變量的持有的,下面我們寫一個循環引用的例子

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    self.myBlock = ^(int num) {
        NSLog(@"%@", self);
    };
    self.myBlock(1);
}
@end

使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,得到

***
// 只貼出來最重要的部分
struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __strong self; // 看這里,Block通過__strong持有了self 
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
***

如果禁止Block的持有,則只能通過__weak修飾self,如下

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    __weak __typeof(self) weakSelf = self;
    self.myBlock = ^(int num) {
        NSLog(@"%@", weakSelf);
    };
    self.myBlock(1);
}
@end

轉化為

struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __weak weakSelf; // 看這里,__weak修飾,Block不再強持有self
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

所以ARC環境下__weak可以解決循環引用

小結

從Objective-C的發展史引出了主題Block。在講Block前,先熟悉了閉包的概念,然后了解到Block其實就是閉包的一種實現。閉包的實質就是捕獲了外部變量的函數,Block要解決捕獲變量和變量內存管理相關的問題。在使用時又用讓編譯器簡化了我們使用的成本。第四部分在講Block的內存管理時,又講到了MRR環境和ARC環境循環引用的原因和解決方案

網上已經有很多篇關于Block的實現,為什么我還要再寫一篇?有兩個原因:其實在2014年底的時候,我寫過一篇關于Block原理的文章,當時還有自己的博客,后來博客到期了,文章也丟了;(;另外我找遍了所有網上的博客,發現千篇一律,而且并沒有說明白循環引用產生的原因和為什么加了__block(MRR)、__weak(ARC)就能避免循環引用。

最后感謝你花了這么長時間看我的這篇絮絮叨叨的文章:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容