理解 Block 實現原理

xietao3發表於2019-06-25

Block 是一種帶有自動變數值的匿名函式。

Block 在 iOS 日常開發中會頻繁使用到,使用起來也十分方便,而它的實現原理和機制很多小夥伴卻一無所知。 Block 是一種帶有自動變數值的匿名函式,它能夠自動捕獲函式內使用到的引數,本文將從細節分析 Block 的實現原理。

一、Block 的實現

在探尋 Block 實現原理中,命令列工具Clang是非常實用的,它可以將其轉換成 C++ 原始碼,方便我們瞭解其中的實現原理。

clang -rewrite-objc main.m
複製程式碼

我們可以利用上面的命令,嘗試將下面這段程式碼轉換成 C++ 原始碼,進而分析 Block 的具體實現:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int tempVar = 1;
        void (^blk)(void) = ^() {
            printf("Block var:%d\n", tempVar);
        };
        blk();    
    }
    return 0;
}
複製程式碼

轉換並剔除多餘程式碼後如下:

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tempVar;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tempVar, int flags=0) : tempVar(_tempVar) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int tempVar = __cself->tempVar; // bound by copy
  printf("Block var:%d\n", tempVar);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int tempVar = 1;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, tempVar));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}
複製程式碼

轉換後得到了一大串程式碼,接下來我們一一分析這段程式碼的實際意義。

第一部分是__block_impl,它是Block實現的最底層的結構體:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
複製程式碼
  • isa:表明具有它和物件一樣特性。
  • Flag:為狀態標誌位。
  • Reserved:升級預留記憶體大小。
  • FuncPtr:函式指標。

第二部分__main_block_desc_0是一個管理Block記憶體佔用大小的結構體:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製程式碼
  • Reserved:升級預留記憶體大小。
  • Block_size:Block 的大小。

第三部分為Block實現結構體:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tempVar;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tempVar, int flags=0) : tempVar(_tempVar) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

該結構體的命名邏輯為__函式名_block_impl_函式內順序,接下來檢視結構體成員:

  • impl:為__block_impl型別結構體,參考第一部分。
  • Desc:為__main_block_impl_0結構體例項大小。
  • tempVar:捕獲的自動變數值。
  • __main_block_impl_0:為__main_block_impl_0結構體的建構函式。

第四部分為Block的函式指標指向的函式__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int tempVar = __cself->tempVar; // bound by copy
  printf("Block var:%d\n", tempVar);
}
複製程式碼

__main_block_impl_0結構體中將捕獲的自動變數值作為成員變數,呼叫時先獲取結構體成員變數的值,然後複製使用。

第五部分為main函式轉換後原始碼:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int tempVar = 1;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, tempVar));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}
複製程式碼

除了一個區域性變數tempVar之外,另外 2 行程式碼分別是Block的的初始化部分和呼叫部分。去除部分型別強轉程式碼後如下:

void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
(blk->FuncPtr)(blk);
複製程式碼

第一行程式碼是將前面宣告的__main_block_func_0&__main_block_desc_0_DATA傳入建構函式,得到blk結構體例項。

第二行程式碼則是呼叫blk的函式指標。

二、捕獲變數值

2.1 自動變數

在 Block 中使用外部的區域性變數時,會自動捕獲該變數並且成為 Block 結構體的成員變數,以便在 Block 內部訪問該變數。除此之外,有其他幾種方式可以訪問外部變數,下面是變數型別和對應的作用域:

  1. 自動變數:捕獲至 Block 內。
  2. 靜態變數:作用域內可用。
  3. 全域性變數:整個程式可用。
  4. 靜態全域性變數:當前檔案可用。

通過將下面的程式碼轉換至 C++ 程式碼,分析 Block 中各種型別變數的訪問方式:

static char globalVar[] = {"globalVar"};
static char globalStaticVar[] = {"globalStaticVar"};

void catchVar() {
    int var1 = 1;
    int var2 = 2;
    static char staticVar[] = {"staticVar"};
    
    void (^blk)(void) = ^{
        printf("%d\n", var1);
        printf("%s\n", staticVar);
        printf("%s\n", globalVar);
        printf("%s\n", globalStaticVar);
    };
    blk();
}
複製程式碼

上面的程式碼分別使用了區域性變數、靜態變數、全域性變數和靜態全域性變數,其轉換後的程式碼:

struct __catchVar_block_impl_0 {
  struct __block_impl impl;
  struct __catchVar_block_desc_0* Desc;
  int var1; // 區域性變數
  char (*staticVar)[10]; // 靜態變數
  __catchVar_block_impl_0(void *fp, struct __catchVar_block_desc_0 *desc, int _var1, char (*_staticVar)[10], int flags=0) : var1(_var1), staticVar(_staticVar) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchVar_block_func_0(struct __catchVar_block_impl_0 *__cself) {
  int var1 = __cself->var1; // bound by copy
  char (*staticVar)[10] = __cself->staticVar; // bound by copy

        printf("%d\n", var1);
        printf("%s\n", (*staticVar));
        printf("%s\n", globalVar);
        printf("%s\n", globalStaticVar);
    }

static struct __catchVar_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __catchVar_block_desc_0_DATA = { 0, sizeof(struct __catchVar_block_impl_0)};
void catchVar() {
    int var1 = 1;
    int var2 = 2;
    static char staticVar[] = {"staticVar"};

    void (*blk)(void) = ((void (*)())&__catchVar_block_impl_0((void *)__catchVar_block_func_0, &__catchVar_block_desc_0_DATA, var1, &staticVar));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼

通過__catchVar_block_func_0函式和 Block 建構函式可以知道各種變數的訪問方式:

  • 全域性變數靜態全域性變數 因其作用域內都可以直接訪問。
  • 靜態變數 成為成員變數,但是從建構函式傳入的是一個記憶體地址,然後通過地址訪問。
  • 區域性變數 成為成員變數,從建構函式直接傳入變數的值並賦值給成員變數,然後通過成員變數訪問。

2.2 物件

下面的程式碼中,Block 內使用了外部的一個物件,這種情況下 Block 內部是如何捕獲該物件的呢?

void catchObject() {
    id obj = [NSObject new];
    
    void (^blk)(void) = ^{
        printf("%d\n", [obj hash]);
    };
    blk();
}
複製程式碼

我們將上面的程式碼轉換成 C++ 程式碼後分析其中實現原理:

struct __catchObject_block_impl_0 {
  struct __block_impl impl;
  struct __catchObject_block_desc_0* Desc;
  __strong id obj;
  __catchObject_block_impl_0(void *fp, struct __catchObject_block_desc_0 *desc, __strong id _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchObject_block_func_0(struct __catchObject_block_impl_0 *__cself) {
  __strong id obj = __cself->obj; // bound by copy

        printf("%d\n", ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("hash")));
    }
static void __catchObject_block_copy_0(struct __catchObject_block_impl_0*dst, struct __catchObject_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __catchObject_block_dispose_0(struct __catchObject_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __catchObject_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __catchObject_block_impl_0*, struct __catchObject_block_impl_0*);
  void (*dispose)(struct __catchObject_block_impl_0*);
} __catchObject_block_desc_0_DATA = { 0, sizeof(struct __catchObject_block_impl_0), __catchObject_block_copy_0, __catchObject_block_dispose_0};


void catchObject() {
    id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));

    void (*blk)(void) = ((void (*)())&__catchObject_block_impl_0((void *)__catchObject_block_func_0, &__catchObject_block_desc_0_DATA, obj, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼

首先來看catchObject()函式,在構建 blk 時,傳入了物件obj和十進位制標誌位570425344

void (*blk)(void) = ((void (*)())&__catchObject_block_impl_0((void *)__catchObject_block_func_0, &__catchObject_block_desc_0_DATA, obj, 570425344));
複製程式碼

Block 結構體中成員變數obj__strong修飾符,傳入的物件obj直接賦值給成員變數,說明是直接使用原物件並且使引用計數 +1 。

其次是原始碼中新增了兩個方法,分別是__catchObject_block_copy_0__catchObject_block_dispose_0,而這兩個方法又分別呼叫了_Block_object_assign_Block_object_dispose方法,這兩個方法是用來管理 Block 中變數儲存的,後面會進行分析。

static void __catchObject_block_copy_0(struct __catchObject_block_impl_0*dst, struct __catchObject_block_impl_0*src) {
  _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __catchObject_block_dispose_0(struct __catchObject_block_impl_0*src) {
  _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼

2.3 Block

將一個 Block 作為另外一個 Block 內的引數來使用,接下來分析這種情況下 Block 的實現。

void catchBlock() {
    void (^block)(void) = ^{};
    void (^blk)(void) = ^{
        block;
    };
    blk();
}
複製程式碼

轉換後程式碼如下:

struct __catchBlock_block_impl_0 {
  struct __block_impl impl;
  struct __catchBlock_block_desc_0* Desc;
  __catchBlock_block_impl_0(void *fp, struct __catchBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchBlock_block_func_0(struct __catchBlock_block_impl_0 *__cself) {
}

static struct __catchBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __catchBlock_block_desc_0_DATA = { 0, sizeof(struct __catchBlock_block_impl_0)};

struct __catchBlock_block_impl_1 {
  struct __block_impl impl;
  struct __catchBlock_block_desc_1* Desc;
  struct __block_impl *block;
  __catchBlock_block_impl_1(void *fp, struct __catchBlock_block_desc_1 *desc, void *_block, int flags=0) : block((struct __block_impl *)_block) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchBlock_block_func_1(struct __catchBlock_block_impl_1 *__cself) {
  void (*block)() = (void (*)())__cself->block; // bound by copy

        block;
    }
static void __catchBlock_block_copy_1(struct __catchBlock_block_impl_1*dst, struct __catchBlock_block_impl_1*src) {_Block_object_assign((void*)&dst->block, (void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static void __catchBlock_block_dispose_1(struct __catchBlock_block_impl_1*src) {_Block_object_dispose((void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static struct __catchBlock_block_desc_1 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __catchBlock_block_impl_1*, struct __catchBlock_block_impl_1*);
  void (*dispose)(struct __catchBlock_block_impl_1*);
} __catchBlock_block_desc_1_DATA = { 0, sizeof(struct __catchBlock_block_impl_1), __catchBlock_block_copy_1, __catchBlock_block_dispose_1};

void catchBlock() {
    void (*block)(void) = ((void (*)())&__catchBlock_block_impl_0((void *)__catchBlock_block_func_0, &__catchBlock_block_desc_0_DATA));
    void (*blk)(void) = ((void (*)())&__catchBlock_block_impl_1((void *)__catchBlock_block_func_1, &__catchBlock_block_desc_1_DATA, (void *)block, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼

同樣的先看catchBlock()函式,其中 Block 建構函式中傳入了block和標誌位570425344,賦值給在__catchBlock_block_impl_1結構體中的一個__block_impl型別的結構體成員變數block

void (*blk)(void) = ((void (*)())&__catchBlock_block_impl_1((void *)__catchBlock_block_func_1, &__catchBlock_block_desc_1_DATA, (void *)block, 570425344));
複製程式碼

在這段程式碼中同樣有__catchObject_block_copy_0__catchObject_block_dispose_0兩個方法,不同的是呼叫_Block_object_assign_Block_object_dispose方法時最後一個入參為7 /*BLOCK_FIELD_IS_BLOCK*/,之前的捕獲物件時傳入的引數是3 /*BLOCK_FIELD_IS_OBJECT*/

static void __catchBlock_block_copy_1(struct __catchBlock_block_impl_1*dst, struct __catchBlock_block_impl_1*src) {
  _Block_object_assign((void*)&dst->block, (void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

static void __catchBlock_block_dispose_1(struct __catchBlock_block_impl_1*src) {
  _Block_object_dispose((void*)src->block, 7/*BLOCK_FIELD_IS_BLOCK*/);
}
複製程式碼

2.4 __block 修飾的變數

Block 將外部的變數捕獲後,可以在內部訪問外部的變數,但是還不能修改外部變數的值(靜態變數、全域性變數和靜態全域性變數可以直接修改)。這個時候需要使用 __block 修飾符,使得在 Block 內部也可以修改 __block 修飾符修飾的變數。

下面通過轉換原始碼來分析實現原理:

void catchBlockVar() {
    __block int blockVar = 1;
    
    void (^blk)(void) = ^{
        blockVar = 2;
        printf("%d\n", blockVar);
    };
    blk();
}
複製程式碼

轉換後:

struct __Block_byref_blockVar_0 {
  void *__isa;
__Block_byref_blockVar_0 *__forwarding;
 int __flags;
 int __size;
 int blockVar;
};

struct __catchBlockVar_block_impl_0 {
  struct __block_impl impl;
  struct __catchBlockVar_block_desc_0* Desc;
  __Block_byref_blockVar_0 *blockVar; // by ref
  __catchBlockVar_block_impl_0(void *fp, struct __catchBlockVar_block_desc_0 *desc, __Block_byref_blockVar_0 *_blockVar, int flags=0) : blockVar(_blockVar->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchBlockVar_block_func_0(struct __catchBlockVar_block_impl_0 *__cself) {
  __Block_byref_blockVar_0 *blockVar = __cself->blockVar; // bound by ref

        (blockVar->__forwarding->blockVar) = 2;
        printf("%d\n", (blockVar->__forwarding->blockVar));
}
    
static void __catchBlockVar_block_copy_0(struct __catchBlockVar_block_impl_0*dst, struct __catchBlockVar_block_impl_0*src) {_Block_object_assign((void*)&dst->blockVar, (void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __catchBlockVar_block_dispose_0(struct __catchBlockVar_block_impl_0*src) {_Block_object_dispose((void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __catchBlockVar_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __catchBlockVar_block_impl_0*, struct __catchBlockVar_block_impl_0*);
  void (*dispose)(struct __catchBlockVar_block_impl_0*);
} __catchBlockVar_block_desc_0_DATA = { 0, sizeof(struct __catchBlockVar_block_impl_0), __catchBlockVar_block_copy_0, __catchBlockVar_block_dispose_0};


void catchBlockVar() {
    __attribute__((__blocks__(byref))) __Block_byref_blockVar_0 blockVar = {(void*)0,(__Block_byref_blockVar_0 *)&blockVar, 0, sizeof(__Block_byref_blockVar_0), 1};

    void (*blk)(void) = ((void (*)())&__catchBlockVar_block_impl_0((void *)__catchBlockVar_block_func_0, &__catchBlockVar_block_desc_0_DATA, (__Block_byref_blockVar_0 *)&blockVar, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼

和其他型別 Block 最大的不同就是多了一個結構體__Block_byref_blockVar_0,經過__block修飾符修飾的變數都會自動生成一個這樣的結構體。

struct __Block_byref_blockVar_0 {
  void *__isa;
__Block_byref_blockVar_0 *__forwarding;
 int __flags;
 int __size;
 int blockVar;
};
複製程式碼
  • __isa:具體和物件一樣的特性。
  • __forwarding:在棧區且未被複制時指向自己,被複制到堆區後指向堆區的結構體。
  • __flags:標誌位。
  • __size:結構體佔用記憶體大小。
  • blockVar:原變數值。

catchBlockVar方法轉換後,之前的int型別變數blockVar變成__Block_byref_blockVar_0型別結構體,然後將此結構體地址傳入 Block 的建構函式中,因此 Block 自動生成的成員變數也為__Block_byref_blockVar_0型別。

void catchBlockVar() {
    __Block_byref_blockVar_0 blockVar = {(void*)0,(__Block_byref_blockVar_0 *)&blockVar, 0, sizeof(__Block_byref_blockVar_0), 1};

    void (*blk)(void) = (&__catchBlockVar_block_impl_0((void *)__catchBlockVar_block_func_0, &__catchBlockVar_block_desc_0_DATA, (__Block_byref_blockVar_0 *)&blockVar, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼

在被 Block 被呼叫時,通過下面的程式碼可以發現,訪問blockVar並不直接訪問,而是通過其__forwarding來訪問其原變數的值。這樣可以在它被複制到堆區時,訪問堆區中的結構體。為什麼要優先訪問堆區的結構體?棧區的物件在超出其作用域後會被釋放,如果希望在作用域外使用就需要複製到堆區中。

static void __catchBlockVar_block_func_0(struct __catchBlockVar_block_impl_0 *__cself) {
  __Block_byref_blockVar_0 *blockVar = __cself->blockVar; // bound by ref

        (blockVar->__forwarding->blockVar) = 2;
        printf("%d\n", (blockVar->__forwarding->blockVar));
}
複製程式碼

在這段程式碼中的__catchObject_block_copy_0__catchObject_block_dispose_0兩個方法中傳入的引數是8 /*BLOCK_FIELD_IS_BYREF*/

static void __catchBlockVar_block_copy_0(struct __catchBlockVar_block_impl_0*dst, struct __catchBlockVar_block_impl_0*src) {
  _Block_object_assign((void*)&dst->blockVar, (void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __catchBlockVar_block_dispose_0(struct __catchBlockVar_block_impl_0*src) {
  _Block_object_dispose((void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

2.5 __block 修飾的物件

在 Block 使用__block修飾的變數和__block修飾的物件,其中內部實現是有一些細微區別的,通過下面的程式碼來進行分析。

void catchBlockObject() {
    
    __block NSObject *obj = [[NSObject alloc] init];
    blk_t block = ^ {
        obj;
    };
}
複製程式碼

轉換後:

struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

struct __catchBlockObject_block_impl_0 {
  struct __block_impl impl;
  struct __catchBlockObject_block_desc_0* Desc;
  __Block_byref_obj_1 *obj; // by ref
  __catchBlockObject_block_impl_0(void *fp, struct __catchBlockObject_block_desc_0 *desc, __Block_byref_obj_1 *_obj, int flags=0) : obj(_obj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __catchBlockObject_block_func_0(struct __catchBlockObject_block_impl_0 *__cself) {
  __Block_byref_obj_1 *obj = __cself->obj; // bound by ref

        (obj->__forwarding->obj);
    }
static void __catchBlockObject_block_copy_0(struct __catchBlockObject_block_impl_0*dst, struct __catchBlockObject_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __catchBlockObject_block_dispose_0(struct __catchBlockObject_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __catchBlockObject_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __catchBlockObject_block_impl_0*, struct __catchBlockObject_block_impl_0*);
  void (*dispose)(struct __catchBlockObject_block_impl_0*);
} __catchBlockObject_block_desc_0_DATA = { 0, sizeof(struct __catchBlockObject_block_impl_0), __catchBlockObject_block_copy_0, __catchBlockObject_block_dispose_0};


void catchBlockObject() {

    __attribute__((__blocks__(byref))) __Block_byref_obj_1 obj = {(void*)0,(__Block_byref_obj_1 *)&obj, 33554432, sizeof(__Block_byref_obj_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};

    blk_t block = ((void (*)())&__catchBlockObject_block_impl_0((void *)__catchBlockObject_block_func_0, &__catchBlockObject_block_desc_0_DATA, (__Block_byref_obj_1 *)&obj, 570425344));
}
複製程式碼

在宣告的結構體__Block_byref_obj_1中,和之前不一樣的是多了__Block_byref_id_object_copy__Block_byref_id_object_dispose兩個管理記憶體的方法。

struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};
複製程式碼

再看到catchBlockObject()函式中,被__block修飾符修飾的obj物件轉換成__Block_byref_obj_1型別結構體。其中copydispose兩個方法傳入的__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131兩個靜態方法。

void catchBlockObject() {

   __Block_byref_obj_1 obj = {(void*)0,(__Block_byref_obj_1 *)&obj, 33554432, sizeof(__Block_byref_obj_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};

    blk_t block = (&__catchBlockObject_block_impl_0((void *)__catchBlockObject_block_func_0, &__catchBlockObject_block_desc_0_DATA, (__Block_byref_obj_1 *)&obj, 570425344));
}
複製程式碼

靜態方法如下,在最後一個引數傳入的是131,其實就是3 + 128

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
複製程式碼

通過下面這個列舉可以明白,為什麼捕獲不同型別的變數,需要不同的入參。根據入參不同,對捕獲的變數複製和釋放的操作都是不同的。131則表示BLOCK_FIELD_IS_BYREF|BLOCK_BYREF_CALLER

// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
複製程式碼

Block 本身的 Copy / Dispose 方法入參還是8 /*BLOCK_FIELD_IS_BYREF*/

三、Block 的儲存域

Block 的儲存域分為 3 種,分別為_NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock

  • _NSConcreteStackBlock:棧區
  • _NSConcreteGlobalBlock:資料區域(.data 區)
  • _NSConcreteMallocBlock:堆區

理解 Block 實現原理

3.1 _NSConcreteStackBlock

正常情況下,定義在類內部的 Block 在捕獲了自動變數的情況下都是在棧區,可以通過下面的程式碼列印出其型別。但是在實際使用中都會定義後都會賦值給一個變數,這會導致實際使用用這個 Block 的時候已經變成_NSConcreteMallocBlock型別。

// block 使用了捕獲的變數 tempVar
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int tempVar = 1;
        NSLog(@"Stack Block:%@\n", ^() {
            printf("Stack Block! %d\n", tempVar);
        });
    }
    return 0;
}

// printf:Stack Block:<__NSStackBlock__: 0x7ffeefbff4a0>
複製程式碼

3.2 _NSConcreteGlobalBlock

在定義全域性變數的區域定義的 Block 型別為_NSConcreteGlobalBlock,另外還有一種情況就是定義在類內部的 Block 在沒有捕獲任何自動變數時,也是_NSConcreteGlobalBlock型別。

// block 內未使用外部變數
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Global Block:%@\n", ^() {
            printf("Global Block!\n");
        });
    }
    return 0;
}

// printf:Global Block:<__NSGlobalBlock__: 0x1000021c8>
複製程式碼

3.3 _NSConcreteMallocBlock

因為 Block 捕獲的 __block 變數存在棧區時,超出其作用域後則被釋放。為了避免這種情況,Block 實現機制將 Block 從棧區複製到堆區,這樣即使超出其作用域,堆區的 Block 和 __block 變數依然存在。

理解 Block 實現原理

理解 Block 實現原理

在未被複制時,__block 變數__forwarding指向自身,被複制後指向堆區中的 __block 變數,這種機制使其無論是在堆區還是棧區都可以正確訪問。

下面這些場景下編譯器會自動處理將 Block 拷貝到堆上:

  • ARC 有效時 block 作為函式或方法的返回值會自動被拷貝到堆上
  • Cocoa 框架中的方法名包含 usingBlock 等時
  • GCD 的 API
  • 將 block 賦值給 __Strong 修飾符 id 型別物件或 Block 型別的成員變數時

以下場景需要手動拷貝至堆上

  • 手動呼叫 block 例項方法
    • 將 block 作為方法中的引數時需要開發者手動拷貝
    • 當將 block 放入陣列並作為返回值時需要手動拷貝

四、 __block 變數的儲存域

當 Block 從棧區被複制到堆區時,對應的__block修飾符修飾的變數也相應地被複制到堆區。

理解 Block 實現原理

在前面的內容我們分析到__block修飾的變數會轉換成一個結構體,結構體中含有成員變數__forwarding,複製到堆區後可以在 Block 變數超出其作用域使用,這個時候棧區結構體成員變數__forwarding指向堆區的結構體(在未被複制時指向自身結構體)。

理解 Block 實現原理

當多個 Block 使用同一個 __block 變數時,複製已經在堆上的 __block 變數引用計數會增加,當釋放時也是將減引用計數減至 0 後才廢棄該 __block 變數。

理解 Block 實現原理

五、_Block_object_assign 和 _Block_object_dispose

在第二章節中介紹了捕獲不同型別的變數時,Block 呼叫_Block_object_assign函式的入參flags都不一樣,下面來看看具體實現。

5.1 _Block_object_assign

直接看實現原始碼:

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:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}
複製程式碼

由上面程式碼可知_Block_object_assign方法根據入參flags做了不同的處理,下面一一分析不同入參的處理方式:

5.1.1 BLOCK_FIELD_IS_OBJECT:複製物件

// 預設_Block_retain_object 被賦值為 _Block_retain_object_default 即什麼都不做
_Block_retain_object(object); 
// 指標指向原物件記憶體地址。
*dest = object; 
複製程式碼

_Block_retain_object方法在 _Block_use_RR2被執行時才有實際意義。

void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}
複製程式碼

5.1.2 BLOCK_FIELD_IS_BLOCK:複製 Block

分析_Block_copy的實現:

 //複製或碰撞引用計數。如果確實要複製,請呼叫複製助手(如果存在)。
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    aBlock = (struct Block_layout *)arg;
    // 已複製 則增加引用計數
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果是全域性 Block 則直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    // 進行復制
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // 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);
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
複製程式碼

5.1.3 BLOCK_FIELD_IS_BYREF:複製 _block 變數

分析_Block_byref_copy的實現:

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) {
        // 複製 Block_byref
        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) {
        
            // 複製 Block_byref2,含有 copy / dispose 方法的變數需要執行這部分程式碼
            
            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;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                // 複製 Block_byref3
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*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));
        }
    }
    // 已經複製到堆上的 引用計數 +1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}
複製程式碼

5.1.4 其他

在其他情況下都是直接指標指向原物件地址:

*dest = object;
複製程式碼

5.2 _Block_object_dispose

下面是 Block 捕獲變數的釋放邏輯:

void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}
複製程式碼

5.2.1 BLOCK_FIELD_IS_BYREF:釋放 _block 變數

分析_Block_byref_release的實現:

static void _Block_byref_release(const void *arg) {
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    byref = byref->forwarding;
    // 判斷是否被複制到堆上
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        // 引用計數 -1 後判斷是否要進行釋放操作
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            // 判斷這個變數是否有 copy / dispose 方法
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}
複製程式碼

需要根據 _block 變數成員變數和標誌位來判斷釋放步驟。

5.2.2 BLOCK_FIELD_IS_BLOCK:釋放 Block

分析_Block_release的實現:

void _Block_release(const void *arg) {
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    if (aBlock->flags & BLOCK_IS_GLOBAL) return;
    if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
    // 全域性 Block 和 棧上的 Block 直接返回
    
    // Block 引用計數 -1 後判斷是否需要進行釋放
    if (latching_decr_int_should_deallocate(&aBlock->flags)) {
        _Block_call_dispose_helper(aBlock);
        _Block_destructInstance(aBlock);
        free(aBlock);
    }
}
複製程式碼

5.2.3 BLOCK_FIELD_IS_OBJECT:釋放物件

_Block_release_object(object);
複製程式碼

_Block_release_object方法在 ARC 環境下無操作,和_Block_retain_object一樣。

六、迴圈引用

一個 Block 作為類的成員變數的同時,Block 內部還使用了類例項的情況下會引發迴圈引用。在這種情況下,類例項持有成員變數 block ,block 持有成員變數 __block 變數,__block 變數結構體持有類例項,形成一個三角迴圈引用關係。

__block id tmp = self;
blk = ^{
  NSLog(@"self = %@", tmp);
}
複製程式碼

解決迴圈引用的方法有 2 種,一種是使用 __weak 修飾符,這種方法打破了__block 變數結構體持有類例項的關係,從而避免迴圈引用。

__weak id tmp = self;
blk = ^{
  NSLog(@"self = %@", tmp);
}
複製程式碼

還有一種情況是使用 __block 修飾符,然後 blk 呼叫函式最後一行將tmp手動置空,這種方法雖然也可以避免迴圈引用,但是一旦 blk 沒有被呼叫的話,同樣會造成迴圈引用。所以還是使用 __weak 修飾符的方式更為安全。

__block id tmp = self;
blk = ^{
  NSLog(@"self = %@", tmp);
  tmp = nil;
}
複製程式碼

總結

整片文章寫下來,最重要的幾個概念:

  • 捕獲不同型別變數的 Block 會生成不同的邏輯。
  • Block 的作用域的機制是為了 Block 和內部使用的變數在超出其作用域仍能使用。
  • 針對不同型別的變數或 __block 變數結構體, 複製和釋放邏輯也不一樣。

參考


點此檢視更多我的文章

相關文章