iOS - 對 block 實現的探究

sas???發表於2019-01-01

Aspects 原始碼的時候,發現了一段程式碼

typedef struct _AspectBlock {
    __unused Class isa;
    AspectBlockFlags flags;
    __unused int reserved;
    void (__unused *invoke)(struct _AspectBlock *block, ...);
    struct {
        unsigned long int reserved;
        unsigned long int size;
        // requires AspectBlockFlagsHasCopyDisposeHelpers
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
        // requires AspectBlockFlagsHasSignature
        const char *signature;
        const char *layout;
    } *descriptor;
    // imported variables
} *AspectBlockRef;

// ......

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
    // .......
}

當時不理解這個程式碼是什麼意思,因此想要探究 block 的本質。以下是一點查閱資料並且自己測試後的一點見解。

1. 要想看 block 的底層實現,首先將 OC 程式碼轉換成 C++ 程式碼

新建 block.m 檔案新增以下 OC 程式碼:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        __block int b = 20;
        NSString *str = @"str";
        __weak NSString *weakStr = @"weakStr";
        void(^block)(int) = ^(int i){
            i;
            a;
            b;
            str;
            weakStr;
        };

        block(1);
    }
    return 0;
}

上述程式碼定義了各種型別的變數用於測試

終端 cd 到 block.m 根目錄執行一下命令進行轉換

clang  -rewrite-objc  block.m

這個時候會報 error

cannot create __weak reference because the current deployment target does not support weak references

stack overflow 上找到這個問題解決方案,通過以下命令進行轉換

clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations block.m

這時候可以發現在當前目錄會生成對應的 C++ 檔案 block.cpp ,通過這個檔案可以看到 block 在 C++ 中的實現,更好的理解 block 機制

找到其中對應 main 函式的程式碼

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 20};
        NSString *str = (NSString *)&__NSConstantStringImpl__var_folders_0__w7c3mlv94_10qs2mxt1kk0980000gn_T_block_3ee913_mi_0;
        __attribute__((objc_ownership(none))) NSString *weakStr = (NSString *)&__NSConstantStringImpl__var_folders_0__w7c3mlv94_10qs2mxt1kk0980000gn_T_block_3ee913_mi_1;
        void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, str, weakStr, (__Block_byref_b_0 *)&b, 570425344));

        ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1);
    }
    return 0;
}

2. 解析 C++ 檔案

2.1 __Block_byref_b_0

OC 程式碼

__block int b = 20;

對應 C++ 程式碼

__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 20};

實際上 b 就是以下型別

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

可以看出被 __block 修飾的 b 是一個與 OC 記憶體結構相容的 struct

  • __isa: 預設為 (void*)0

  • __forwarding: 在建構函式中傳入的是 (__Block_byref_b_0 *)&b ,是一個指向 b 的指標,在下面 block 的 C++ 實現(2.2 __main_block_impl_0)中可以看到建構函式傳入的就是該指標,因此用 __block 修飾的變數實際傳入 block 的是一個指向該變數的指標,因此可以對其進行修改

  • __size: 大小

  • b :b 的值 20

2.2 __main_block_impl_0

OC 程式碼

void(^block)(int) = ^(int i){
    i;
    a;
    b;
    str;
    weakStr;
};

對應 C++ 程式碼

void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, str, weakStr, (__Block_byref_b_0 *)&b, 570425344));

實際上 block 就是以下型別

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  NSString *__strong str;
  NSString *__weak weakStr;
  __Block_byref_b_0 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSString *__strong _str, NSString *__weak _weakStr, __Block_byref_b_0 *_b, int flags=0) : a(_a), str(_str), weakStr(_weakStr), b(_b->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到 block 實際也是一個結構體,內部包含一個 __block_impl 的結構體、__main_block_desc_0 結構體指標,以及用到的外部變數 abstrweakStr 和一個建構函式

通過觀察建構函式,可以看出外部變數傳遞到 block 中時,只有用 __block 修飾過的 b 傳入的是 __Block_byref_b_0 * 指標,對 b 的修改操作都是通過 _b->__forwarding 來直接對原始b 資料進行修改。而其他未用 __block 只是進行賦值操作,對 astrweakStr 的修改無法影響到 block 結構體內部的對應的屬性的值。

因此會產生以下程式碼的差異

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        __block int b = 10;
        void(^block)(void) = ^ {
            NSLog(@"%d,%d", a, b);
        };
        a = 20;
        b = 20;
        block(); // 輸出 10,20
    }
    return 0;
}
2.3 __block_impl

__block_impl 結構體的定義如下,儲存 block 的實現

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
  • isa: 表示一個類物件指標,指向 block 的類。我們可以看到在 __main_block_impl_0 建構函式中有以下程式碼

    impl.isa = &_NSConcreteStackBlock;
    

    isa 被賦值為 &_NSConcreteStackBlock ,指向類 __NSStackBlock__ 的指標,說明了該 block 的是一個存放在棧區的 block,下面 3. Block 型別 有對 block 的型別進行介紹

  • Flags:標記位,標記 desc(2.4 __main_block_desc_0) 中有無 copy , dispose方法,有無方法簽名字元 signature,layout 等

  • FuncPtr :指標指向 block 的具體實現,看 main 方法中傳入的引數為方法 __main_block_func_0

2.4 __main_block_desc_0

__main_block_desc_0 結構體的定義如下,儲存 block 描述資訊

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
  // 以下內容在本例中沒有,但是若 __block_impl 中 Flags 有標記位 1 << 30 則會有
  // const char *signature;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
  • reserved: 0
  • Block_size:block 大小
  • copy:copy 傳入的是方法 __main_block_copy_0,實現的功能就是當 block 進行 copy 時,會呼叫該方法。具體實現如下
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->weakStr, (void*)src->weakStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
  • dispose:dispose 傳入的是方法 __main_block_dispose_0,實現的功能就是當 block 進行從堆中移除時呼叫。具體實現如下
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->weakStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
  • signature:在本例中沒有,但是若 block_impl(2.3 __block_impl) 中 Flags 有標記位 1 << 30 則會有,對方法引數的簽名,對應 NSMethodSignature
2.5 __main_block_func_0

OC 程式碼

{
    i;
    a;
    b;
    str;
    weakStr;
};

對應 C++ 程式碼,方法 __main_block_func_0 定義如下

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int i) {
  __Block_byref_b_0 *b = __cself->b; // bound by ref
  int a = __cself->a; // bound by copy
  NSString *__strong str = __cself->str; // bound by copy
  NSString *__weak weakStr = __cself->weakStr; // bound by copy

            i;
            a;
            (b->__forwarding->b);
            str;
            weakStr;
        }

__cself 指向 block 物件,只有用 __block 修飾過的變數會用特殊的方式呼叫 b->__forwarding->b

3. Block 型別

block 型別有 _NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock ,這 3 類的區別如下

_NSConcreteStackBlock _NSConcreteGlobalBlock _NSConcreteMallocBlock
存放區域 棧區 全域性區(靜態區) 堆區
型別說明 block 宣告為一個區域性變數 Block 宣告為一個全域性變數 stackBlock copy 後生成的 block 為 NSMallocBlock型別

如下程式碼可以分別生成 3 種型別的 block

// _NSConcreteGlobalBlock 
void(^block_global)(int) = ^(int i){};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // _NSConcreteStackBlock
        ^(int i){};
        // _NSConcreteMallocBlock
        void(^block_malloc)(int) = [^(int i){} copy];
    }
    return 0;
}

對應 C++ 程式碼

// _NSConcreteGlobalBlock 
// void(^block_global)(int) = ^(int i){};
struct __block_global_block_impl_0 {
  struct __block_impl impl;
  struct __block_global_block_desc_0* Desc;
  __block_global_block_impl_0(void *fp, struct __block_global_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// _NSConcreteStackBlock
// ^(int i){};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// _NSConcreteMallocBlock
// void(^block_malloc)(int) = [^(int i){} copy];
void(*block_malloc)(int) = (void (*)(int))((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA)), sel_registerName("copy"));

因為 block 建立後預設會在棧區,因此對 block 進行賦值的時候要使用 copy 生成一個存放在堆區的 block,避免 block 釋放後呼叫 block。用 @property 宣告一個 block 經常用 copy 關鍵字

但是,在 ARC 環境下,卻有所改變。在 ARC 環境下,將 block 賦值給一個 __strong 物件,會自動對其進行 copy ,因此在 ARC 環境下 @property 宣告一個 block 經常用 strong 關鍵字也是沒什麼問題的

在 ARC 情況下還有以下情況會自動將 block copy 到堆區

  • block 作為函式返回值
  • 將 block 賦值給__strong 物件
  • Cocoa API 中方法名含有 usingBlock 的方法使用 block
  • GCD 中使用的 block

相關文章