最詳細的block底層

主要講述的要點:

  1. block 幹什麼用的

  2. block 語法

  3. block 底層實現

  4. block 變量捕捉

  5. block 的種類、在存儲空間中的存儲位置

  6. block 循環引用

  7. __block 在ARC 中 與 在MRC 中的是否形成循環引用問題

  8. 棧block生命週期

1.首先咱們來講說block幹什麼用的?

block英語中是"塊"的意思, 對就是保存一塊代碼用的, 只不過Block是C語言中的一種擴充數據類型, 把一塊代碼保存到一個Block中, 當你用到的時候利用調用函數的方式, 函數名()調用, 作過其餘面嚮對象語言開發的同窗們很熟悉, 有點像匿名函數的感受, 對它就是objective-c中的匿名函數。  注意: Block是預先準備好的.編譯的時候就肯定完的.c++

2. block 語法

注意: 若是沒有參數的block也能夠省略參數的括號, 只是參數的括號, 不是聲明參數的括號 void (^name)() = ^{};git

3. block 底層實現

 block的語法看上去好像很特別,但其實是做爲極爲普通的C語言代碼來處理的。這裏咱們借住clang編譯器的能力:具備轉化爲咱們可讀源代碼的能力。程序員

咱們定義一個run方法, 裏面實現一個testBlock. 注意: 下面block沒有應用外部變量, 引用外部變量的block跟沒有引用外部變量的block底層仍是有點區別的後面會介紹到.github

void run() {
    void (^testBlock)() = ^{
        NSLog(@"BDTrip\n");
    };
    testBlock();
}

咱們借住clang編譯器查看面試

注意: 說明一下clang的基本語法objective-c

打開終端Console/ iTerm --> 進入當前文件所在位置目錄 --> clang -rewrite-objc main.m 默認是編譯非ARC的代碼 數據結構

                                                                     clang -rewrite-objc -fobjc-arc main.h 這個是編譯成ARC代碼閉包

下面的代碼默認全是非ARC的, 若是是ARC會在下面標註app

// Block 基結構體(就是基類, 因爲都是結構體你懂得, 全部的結構體都有這個屬性), 存放Block 基本信息
struct __block_impl {
    void *isa; // 指向block的類型, 它包含了一個isa指針, 也就是說block也是一個對象(runtime裏面,對象和類都是用結構體表示)。這也是爲何Block能當作id類型的參數進行傳遞。是libSystem提供的地址
    int Flags; // 標誌變量.記錄引用次數, 若是式第一次copy會複製到堆中, 以後在copy只是增長內部的引用計數而已, 因此這個變量很重要
    int Reserved; // 保留變量
    void *FuncPtr; // block實現的函數指針
};
// 保存Block上下文信息
// 命名規則: __FuncName__block_func_blockInFuncNumber
///        FuncName: 所在方法名
///        blockInFuncNumber: 當前block在所在方法內屬於第幾個block
struct __run_block_impl_0 {
    struct __block_impl impl; // block 基本信息
    struct __run_block_desc_0* Desc; // 描述信息
    __run_block_impl_0(void *fp, struct __run_block_desc_0 *desc, int flags=0) { // 結構體構造函數C++ 特有的
        impl.isa = &_NSConcreteStackBlock; // (棧)型Block
        impl.Flags = flags; // 標誌變量,在實現block的內部操做時會用到
        impl.FuncPtr = fp; // block執行時調用的函數指針
        Desc = desc; // 描述信息
    }
};
// Block 內部實現
// 命名規則: __FuncName__block_func_blockInFuncNumber
///        FuncName: 所在方法名
///        blockInFuncNumber: 當前block在所在方法內屬於第幾個block
static void __run_block_func_0(struct __run_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6j_6wv50f5n44z0hbzk3kf4y6dc0000gn_T_main_d5fa75_mi_0);
}
// 描述信息
// 命名規則: __FuncName__block_desc_blockInFuncNumber
///        FuncName: 所在方法名
///        blockInFuncNumber: block在所在方法內屬於第幾個block
static struct __run_block_desc_0 {
    size_t reserved; // 保留字段
    size_t Block_size; // block大小 sizeof(struct __run_block_impl_0)
} __run_block_desc_0_DATA = { 0, sizeof(struct __run_block_impl_0)}; // 初始化一個變量__run_block_desc_0_DATA 爲了給 __run_block_impl_0 初始化的時候賦值用
void run() {
    // 初始化一個無參函數指針testBlock 指向__run_block_impl_0類型的變量的地址. 看起來很麻煩, 咱們來拆解這句代碼
    /// __run_block_func_0 __run_block = __run_block_impl_0((void *)__run_block_func_0, &__run_block_desc_0_DATA);
    /// 解釋: 利用結構體構造函數初始化一個結構體變量 __run_block
    /// void (*testBlock)() = ((void (*)())&__run_block);
    /// 解釋: 利用一個函數指針(void (*)())把結構體變量的地址進行強轉事後, 建立一個testBlock無參函數指針指向
    void (*testBlock)() = ((void (*)())&__run_block_impl_0((void *)__run_block_func_0, &__run_block_desc_0_DATA));
    
    // 調用Block的實現. 看起來也很麻煩, 咱們來拆解這句代碼
    /// void *funcBlock = ((__block_impl *)testBlock)->FuncPtr
    /// 第一步 由於如今testBlock 是函數指針( (void (*)())類型 ), 因此要把testBlock 強制轉換成(__block_impl *)類型, 而後再取出block實現指針FuncPtr
    /// void(*testFuncBlock)(__block_impl *) = (void (*)(__block_impl *))funcBlock
    /// 第二步 block實現指針 強制轉化成((void (*)(__block_impl *))類型指針
    /// testFuncBlock((__block_impl *)testBlock)
    /// 第三步 執行實現指針, 而且 把testBlock強制轉(__block_impl *) 類型傳入實現函數中執行
    ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
}

上面是咱們經過clang作的代碼轉換, 下面咱們看看源碼是什麼樣子, 源碼保存在<block_private.h>這個文件當中, 網上也有地址 https://opensource.apple.com/source/libclosure/libclosure-38/Block_private.hide

struct Block_descriptor {
    unsigned long int reserved; // 保留字 
    unsigned long int size; // block大小 void (*copy)(void *dst, void *src); // 1. C++ 棧上對象 2.OC對象 3. 其餘block對象 4. __block修飾的輔助函數, 處理block範圍外的變量時使用
    void (*dispose)(void *); // 1. C++ 棧上對象 2.OC對象 3. 其餘block對象 4. __block修飾的輔助函數, 處理block範圍外的變量時使用
};

struct Block_layout {
    void *isa; // 這也是爲何Block能當作id類型的參數進行傳遞。是libSystem提供的地址
    int flags; // reference標識: 傳不一樣的值意義也不一樣: (這個標誌代表block須要釋放,在以及時會用到)、BLOCK_IS_GLOBAL全局block
    int reserved; // 保留字 void (*invoke)(void *, ...); // block執行時調用的函數指
    struct Block_descriptor *descriptor; // block的描述信息 // imported variables
};BLOCK_NEEDS_FREErelease再次拷貝

注意: copy 與dispose 是引用外部變量以後纔會涉及, 在下面變量捕捉會說起

官方對其copy 與dispose解釋以下

A Block can reference four different kinds of things that require help when the Block is copied to the heap.
1) C++ stack based objects
2) References to Objective-C objects
3) Other Blocks
4) __block variables
In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers.  The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign.  The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.
The flags parameter of _Block_object_assign and _Block_object_dispose is set to
    * BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,
    * BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and
    * BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.
If the __block variable is marked weak the compiler also or's in BLOCK_FIELD_IS_WEAK (16).
So the Block copy/dispose helpers should only ever generate the four flag values of 3, 7, 8, and 24.
When  a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions.  Similarly to the Block copy helper, the "__block" copy helper (formerly and still a.k.a. "byref" copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor.  And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.
So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or'ed as appropriate and always 128 or'd in, for the following set of possibilities:
    __block id                   128+3
    __weak block id              128+3+16
    __block (^Block)             128+7
    __weak __block (^Block)      128+7+16
        
The implementation of the two routines would be improved by switch statements enumerating the eight cases.

簡單的翻譯就是, 部分被忽略

 當Block被複制到堆時後,Block能夠引用須要幫助的四種不一樣類型的var。
 1) C++ stack based objects
    基於C ++棧的對象
 2) References to Objective-C objects
    引用Objective-C對象
 3) Other Blocks
    其餘Blocks
 4) __block variables
    __block 修飾的變量
 
在這些狀況下,輔助函數由編譯器合併用於Block_copy和Block_release,稱爲複製和處理助手。複製的助手發出調用c++ const基於c++棧對象的拷貝構造函數,而且將其他調用發送到運行時支持函數_Block_object_assign。處理助手調用了case 1的C++析構函數,其他的調用到_Block_object_dispose。
 
 The flags parameter of _Block_object_assign and _Block_object_dispose is set to
    * BLOCK_FIELD_IS_OBJECT (3),證明: objective-c 對象
    * BLOCK_FIELD_IS_BLOCK (7), 證明: 內部引用一個block
    * BLOCK_FIELD_IS_BYREF (8), 證明: 被__block修飾的變量
    * BLOCK_FIELD_IS_WEAK (16)  被__weak修飾的變量,只能被輔助copy函數使用
    * BLOCK_BYREF_CALLER (128)  block輔助函數調用(告訴內部實現不要進行retain或者copy)
 
 因此Block複製/處理助手只能生成3,7,8和24的四個標誌值。
 *** 沒有遇到過
 __block複製/處理助手將分別爲對象和塊生成3或7的標誌值,BLOCK_FIELD_IS_WEAK(16)或適當的時候會爲128或「in」,爲如下幾種可能性:
    __block id                   128+3
    __weak block id              128+3+16
    __block (^Block)             128+7
    __weak __block (^Block)      128+7+16

當Block引用了 1. C++ 棧上對象 2. OC對象 3. 其餘block對象 4. __block修飾的變量,並被拷貝至堆上時則須要copy/dispose輔助函數。輔助函數由編譯器生成。
除了1,其餘三種都會分別調用下面的函數:

void _Block_object_assign(void *destAddr, const void *object, const int flags);
void _Block_object_dispose(const void *object, const int flags);
當Block從棧賦值到堆時,使用_Block_object_assign函數,持有Block引用的對象。 當堆上的Block被廢棄時,使用_Block_object_dispose函數,釋放Block引用的對象。 

4. 變量捕捉

1. 訪問全局變量. 

咱們用全局的block去訪問全局的變量以下代碼:

// 全局block
int i = 0;
void (^globalkTestBlock)() = ^{
    NSLog(@"%d\n", i);
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        i = 4;
        globalkTestBlock();
        return 0;
    }
}

結果

用控制檯解析出來結果能夠看出打印結果爲最後修改的值. 咱們用clang看看底層實現

// 全局變量
int i = 0;

struct __globalkTestBlock_block_impl_0 {
  struct __block_impl impl;
  struct __globalkTestBlock_block_desc_0* Desc;
  __globalkTestBlock_block_impl_0(void *fp, struct __globalkTestBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock; // 全局block
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __globalkTestBlock_block_func_0(struct __globalkTestBlock_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6j_6wv50f5n44z0hbzk3kf4y6dc0000gn_T_main_a4687a_mi_0, i);
}

static struct __globalkTestBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __globalkTestBlock_block_desc_0_DATA = { 0, sizeof(struct __globalkTestBlock_block_impl_0)};

// 靜態變量block變量
static __globalkTestBlock_block_impl_0 __global_globalkTestBlock_block_impl_0((void *)__globalkTestBlock_block_func_0, &__globalkTestBlock_block_desc_0_DATA);

void (*globalkTestBlock)() = ((void (*)())&__global_globalkTestBlock_block_impl_0);

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        i =4;
        ((void (*)(__block_impl *))((__block_impl *)globalkTestBlock)->FuncPtr)((__block_impl *)globalkTestBlock);
        return 0;
    }
}

能夠看出,由於全局變量都是在靜態數據存儲區,在程序結束前不會被銷燬,因此block直接訪問了對應的變量,而沒有在__globalkTestBlock_block_impl_0

結構體中給變量預留位置。

2. 局部變量

咱們用全局的block去訪問全局的變量以下代碼:

void stackTestBlock();
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        stackTestBlock();
    }
    
    return 0;
}

void stackTestBlock() {
    int a = 10;
    void (^testBlock)() = ^{
        NSLog(@"%d\n", a);
    };
    
    a = 20;
    testBlock();
}

用控制檯解析出來結果能夠看出打印結果爲10. 咱們用clang看看底層實現

void stackTestBlock();
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        stackTestBlock();
    }
    
    return 0;
}

struct __stackTestBlock_block_impl_0 {
    struct __block_impl impl;
    struct __stackTestBlock_block_desc_0* Desc;
    int a;
    __stackTestBlock_block_impl_0(void *fp, struct __stackTestBlock_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __stackTestBlock_block_func_0(struct __stackTestBlock_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6j_6wv50f5n44z0hbzk3kf4y6dc0000gn_T_main_80a2be_mi_0, a);
}

static struct __stackTestBlock_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __stackTestBlock_block_desc_0_DATA = { 0, sizeof(struct __stackTestBlock_block_impl_0)};

void stackTestBlock() {
    int a = 10;
    void (*testBlock)() = ((void (*)())&__stackTestBlock_block_impl_0((void *)__stackTestBlock_block_func_0, &__stackTestBlock_block_desc_0_DATA, a));
    
    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
}

能夠看出block引用的a 是值引用

3.__block修飾的變量

咱們用全局的block去訪問全局的變量以下代碼: 

void stackTestBlock();
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        stackTestBlock();
    }
    
    return 0;
}

void stackTestBlock() {
    __block int a = 10;
    void (^testBlock)() = ^{
        NSLog(@"%d\n", a);
    };
    
    a = 20;
    testBlock();
}

用控制檯解析出來結果能夠看出打印結果爲20. 咱們用clang看看底層實現

// block內部訪問的一個外部變量時, 對這個外部變量的包裝
// 命名規則: __Block_byref_var_0
///        var: 外部變量名
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding; // 複製以前, 棧上 block_byref 的 forward 指向自身, 而棧上的 block 使用 block_byref->forward->val 就能夠修改棧上的數據.
                                 // 複製以後,棧上的 block_byref 被複制到堆上, 而棧上的 block_byref 的 forward 被修改爲指向堆上的數據, 而堆上 block_byref 的 forward 指向自身. 這樣不管是棧的 block 或者堆的 block修改變量都是修改的堆中的變量, 保證數據的同步性.
 int __flags;
 int __size;
 int a;
};

// block 內置信息
struct __stackTestBlock_block_impl_0 {
  struct __block_impl impl;
  struct __stackTestBlock_block_desc_0* Desc;
  __Block_byref_a_0 *a; // 外部變量結構體指針
  __stackTestBlock_block_impl_0(void *fp, struct __stackTestBlock_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 內部實現
static void __stackTestBlock_block_func_0(struct __stackTestBlock_block_impl_0 *__cself) {
    // 取值從block內置信息指針 中把結構體(__Block_byref_a_0)a指針取出,
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    // 而後再根據結構體a指針取出內部自身指針(就是__forwarding)最後獲得a變量值
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6j_6wv50f5n44z0hbzk3kf4y6dc0000gn_T_main_1cc9a5_mii_0, (a->__forwarding->a));
}

// 將__Block_byref_a_0 copy 外部var到堆當中
// 命名規則: __FuncName_block_copy_0
///        FuncName: 所在方法名
// 參數:
///     dst: 堆上的block
///     src: 棧上的block
static void __stackTestBlock_block_copy_0(struct __stackTestBlock_block_impl_0*dst, struct __stackTestBlock_block_impl_0*src) {
    /**
     * BLOCK_FIELD_IS_OBJECT (3), 證明: objective-c 對象 == 2進制 11
     * BLOCK_FIELD_IS_BLOCK (7),  證明: 內部引用一個block == 2進制 111
     * BLOCK_FIELD_IS_BYREF (8),  證明: 被__block修飾的變量 == 2進制 1000
     * BLOCK_FIELD_IS_WEAK (16) 被__weak修飾的變量,只能被輔助copy函數使用 == 2進制 10000
     * BLOCK_BYREF_CALLER (128) block輔助函數調用(告訴內部實現不要進行retain或者copy) == 2進制 10000000
     * 
     ** __block修飾的基本類型時會被包裝爲對象.
     */
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/* BLOCK_FIELD_IS_BYREF */);
}

// 釋放copy到堆上Block引用的對象
// 命名規則: __FuncName_block_dispose_0
///        FuncName: 所在方法名
static void __stackTestBlock_block_dispose_0(struct __stackTestBlock_block_impl_0*src) {
    // _Block_object_dispose其餘文件封裝的函數, 上面有函數聲明引用, 當堆上的Block被廢棄時, 釋放Block截獲的對象
    _Block_object_dispose((void*)src->a, 8/* BLOCK_FIELD_IS_BYREF: 表示對象是否進行retain或copy */);
}

// block 描述信息
static struct __stackTestBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __stackTestBlock_block_impl_0*, struct __stackTestBlock_block_impl_0*);
  void (*dispose)(struct __stackTestBlock_block_impl_0*);
} __stackTestBlock_block_desc_0_DATA = { 0, sizeof(struct __stackTestBlock_block_impl_0), __stackTestBlock_block_copy_0, __stackTestBlock_block_dispose_0};

void stackTestBlock() {
    // 外部變量初始化
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    // a是一個結構體, 體內存放變量a值, 把結構體a地址儲存到block內置信息裏
    void (*testBlock)() = ((void (*)())&__stackTestBlock_block_impl_0((void *)__stackTestBlock_block_func_0, &__stackTestBlock_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    // 修改變量值根據結構體自身指針找到本身的內部變量a修改
    (a.__forwarding->a) = 20;
    ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
}

由此看出爲何加上__block後會把地址傳過去, 內部獲取的是指針內部的值, 屬於地址傳遞

注意: 對象傳的是地址, 因此對象地址不變, 操做地址內容是能夠修改的

5. block的種類、 block 在存儲空間中的存儲位置

  • _NSConcreteGlobalBlock(全局)全局的靜態 block,不會訪問任何外部變量
  • _NSConcreteStackBlock(棧)保存在棧中的 block,當函數返回時會被銷燬
  • _NSConcreteMallocBlock(堆)保存在堆中的 block,當引用計數爲 0 時會被銷燬

注意:雖然目前 ARC 編譯器在設置屬性時,已經替程序員複製了 block,可是定義 block時,仍然建議使用 copy 屬性

 _NSConcreteGlobalBlock

void (^globalkTestBlock)() = ^{
    NSLog(@"%s\n", __func__);
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        stackTestBlock();
    }
    
    return 0;
}

利用clang 轉換完的代碼:

struct __globalkTestBlock_block_impl_0 {
  struct __block_impl impl;
  struct __globalkTestBlock_block_desc_0* Desc;
  __globalkTestBlock_block_impl_0(void *fp, struct __globalkTestBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock; // 全局
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

咱們能夠看出block的isa指向了_NSConcreteGlobalBlock 控制檯打印的也能夠看出block在_NSConcreteGlobalBlock被建立

_NSConcreteStackBlock

void stackTestBlock();
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        stackTestBlock();
    }
    
    return 0;
}

void stackTestBlock() {
    int a = 10;
    void (^testBlock)() = ^{
        NSLog(@"%d\n", a);
    };
    
    a = 20;
    testBlock();
}

利用clang 轉換完的代碼:

struct __stackTestBlock_block_impl_0 {
    struct __block_impl impl;
    struct __stackTestBlock_block_desc_0* Desc;
    int a;
    __stackTestBlock_block_impl_0(void *fp, struct __stackTestBlock_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

咱們能夠看出這個block的isa指向了_NSConcreteStackBlock, 固然若是在MRC下也能夠看到控制檯查看打印類型, 若是在ARC下系統會自動copy到堆中, 因此打印出來的結果能夠驗證此結論

ARC 下打印結果:

MRC 下打印結果:

_NSConcreteMallocBlock

一般這種類型的block不會在源碼中出現, 由於1個 block被建立, 纔會將這個 block 複製到堆中, 上面其實已經出現了驗證結果, 在ARC下棧中的block會默認copy到堆中. 而在MRC下根本沒法看到這一場景, 只有在編譯的時候才能肯定block的所在位置

接下來咱們用代碼看看一個block如何copy到堆中的

// 複製block 或 對block var引用計數。若是真的複製,請調用複製助手(存在)。
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    
    if (!arg) return NULL;
    
    aBlock = (struct Block_layout *)arg;
    // 當再次拷貝i時,則僅僅retain其引用計數
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 全局block不會copy到堆中而是在全局區因此直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    
    // 它的一個棧block。copy到堆中。
    if (!isGC) {
        // 在堆中申請一個大小相同的block內存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // memmove用於從aBlock拷貝size個字節到result,若是目標區域和源區域有重疊的話,memmove可以保證源串在被覆蓋以前將重疊區域的字節拷貝到目標區域中。但複製後aBlock內容會被更改。可是當目標區域與源區域沒有重疊則和memcpy函數功能相同。
        memmove(result, aBlock, aBlock->descriptor->size);
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK);
        result->flags |= BLOCK_NEEDS_FREE | 1; // 注入 BLOCK_NEEDS_FREE標識, 之後在copy指會retain引用計數而已
        result->isa = _NSConcreteMallocBlock; // 改變isa指向_NSConcreteMallocBlock,即堆block類型
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) { // 若是有用到輔助函數調用輔助函數 __stackTestBlock_block_copy_0 可是內部實現是調用 _Block_object_assign
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
}

從以上代碼以及註釋能夠很清楚的看出,函數經過memmove將棧中的block的內容拷貝到了堆中,並使isa指向了_NSConcreteMallocBlock
block主要的一些學問就出在棧中block向堆中block的轉移過程當中了。 

6. block循環引用

當咱們在Block使用__strong修飾符的對象類型自動變量,那麼當Block從棧複製到堆時,該對象爲Block所持有。 這樣容易引發循環引用:

下面咱們先看一下循環引用例子: 

緣由: block沒有引用過外部變量會在全局區當中, 可是當block引用外部變量時,會從棧區copy到堆區, 爲何會copy到堆區, 棧你們都知道系統本身維護, block執行完就會被釋放, 因此須要copy到堆當中長期保存, 由程序員手動管理, 那麼問題就來了, 程序員手動管理在內部又引用了其餘對象, 而block在此對象中, 這樣2個對象強引用彼此, 就形成了循環引用現象, 用一幅圖很清晰解釋清楚: 

 

補充: 若是一個方法內部的block沒用引用外部變量也屬於全局block 

因此咱們須要對外界對象進行弱引用轉換

轉換事後的模擬圖

7.__block 在ARC 中 與 在MRC 中的是否形成循環引用問題

爲何要把這個問題單獨提出來? 面試的時候會問到這個. 因此單獨說說爲何.

ARC:

使用以下代碼驗證(ARC環境下)

#import <Foundation/Foundation.h>
#include <stdio.h>
#define logFunc NSLog(@"%s\n", __func__);

typedef void(^TestBlocks)();
@interface TTPerson : NSObject
@property (nonatomic, copy) TestBlocks block;
@end

@implementation TTPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        __block TTPerson *person = self;
        self.block = ^{
            NSLog(@"%@", person);
        };
    }
    return self;
}

- (void)dealloc {
    logFunc
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TTPerson *person = [TTPerson new];
        person.block();
    }
    return 0;
}

運行結果以下: 

能夠看出明顯在ARC中__block引發了循環引用問題__block 在ARC中會形成循環引用. 利用clang 查看一下底層代碼

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TTPerson *__strong person;
};

能夠看到變量用__strong修飾, 會對其內部引用計數加1, 明顯的是對變量強引用操做

MRC

也使用代碼驗證. 

#import <Foundation/Foundation.h>
#define logFunc NSLog(@"%s\n", __func__);

typedef void(^TestBlocks)();
@interface TTPerson : NSObject
@property (nonatomic, copy) TestBlocks block;
@end

@implementation TTPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        __block TTPerson *person = self;
        self.block = ^{
            NSLog(@"%@", person);
            
        };
    }
    return self;
}

- (void)dealloc {
    logFunc
    [super dealloc];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TTPerson *person = [TTPerson new];
        person.block();
        [person release];
    }
    return 0;
}

運行結果: 

能夠看出在MRC下__block是不引發循環引用問題的, 也藉助clang助手.

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TTPerson *person;
};

能夠看出MRC下沒有__strong, 不會操做引用計數, 因此不會形成循環引用

8. block的生命週期

block是編譯時候就須要寫好的, 因此block是在編譯器編譯的時候copy到堆中的. 你們都知道globalblock屬於全局隨app生而生 死而死, 因此不考慮, 只考慮棧block

下面咱們一塊兒看看block如何copy到堆中的. 

首先系統在編譯的時候會調用以下方法

// 個人猜測程序編譯的同時會調用這個方法
void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

內部又調用了_Block_copy_internal

// 複製block 或 對block var引用計數。若是真的複製,請調用複製助手(存在)。
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    
    if (!arg) return NULL;
    
    aBlock = (struct Block_layout *)arg;
    // 當再次拷貝i時,則僅僅retain其引用計數
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 全局block不會copy到堆中而是在全局區因此直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    
    // 它的一個棧block。copy到堆中。
    if (!isGC) {
        // 在堆中申請一個大小相同的block內存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // memmove用於從aBlock拷貝size個字節到result,若是目標區域和源區域有重疊的話,memmove可以保證源串在被覆蓋以前將重疊區域的字節拷貝到目標區域中。但複製後aBlock內容會被更改。可是當目標區域與源區域沒有重疊則和memcpy函數功能相同。
        memmove(result, aBlock, aBlock->descriptor->size);
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK);
        result->flags |= BLOCK_NEEDS_FREE | 1; // 注入 BLOCK_NEEDS_FREE標識, 之後在copy指會retain引用計數而已
        result->isa = _NSConcreteMallocBlock; // 改變isa指向_NSConcreteMallocBlock,即堆block類型
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) { // 若是有用到輔助函數調用輔助函數 __stackTestBlock_block_copy_0 可是內部實現是調用 _Block_object_assign
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
}

當一個對象再次引用不會再次copy會對其內部的引用計數累加

// 當再次拷貝i時,則僅僅retain其引用計數
static int latching_incr_int(int *where) {
    while (1) {
        int old_value = *(volatile int *)where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value+1, (volatile int *)where)) {
            return old_value+1;
        }
    }
}

此方法就是編譯器編譯的時候把block copy到堆當中, 而且能夠看出內部判斷了是否有輔助函數的支持, 若是有會調用copy方法引用外部變量, copy方法是編譯器生成block 結構體描述的時候生成的

// 將__Block_byref_a_0 copy 外部var到堆當中
// 命名規則: __FuncName_block_copy_0
///        FuncName: 所在方法名
// 參數:
///     dst: 堆上的block
///     src: 棧上的block
static void __stackTestBlock_block_copy_0(struct __stackTestBlock_block_impl_0*dst, struct __stackTestBlock_block_impl_0*src) {
    /**
     * BLOCK_FIELD_IS_OBJECT (3), 證明: objective-c 對象 == 2進制 11
     * BLOCK_FIELD_IS_BLOCK (7),  證明: 內部引用一個block == 2進制 111
     * BLOCK_FIELD_IS_BYREF (8),  證明: 被__block修飾的變量 == 2進制 1000
     * BLOCK_FIELD_IS_WEAK (16) 被__weak修飾的變量,只能被輔助copy函數使用 == 2進制 10000
     * BLOCK_BYREF_CALLER (128) block輔助函數調用(告訴內部實現不要進行retain或者copy) == 2進制 10000000
     * 
     ** __block修飾的基本類型會被包裝爲指針對象.
     */
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/* BLOCK_FIELD_IS_BYREF */);
}

// 釋放copy到堆上的Block對象
// 命名規則: __FuncName_block_dispose_0
///        FuncName: 所在方法名
static void __stackTestBlock_block_dispose_0(struct __stackTestBlock_block_impl_0*src) {
    // _Block_object_dispose其餘文件封裝的函數, 上面有函數聲明引用, 當堆上的Block被廢棄時, 釋放Block截獲的對象
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF: 表示對象是否進行retain或copy, 具體表明什麼不詳*/);
}

// block 描述信息
static struct __stackTestBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __stackTestBlock_block_impl_0*, struct __stackTestBlock_block_impl_0*);
  void (*dispose)(struct __stackTestBlock_block_impl_0*);
} __stackTestBlock_block_desc_0_DATA = { 0, sizeof(struct __stackTestBlock_block_impl_0), __stackTestBlock_block_copy_0, __stackTestBlock_block_dispose_0};

咱們能夠看出__stackTestBlock_block_copy_0 就是copy方法, 其內部又調用了 _Block_object_assign 方法

/**
 當Block被複制到堆時後,Block能夠引用須要幫助的四種不一樣類型的var。
 1) C++ stack based objects
    基於C ++棧的對象
 2) References to Objective-C objects
    引用Objective-C對象
 3) Other Blocks
    其餘Blocks
 4) __block variables
    __block 變量
 
 在這些狀況下,輔助函數由編譯器合併用於Block_copy和Block_release,稱爲複製和處理助手。複製的助手發出調用c++ const基於c++棧對象的拷貝構造函數,而且將其他調用發送到運行時支持函數_Block_object_assign。處理助手調用了case 1的C++析構函數,其他的調用到_Block_object_dispose。
 
 The flags parameter of _Block_object_assign and _Block_object_dispose is set to
    * BLOCK_FIELD_IS_OBJECT (3),證明: objective-c 對象11
    * BLOCK_FIELD_IS_BLOCK (7), 證明: 內部引用一個block
    * BLOCK_FIELD_IS_BYREF (8), 證明: 被__block修飾的變量
    * BLOCK_FIELD_IS_WEAK (16)  被__weak修飾的變量,只能被輔助copy函數使用
    * BLOCK_BYREF_CALLER (128)  block輔助函數調用(告訴內部實現不要進行retain或者copy)
 
 因此Block複製/處理助手只能生成3,7,8和24的四個標誌值。
 
 __block複製/處理助手將分別爲對象和塊生成3或7的標誌值,BLOCK_FIELD_IS_WEAK(16)或適當的時候會爲128或「in」,爲如下幾種可能性:
    __block id                   128+3
    __weak block id              128+3+16
    __block (^Block)             128+7
    __weak __block (^Block)      128+7+16
*/
// 參數
/// destAddr: var地址
/// object: var對象
/// flags: var類型
void _Block_object_assign(void *destAddr, const void *object, const int flags) {

    // 引用的object被__block修飾
    if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
        // 將引用的object從棧複製到堆
        // flags將指示它是否包含__weak引用,而且須要一個特殊的isa(只有在GC)
        _Block_byref_assign_copy(destAddr, object, flags);
    }
    // 引用的object是一個block
    else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) {
        // 將引用的object從棧複製到堆, 若是此object已經在堆中會retain引用計數, 不會在次copy
        _Block_assign(_Block_copy_internal(object, flags), destAddr);
    }
    // 引用的object是一個objective-c 對象
    else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) {
        _Block_retain_object(object); // 當咱們沒有開啓arc時,這個函數會retian此object
        _Block_assign((void *)object, destAddr); // 而此函數僅僅是賦值
    }
}

能夠看出內部有3個判斷, 分別調用了 _Block_byref_assign_copy 、 _Block_assign、_Block_retain_object

1.引用的object被__block修飾

// 運行時入口點,用於維護分支數據塊的共享知識. 複製閉包後, 修改引用var時共享同步byref數據, byref 指針已經複製到堆上, 棧上修改堆上須要同步
// var byref指針是否已經被複制到堆中,若是是的話,以後在用var直接retain引用計數。
// 不然,咱們須要複製它, 並更新棧forwarding指針指向
// 參數
/// dest: var地址
/// arg: var對象
/// flags: var類型
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
    // 能夠看到初始化時將flags賦值爲0 表明初次copy var
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 是不是weak(只有在GC) 因此能夠忽略 === fasle
        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
        /// 若是它的弱請求一個對象(僅在GC下重要)
        // 堆中申請一個src大小的內存而且用byref類型指針指向, 其實就是複製var到堆中
        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
        // 下面就是爲copy出來的byref 指針賦值而已
        // 非GC用於調用者,一個用於棧, 植入BLOCK_NEEDS_FREE, 保證後期在次引用不會再copy
        copy->flags = src->flags | _Byref_flag_initial_value;
        /// 同步數據用
        copy->forwarding = copy; // patch 拷貝堆中指針 指向本身
        src->forwarding = copy;  // patch 棧指向堆拷貝
        copy->size = src->size;
        // 若是是weak(只有在GC) 因此不會進
        if (isWeak) {
            copy->isa = &_NSConcreteWeakBlockVariable;  // 標記isa字段,所以它會弱掃描
        }
        // BLOCK_HAS_COPY_DISPOSE 這個byref var具備本身的copy/dispose輔助函數,而此時咱們的內部實現不會進行默認的複製操做
        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            (*src->byref_keep)(copy, src);
        }
        else {
            // just bits.  Blast 'em using _Block_memmove in case they're __strong
            // 只是bits。Blast'em使用_Block_memmove,以防它們是__strong
            _Block_memmove(
                           (void *)&copy->byref_keep,
                           (void *)&src->byref_keep,
                           src->size - sizeof(struct Block_byref_header));
        }
    }
    // already copied to heap
    // 已複製到堆以後在調用
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        // 當再次拷貝i時,則僅僅retain其引用計數
        latching_incr_int(&src->forwarding->flags);
    }
    // assign byref data block pointer into new Block
    //將byref數據塊指針分配給新的Block
    _Block_assign(src->forwarding, (void **)destp); // 這句僅僅是直接賦值,其函數實現只有一行賦值語句,查閱runtime.c可知
}

2. 引用的object是一個block

static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default;
static void _Block_assign_default(void *value, void **destptr) {
    *destptr = value;
}

3. 引用的object是一個objective-c 對象

// 若是在MRC下內部會對其retain
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr) {
    if (!ptr) return;
}
/*
 * 系統首先會調用這個函數, 改變 _Block_retain_object 和_Block_release_object 的指向
 * 把_Block_retain_object 指向 retain若是引用對象能夠作retain操做
 * 把_Block_release_object 指向 release若是引用對象銷燬能夠作release操做
 */
void _Block_use_RR( void (*retain)(const void *),
                   void (*release)(const void *)) {
    _Block_retain_object = retain;
    _Block_release_object = release;
}

到此一個block複製完成, 接着咱們看看block的銷燬

block銷燬系統會首先調用以下方法

// 個人猜測當一個block不在用到須要釋放的時調用這個方法
void _Block_release(void *arg) {
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    int32_t newCount;
    if (!aBlock) return;
    // release引用計數引用
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    // 引用計數>0 不須要銷燬
    if (newCount > 0) return;
    // BLOCK_NEEDS_FREE 這個標誌代表block須要釋放var
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // 表明這個block是有輔助參數的block
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        // 銷燬block
        _Block_deallocator(aBlock);
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }
    else {
        
    }
}

對其block作release操做

// release引用計數
static int latching_decr_int(int *where) {
    while (1) {
        int old_value = *(volatile int *)where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
            return 0;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value-1, (volatile int *)where)) {
            return old_value-1;
        }
    }
}

能夠看出 內部有這麼一句判斷 if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);

當有輔助函數的時候系統會調用dispose, 前面咱們看到block描述結構體初始化的時候, dispose初始化成 __stackTestBlock_block_dispose_0

// 釋放copy到堆上的Block對象
// 命名規則: __FuncName_block_dispose_0
///        FuncName: 所在方法名
static void __stackTestBlock_block_dispose_0(struct __stackTestBlock_block_impl_0*src) {
    // _Block_object_dispose其餘文件封裝的函數, 上面有函數聲明引用, 當堆上的Block被廢棄時, 釋放Block截獲的對象
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF: 表示對象是否進行retain或copy, 具體表明什麼不詳*/);
}

內部調用了_Block_object_dispose 去銷燬內部引用對象

// 當Blocks或Block_byrefs保存對象時,它們的destroy helper(銷燬助手)一般會調用此入口點
// 參數
/// object: var指針
/// flags:  var類型
void _Block_object_dispose(const void *object, const int flags) {
    // 引用的object被__block修飾
    if (flags & BLOCK_FIELD_IS_BYREF)  {
        // 擺脫__block數據結構在一個block
        _Block_byref_release(object);
    }
    
    // 引用的變量是一個block
    else if ((flags & (BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_BLOCK) {
        // 擺脫此block所持有的引用block
        _Block_destroy(object);
    }
    
    // 引用的變量是一個objective-c 對象
    else if ((flags & (BLOCK_FIELD_IS_WEAK|BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_OBJECT) {
        // release引用對象
        _Block_release_object(object);
    }
}

內部也是有3個函數. 對應3種狀況

1. 引用的object被__block修飾

// __block 修飾變量
static void _Block_byref_release(const void *arg) {
    struct Block_byref *shared_struct = (struct Block_byref *)arg;
    int refcount;
    
    // 指向堆中的指針
    shared_struct = shared_struct->forwarding;
    

    // 棧或GC或全局參數
    if ((shared_struct->flags & BLOCK_NEEDS_FREE) == 0) {
        return; // stack or GC or global
    }
    refcount = shared_struct->flags & BLOCK_REFCOUNT_MASK;
    if (refcount <= 0) {
        
    }
    else if ((latching_decr_int(&shared_struct->flags) & BLOCK_REFCOUNT_MASK) == 0) {
        // 若是此指針支持輔助函數
        if (shared_struct->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*shared_struct->byref_destroy)(shared_struct);
        }
        // 最後銷燬當前對象
        _Block_deallocator((struct Block_layout *)shared_struct);
    }
}

2. 引用的變量是一個block

// 釋放一個複製的block, 使用的編譯器在處理helpers中
static void _Block_destroy(const void *arg) {
    struct Block_layout *aBlock;
    if (!arg) return;
    aBlock = (struct Block_layout *)arg;
    // 首先銷燬當前block
    _Block_release(aBlock);
}

3. 引用的變量是一個objective-c 對象

static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default;
// MRC下會對其object作release
static void _Block_release_object_default(const void *ptr) {
    if (!ptr) return;
}
/*
 * 系統首先會調用這個函數, 
 * 把_Block_retain_object 指向 retain若是引用對象能夠作retain操做
 * 把_Block_release_object 指向 release若是引用對象銷燬能夠作release操做
 */
void _Block_use_RR( void (*retain)(const void *),
                   void (*release)(const void *)) {
    _Block_retain_object = retain;
    _Block_release_object = release;
}

把對象都銷燬就此一個block真正的銷燬

參考博文

談Objective-C Block的實現

Block_private.h

Block技巧與底層解析

gitHub的源碼runtime.c 

相關文章
相關標籤/搜索