iOS开发 — Block原理分析

682 阅读12分钟

在日常的开发中Block也算是我们经常使用的,那么它的底层原理是什么?它的使用又会引起什么问题?解决的方法是什么?笔者将会逐步探索并把结果记录到本篇文章中。

Block的种类

我们平常经常用的Block一般是三种,但其实Block一共有六种,这个可以在闭包源码libclosure中查看到:

/********************
NSBlock support

We allocate space and export a symbol to be used as the Class for the on-stack and malloc’ed copies until ObjC arrives on the scene.  These data areas are set up by Foundation to link in as real classes post facto.

We keep these in a separate file so that we can include the runtime code in test subprojects but not include the data so that compiled code that sees the data in libSystem doesn't get confused by a second copy.  Somehow these don't get unified in a common block.
**********************/

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

我们经常用到是_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这三种,而剩下的三种是系统级别使用的,iOS开发中不太能用的到。那我们经常使用的三种表现形式例如以下代码:

void (^block1)(void) = ^{};
NSLog(@"%@",block1);
    
int a = 10;
void (^block2)(void) = ^{
    NSLog(@"123 - %d",a);
};
NSLog(@"%@",block2);
    
NSLog(@"%@",^{
    NSLog(@"123 - %d",a);
});

在控制台会看到输出:

<__NSGlobalBlock__: 0x1080c2090>
<__NSMallocBlock__: 0x600002e9f990>
<__NSStackBlock__: 0x7ffee7b3c458>

Block的本质

Block的本质以及调用

先创建一个block1.c文件,文件内代码如下:

#include "stdio.h"

int main(){
    void(^block)(void) = ^{
        printf("block1");
    };
    block();
    return 0;
}

然后用clang指令clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk block1.c去编译会等到一个block1.cpp文件,在cpp文件中会看到代码:

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   printf("block1");
}

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(){
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

我们先看void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));这段代码,=前是block的声明,=后简化一下__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);,这是一个构造函数,而我们可以看到__main_block_impl_0其实是一个结构体,由此可知block的声明其实是一个结构体类型,而我们知道block是可以用%@打印的,所以它的本质是一个对象,而对象的本质其实就是结构体。

我们也可以看到__main_block_impl_0()的第一个参数__main_block_func_0是一个函数,内容就是在block代码块内写的东西,而__main_block_func_0保存在了结构体__main_block_impl_0的函数__main_block_impl_0impl.FuncPtr中,而在调用时我们可以看到代码((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);其实就等同于block->FuncPtr,所以这也就解释了block为什么需要调用,因为block声明的时候只是函数声明,具体的函数实现是需要调用的。

Block捕获外界变量

为了研究这个问题,我们先把代码修改一下:

int main(){
    int a = 10;
    void(^block)(void) = ^{
        printf("block1 - %d",a);
    };
    block();
    return 0;
}

同样用clang指令编译得到cpp文件查看:

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

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 a = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

可以看到,我在源码中加了一行int a = 10;,而编译后可以看到__main_block_impl_0函数多了一个参数a,而在结构体__main_block_impl_0中也多了一个属性int a;,通过传值把10这个值赋值给了a由此我们可知,block在捕获外界变量的时候会自动生成一个属性来保存外界传进来的值。

__block

我们都知道block要想在内部改变外界的变量,需要在变量声明前加一个__block,那么它的原理是什么呢?这里我们再次修改代码:

int main(){
    __block int a = 10;
    void(^block)(void) = ^{
        printf("block1 - %d",a);
    };
    block();
    return 0;
}

可以看到只是在int a前面加了一个__block,然后同样用clang编译,得到结果:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
    printf("block1 - %d",(a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(){
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

我们可以看到代码中生成了一个新的结构体__Block_byref_a_0并把a的地址指针和值分别传到了结构体的__forwardinga中,而__main_block_impl_0函数的第三个参数也变为(__Block_byref_a_0 *)&a,当然对应的结构体__main_block_impl_0中也生成了一个__Block_byref_a_0 *a属性来接收,__main_block_func_0函数内的代码也变成了__Block_byref_a_0 *a = __cself->a;,这相当于指针拷贝,所以在调用的时候在block内部也能修改外界的变量。

我们还注意到cpp文件中还生成了函数__main_block_copy_0__main_block_dispose_0和结构体__main_block_desc_0,这些也都需要我们继续进行探索。

Block源码分析

Block的结构

接下来我们就进入源码分析阶段,同样在libclosure中,打开Block_private.h文件,我们可以看到这样一个结构:

struct Block_layout {
    void *isa; // isa指针
    volatile int32_t flags; // 用于记录block的状态
    int32_t reserved; // 保留变量
    BlockInvokeFunction invoke; // 函数指针,指向具体的函数实现
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

// 可选
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

这个其实就是block的数据结构。这里我们关注一下flagsdescriptor,先看flags

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime 释放标记,一般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime 存储引用计数的值,是一个可选用参数
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime 第16位是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler 是否拥有拷贝辅助函数
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code 是否拥有block析构函数
    BLOCK_IS_GC =             (1 << 27), // runtime 标志是否有垃圾回收
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler 标志是否是全局block
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler 与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于runtime时动态调用
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

可以看出flags决定了很多block的信息状态,例如BLOCK_HAS_COPY_DISPOSE就决定了block中是否含有Block_descriptor_2BLOCK_HAS_SIGNATURE决定了block中是否含有Block_descriptor_3,这也说明Block_descriptor_2Block_descriptor_3是可选的,那么它们是如何获取到的呢,我们在源码runtime.cpp文件中搜索可以看到:

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

可以看到,不管是要获取Block_descriptor_2还是Block_descriptor_3都需要先进行判断,其实也就是判断flags,如果符合条件的话,就通过指针地址偏移的方式得到对应的结构,这其实也说明了Block_descriptor_1、2、3的内存地址是连续的。

Block的拷贝

接下来我们来研究以下block是如何进行拷贝的,我们在代码中可以找到_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) { // 全局block
        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);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

这里的代码其实逻辑很简单:先同block->flags来判断block包含的信。

  • 如果需要对引用计数处理,那就调用函数去处理。
  • 如果是一个全局block的话就不做处理直接返回。
  • 不是全局block的话那就是一个栈block,会通过malloc函数在堆区申请一份新的同样大小的内存空间,然后把栈block中的信息全部拷贝到新的堆区中,同时将isa置为_NSConcreteMallocBlock,这就是block从栈拷贝到堆的过程。

__block的拷贝

在上面我们看到了block对于自身的拷贝,那么对于__block修饰的变量究竟又做了什么呢?首先我们需要改一下代码:

__block NSString *name = [NSString stringWithFormat:@"luode"];
void(^block)(void) = ^{
    name = @"luode123";
    NSLog(@"block - %@",name);
};
block();

然后继续通过clang编译:

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

        __attribute__((__blocks__(byref))) __Block_byref_name_0 name = {
            (void*)0,
            (__Block_byref_name_0 *)&name,
            33554432,
            sizeof(__Block_byref_name_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders__l_qrwh0b994bv2231f8v82qgdr0000gn_T_main_8fee07_mi_0)};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

我们可以看到__block修饰的name转化成了一个__Block_byref_name_0的结构体,它在源码中对应的结构是:

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};

在编译后的文件中我们可以发现__main_block_copy_0中调用了_Block_object_assign函数,于是我们去源码中查找可以找到对应的函数:

// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
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;
    }
}

再把switch判断的标识展示出来:

// 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变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable  __block修饰的结构体
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers  __weak修饰的变量
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.  处理block_bref内部对象内存的时候会加一个额外的标记,配合上面的枚举一起使用
};

从源码中可以看到:

  • 如果传入的是一个对象的话,就会调用_Block_retain_object,而_Block_retain_object内部其实是一个空实现,推测这么做的原因在于block并不需要对这个对象进行持有,因为该对象的引用计数已经交给ARC进行管理了,只需要通过*dest = object;进行指针赋值即可。
  • 如果传入的是一个block的话,就会调用_Block_copy函数,也就是我们上面研究过的block的拷贝。
  • 如果传入的是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) {
        // 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;
        
        // __block 修饰变量 block具有修改能力
        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;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                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));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

首先通过malloc函数在堆区拷贝了一份,然后进行赋值,这里需要注意的是:

copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy

这两行代码让原来的__block修饰的结构体和新拷贝的结构体都指向了copy,这也就是为什么__block修饰的变量在block内部具有修改的能力,在前面我们通过clang看到是指针拷贝,那这里就是对应的源码实现。

接着往下看我们会看到(*src2->byref_keep)(copy, src);这样一句代码,src2其实就是src+1通过内存地址偏移得到的,这里src2调用了byref_keep,我们跟踪一下这个byref_keep会发现它是上面提到的结构体Block_byref中的第五个属性,我们在编译后的cpp文件中已经得知了被__block修饰的变量会转化成一个Block_byref的结构体,我们查看这个结构体name会发现它的第五个属性是__Block_byref_id_object_copy_131,我们在cpp文件中搜索可以找到:

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

可以看到该函数也调用了上面探索过的_Block_object_assign函数,但是传的参数多了个+40,这个是地址偏移应该不难理解,那么为什么是偏移40字节呢,这里我们就需要看一下它的参数也就是__Block_byref_name_0的结构:

struct __Block_byref_name_0 {
  void *__isa;
__Block_byref_name_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *name;
};

不难看出,偏移40个字节其实正好取到name的内存地址,这里其实是对name这个对象又做了一次拷贝。

所以__block修饰的外部变量,在block内部会出现三层拷贝:

  • 第一次是block自身的拷贝,从栈区拷贝到堆区。
  • 第二次是对新生成的结构体的拷贝,__block修饰的变量会生成一个__Block_byref_XXX_0的结构体,通过调用_Block_object_assign函数中的_Block_byref_copy函数拷贝到堆区中。
  • 第三次就是刚讲过的对__block修饰的变量本身的拷贝。

以上就是本次对Block的探索,笔者能力有限,如有错误还请指正。