iOS-Block底层探索

619 阅读25分钟

blockios开发中很常见的内容,正确的使用block可以使代码更简洁,可读性更强。但是block也有自己的一些弊端,如果对代码不够熟悉,调试的时候就不是很方便,而且使用不当也会造成一些其他的问题,比如循环引用或者其他方式的内存泄漏等等。知己知彼,百战不殆,今天主要来探究一下block的相关实现。

探索block的内部结构

正确的block如下所示:

int age = 10;
void(^myBlock)(int, int) = ^(int a, int b) {
    NSLog(@"age = %d", age);
    NSLog(@"a = %d, b = %d", a, b);
};
myBlock(20, 30);

上述代码声明了一个名为myBlockblock,包含了两个int类型的参数,返回值为void,同时myBlock还捕获了一个外部变量age,当我们执行myBlock的时候,输入如下:

age = 10
a = 20, b = 30

通过命令行可以将上述代码转换成底层实现代码:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

转换成功后main.m旁边会多出一个main.cpp的文件,打开文件翻到最底部,可以看到转换完毕的对应代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 10;
        void(*myBlock)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        ((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 20, 30);
    }
    return 0;
}

通过上述代码可以发现,myBlockc++代码中被转换成了一个__main_block_impl_0*类型的结构体指针。我们接下来看一下__main_block_impl_0结构体里面具体都有什么。

__main_block_impl_0

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

__main_block_impl_0内部成员变量有:

  • 结构体__block_impl
  • 结构体指针__main_block_desc_0*
  • 捕获的外部变量age
  • 一个构造方法

后面都会一一说到。

__block_impl

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

__block_impl4个成员变量,我们通过lldb观察一下myBlock的结构如下图:

可以发现它们是一一对应的,而且根据isa还可以确定的是myBlock确实是一个oc对象

__main_block_impl_0构造函数

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

该函数接受了四个参数:

  • void *fp

在本例中即是:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
    int age = __cself->age; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_fb5446_mi_0, age);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_fb5446_mi_1, a, b);
}

很显然,__main_block_func_0函数中存放的是我们定义的myBlock大括号里面的代码,该函数直接赋值给__main_block_impl_0构造函数的第一个参数fp指针,而fp又赋值给__block_impl结构体中的FuncPtr变量。

  • __main_block_desc_0_DATA 参数
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)};

__main_block_desc_0_DATA是一个结构体,包含了__main_block_impl_0的内存大小,最终赋值给了__main_block_impl_0结构体类型变量Desc

  • _age
    直接赋值给了age,需要注意的是此处捕获了自动变量为值拷贝
  • flags
    默认设置为0,在block的内部操作中会用到,记录了引用计数以及其他的一些特性

block的调用

((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 20, 30);

其中:

  • ((void (*)(__block_impl *, int, int))为函数的类型
  • ((__block_impl *)myBlock)->FuncPtr),在上面我们已经知道myBlock其实对应的是__main_block_impl_0的结构体,但是这里进行了类型转换,转换成了__block_impl的结构体,进而拿到了对应的FuncPtr。这样做的原因是因为__main_block_impl_0__block_impl位于结构体的第一位,因此他们的指针地址都指向了同一片内存空间,可以进行强制转换,至于为什么这样做而不是先获取__block_impl再获取FuncPtr,可能是因为直接进行类型转换在性能上会优越一点,毕竟少了一步获取__block_impl的过程。
  • 获取到FuncPtr之后,因为FuncPtr对应的其实就是我们的__main_block_func_0,它需要接收三个参数分别是:block本身以及两个形参a和b

由上分析上述代码可以简化为:

(__block_impl *)block->FuncPtr(block, 20, 30);

其实就是调用了myBlock中我们自己定义的实现。

Block访问(捕获)外部变量

int a = 10;
static int b = 20;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        auto int c = 30;
        static int d = 40;
        
        void(^myBlock)(void) = ^() {
            NSLog(@"a=%d, b=%d, c=%d, d=%d", a, b, c, d);
        };
        
        a++;
        b++;
        c++;
        d++;
        
        myBlock();
    }
    return 0;
}

执行以上代码,结果如下:

 a=11, b=21, c=30, d=41

发现除了自动变量c以外的全局变量a、静态全局变量b、静态变量d都变了。为什么呢?转换为底层代码来一探究竟。

int a = 10;
static int b = 20;

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_b7ba44_mi_0, a, b, c, (*d));
        }

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; 

        auto int c = 30;
        static int d = 40;

        void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, c, &d));
        a++;
        b++;
        c++;
        d++;

        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    return 0;
}

根据转换过来的c++源码,发现其中的结构体和函数之间的关系大致如下图所示:

局部变量/静态局部变量

我们先来看__main_block_impl_0结构体,和之前一样,局部变量c是以值拷贝的形式传到了结构体中,而静态变量d并不是值拷贝的形式被捕获,__main_block_impl_0结构体中保存了&d,即一个指向静态变量d的指针。

再看一下myBlock的实现代码

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int c = __cself->c; // bound by copy
  int *d = __cself->d; // bound by copy
  ...
}

c是值拷贝的自动变量,d是值拷贝的指针,这两个有什么区别呢?区别就在于我们在block内部访问的值c和外部的自动变量c没有任何关系。而我们通过*d访问到的还是外部的静态变量d,虽然拷贝了一份指针,但是他们还是指向了同一份内存空间。因此执行了d++以后我们执行myBlock访问到了修改以后的静态变量d

全局变量/静态全局变量

上面分析了cd,接下来我们看一下全局变量a、b

我们发现__main_block_impl_0内部并没有a、b这两个变量,但是在__main_block_func_0中直接就访问了这两个变量进行了输出。说到这里需要补充一下a、b、c、d这四种变量的声明周期和作用域。

在ios的内存分配中,全局变量a、全局静态变量b、静态变量d存放在全局区,并不会随时被销毁,其值一直会在内存中保持不变,直到整个程序结束时才销毁。局部变量c存放在栈区,因为作用域(或生命周期)有限,在作用域结束之后会被销毁,故block在引用时系统会自动将其值保存在block结构体中(即捕获)。

因此,block不需要捕获a、b,而是在使用的时候直接访问即可。静态变量d之所以拷贝了指针,是因为d属于局部静态变量,它的作用域在当前的函数内部,传递指针可以确保block在出了作用域之后仍然可以访问到d

oc对象

main.m代码如下:

typedef void(^blk_t)(id obj);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        blk_t blk;
        {
            id array = [[NSMutableArray alloc]init];
            blk = ^(id obj) {
                [array addObject:obj];
                NSLog(@"array count = %ld", [array count]);
            };
        }
        blk([[NSObject alloc]init]);
        blk([[NSObject alloc]init]);
        blk([[NSObject alloc]init]);
    }
    return 0;
}

array变量的作用于结束的同时,变量array被废弃,其强引用失效,因此赋值给变量arrayNSMutableArray类的对象必定被释放并废弃。但是该源代码运行正常,结果如下:

array count = 1
array count = 2
array count = 3

这一结果意味着赋值给变量arrayNSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。通过以下命令将main.m转换为c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

先上关系图:

我们一点一点来分析对应的代码

__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0其内部多了一个id类型的array

__main_block_desc_0

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};

发现__main_block_desc_0结构体中多了两个方法copydispose,这两个方法的参数都是和我们捕获的对象array有关。具体作用后面会讲到。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

__main_block_func_0 因为结构体__main_block_impl_0中的array其实就是一个指向array对象的指针,因此和外部栈上的array指向的是同一份内存空间,调用addObject方法是一样的,可以正常调用。

__block变量

除了以上几种,和block相关的还有一个__block类型的变量。__block修饰的变量被block捕获之后,在block的内部可以修改该变量的值,注意,修改的该变量本身的值。如果我们尝试修改非__block修饰的变量时,编译器会报一下错误:

同时,编译器也会提醒我们变量a缺少了__block修饰符。

typedef void(^blk_t)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        blk_t blk = ^{
            a = 1;
        };
        blk();
        NSLog(@"a=%d", a);
    }
    return 0;
}

修改为上述代码后,打印结果为:

a=1

变量a经过__block修饰之后就可以在block之中来进行修改了,接下来就来探索一下其可以修改的原因。

首先,还是通过命令将main.m进行转换。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

typedef void(*blk_t)(void);

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

            (a->__forwarding->a) = 1;
        }
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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        blk_t blk = ((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 *)blk)->FuncPtr)((__block_impl *)blk);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_dfd779_mi_0, (a.__forwarding->a));
    }
    return 0;
}

转换过后代码变得好长,我们一点一点来看,还是先上图:

__Block_byref_a_0

变量a经过__block修饰之后,结构发生了变化,已经不是一个简单的局部变量了,变成了一个__Block_byref_a_0类型的结构体。

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

main函数中,该结构体初始化的代码为

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

我们可以发现结构体中变量a的值为初始化的值10,而__forwarding是一个指向结构体本身的指针。暂且先了解到这里,具体原因后面再讲。

__main_block_impl_0

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;
  }
};

__main_block_impl_0里面也包含了一个__Block_byref_a_0*指针变量a,需要注意的是在结构体初始化方法中,指针a指向的地址并不是传进来的形参_a,而是_a->__forwarding

main函数中,该结构体初始化的代码为:

blk_t blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 1;
        }

并不是直接修改结构体中int类型变量a的值,而是修改了a->__forwarding结构体中int类型变量a的值。

__main_block_desc_0

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};

__main_block_desc_0相关的代码和block访问对象时类似,唯一的区别是copydispose函数的最后一个参数由3变成了8,即从BLOCK_FIELD_IS_OBJECT变成了BLOCK_FIELD_IS_BYREF。关于copydispose相关的内容会在后面进行解释。

__block对象

__block BlockObject *blockObject = [BlockObject new];
blk_t blk1 = ^(){
    NSLog(@"%@", blockObject);
};

转换之后:

struct __Block_byref_blockObject_0 {
  void *__isa;
__Block_byref_blockObject_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 BlockObject *blockObject;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_blockObject_0 *blockObject; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockObject_0 *_blockObject, int flags=0) : blockObject(_blockObject->__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_blockObject_0 *blockObject = __cself->blockObject; // bound by ref

            (blockObject->__forwarding->blockObject);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->blockObject, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_blockObject_0 blockObject = {(void*)0,(__Block_byref_blockObject_0 *)&blockObject, 33554432, sizeof(__Block_byref_blockObject_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((BlockObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BlockObject"), sel_registerName("new"))};
        blk_t blk1 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_blockObject_0 *)&blockObject, 570425344));
        struct Block_layout* block_layout1 = (__bridge struct Block_layout*)blk1;
    }
    return 0;
}

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);
}

上述代码看起来像是__block基本变量block访问变量二者的结合体。区别在于__Block_byref_blockObject_0结构体内部增加了copy/dispose的函数指针。具体作用后面会讲到。

Block的存储域

在上一部分,我们分别分析了block访问外部变量的几种情况:

  • 全局(包括全局静态)变量:直接访问
  • 局部非静态变量:值拷贝
  • 局部静态变量:拷贝指针
  • 对象:拷贝指针
  • __block变量:转换为结构体,保存__forwarding指针

其中还留有一些疑问,主要包括:

  1. 上面几种情况下C++源码中__main_block_impl_0isa全部都是&_NSConcreteStackBlock,这个是什么意思?还有没有其他的值?
  2. __block__forwarding指针有什么作用?为什么要这样设计?
  3. block捕获对象的时候,为什么array出了作用域还能够存在?
  4. __main_block_desc_0中的copydispose函数是干啥用的?

接下来我们来解答这些问题

isa

先看下面的这段代码:

typedef void(^blk)(void);

int a = 1;

blk blk0 = ^(){
    NSLog(@"%d", a);
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        blk blk1 = ^{
            NSLog(@"%d", a);
        };
        
        static int b = 2;
        blk blk2 = ^{
            NSLog(@"%d", b);
        };
        
        blk blk3 = ^{
            
        };
        
        int d = 4;
        blk blk4 = ^{
            NSLog(@"%d", d);
        };
        
        NSLog(@"blk0=%@", blk0);
        NSLog(@"blk1=%@", blk1);
        NSLog(@"blk2=%@", blk2);
        NSLog(@"blk3=%@", blk3);
        NSLog(@"blk4=%@", blk4);
        
        int e = 5;
        NSLog(@"blk5=%@", ^{e;});
        NSLog(@"blk6=%@", ^{a;});
        NSLog(@"blk7=%@", ^{});
    }
    return 0;
}
打印结果为:
blk0=<__NSGlobalBlock__: 0x100001030>
blk1=<__NSGlobalBlock__: 0x100001050>
blk2=<__NSGlobalBlock__: 0x100001070>
blk3=<__NSGlobalBlock__: 0x100001090>
blk4=<__NSMallocBlock__: 0x10061ecf0>
blk5=<__NSStackBlock__: 0x7ffeefbff448>
blk6=<__NSGlobalBlock__: 0x1000010d0>
blk7=<__NSGlobalBlock__: 0x1000010f0>

将上述结构进行分类如下所示:

捕获变量类型 声明位置 类型
全局变量 全局区域 Global
全局变量 main内 Global
静态局部变量 main内 Global
未捕获变量 main内 Global
自动变量 main内 Malloc
自动变量(直接打印) main内 Stack
全局变量(直接打印) main内 Global
未捕获变量(直接打印) main内 Global

NSStackBlock

该类的对象Block设置在栈上,我们知道栈上的变量会随着作用域的结束而被释放。通过转换C++的源码可以发现,其实除了在全局区域声明的blk0isa&_NSConcreteGlobalBlock,其他的blk全部为_NSConcreteStackBlock,但是打印的结果来看只有直接打印blk5才属于__NSStackBlock__,但是这种类型的block没有办法使用,因为没有一个变量来指向它,如果直接声明之后就调用这种情况又没有意义。为什么__NSStackBlock__最终会转换成__NSGlobalBlock__或者__NSMallocBlock__呢?

NSGlobalBlock

__NSGlobalBlock__顾名思义,和全局变量一样,该类的对象Block设置在程序的数据区域(.data区)中。
通过C++源码我们可以发现,当block捕获全局变量时,它的isa&_NSConcreteGlobalBlock,这个没有问题。但是发现在main函数内部声明的blk1、blk2、blk3、blk6、blk7isa&_NSConcreteStackBlock,只不过最终打印出来的是__NSGlobalBlock__。也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

总结以下两种情况生成的block属于__NSGlobalBlock__

  • 记述全局变量的地方有Block语法时
  • Block语法的表达式中不使用应截获的自动变量时。

NSMallocBlock

配置在栈上的Block会随着所属的变量作用域的结束而被废弃,如果Block当中有__block变量,该变量同样也会随着作用域的结束而被废弃。这就是__NSMallocBlock__的由来,Blocks提供了将Block__block变量从栈上复制到堆上的方法来解决这个问题。这样即使栈上Block的变量作用域结束,堆上的Block还可以继续存在。此时Blockisa将由_NSConcreteStackBlock变为_NSConcreteMallocBlock
我们接着来看如下代码:

typedef void(^blk_t)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        NSLog(@"a的地址为%p", &a);
        ^{
            a = 1;
            NSLog(@"stack-a的地址为%p", &a);
        }();
        blk_t blk = ^{
            a = 1;
            NSLog(@"heap-a的地址为%p", &a);
        };
        blk();
        NSLog(@"a的地址为%p", &a);
    }
    return 0;
}
打印输出为:
a的地址为0x7ffeefbff4b8
stack-a的地址为0x7ffeefbff4b8
heap-a的地址为0x1007049c8
a的地址为0x1007049c8

如上图所示,在blk_t blk赋值之前的两次打印打印的是栈上的地址。

第三次和第四次打印的时候,__Block_byref_a_0结构体a已经被复制到了堆上,因此打印出来的的是堆上的地址。此时,栈上的结构体a可以通过__forwarding指针访问到堆上的a,即使栈上的变量废弃了,堆中的变量依然存在,可以正常访问。

那么什么情况下,栈上的block会被复制到堆上呢?大多数情况下编译器会适当的进行判断,自动生成将Block从栈上复制到堆上的代码。

  • 调用Blockcopy方法时
  • Block作为函数返回值返回时
  • Block赋值给__strong修饰符id类型的变量或者Block类型的成员变量
  • 在方法名中含有usingBlockCocoa框架方法或者Grand central DispatchAPI中传递Block时。

不过此外的情况需要我们手动进行copy生成将block从栈上复制到堆上的代码,以下是一个需要我们手动复制Block的一个例子:

@implementation BlockObject
+ (NSMutableArray *)blockArray {
    int val = 10;
    return [[NSMutableArray alloc]initWithObjects:
            [^{NSLog(@"blk0:%d", val);}copy],
            [^{NSLog(@"blk1:%d", val);}copy], nil];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [BlockObject blockArray];
        typedef void(^blk_t)(void);
        blk_t blk0 = array[0];
        blk_t blk1 = array[1];
        blk0();
    }
    return 0;
}

如果不加copy会导致blk_t在出了作用域之后被废弃,出现野指针异常

Block源码剖析

接下来我们看一下关于Block的真正源码。

Block.private

通过苹果源码当中Block_private可以发现block的结构如下:

block结构体

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

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

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

虽然Block_layout只有Block_descriptor_1,但是实际上会根据flags的值在Block_descriptor_1内存区域的后面追加Block_descriptor_2或者Block_descriptor_3,后面会对这一块进行分析。

接下来看一下和Block_layout->flags相关的定义。

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime Block被释放
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime 引用计数掩码
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime 标识堆block
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler 含有copy/dispose辅助函数
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    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_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

既然我们知道了Block_layout的结构,我们怎么验证呢?其实也很简单,代码如下:

虽然源码不能运行,我们可以通过手写结构体,然后进行类型转换的方式来查看对应位置的值。

通过上图可以发现,blkblock_layout是可以一一对应的,同时block_layout也持有一个object的指针,指向我们创建的object对象,这也从侧面验证了block容易造成循环引用。

  • 关于flags

细心的读者可以发现,flags的值是一个负数。这是什么原因呢?

可以发现,其实是因为falgs是一个4字节int值,它的首位为1,所以在打印的时候系统认为第一位是符号位,被转成了一个负值。将诶下来我们验证一下falgs的值。

根据上面二进制的打印,可以看出,当前flags的组成为

BLOCK_NEEDS_FREE|BLOCK_HAS_COPY_DISPOSE|BLOCK_HAS_SIGNATURE|BLOCK_HAS_EXTENDED_LAYOUT

同时block的引用计数为1,和上上图中的内容相符合。

  • 关于Block_descriptor_2Block_descriptor_3的取值

用图表示就是:

通过源码可以发现,Block_descriptor_2Block_descriptor_3并没有提供类似指针的方法可以获取,而是只能通过Block_descriptor_1的指针加上对应的偏移量来获取,因为Block_descriptor_2Block_descriptor_3并不是一定会存在的。

__block结构体

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;
};

Block_byref->flags的定义如下:

// Values for Block_byref->flags to describe __block variables
enum {
    // Byref refcount must use the same bits as Block_layout refcount
    // 引用计数必须和block_layout的引用计数用同样的位数
    // BLOCK_DEALLOCATING =      (0x0001),  // runtime
    // BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime

    BLOCK_BYREF_LAYOUT_MASK =       (0xf << 28), // compiler
    BLOCK_BYREF_LAYOUT_EXTENDED =   (  1 << 28), // compiler
    BLOCK_BYREF_LAYOUT_NON_OBJECT = (  2 << 28), // compiler
    BLOCK_BYREF_LAYOUT_STRONG =     (  3 << 28), // compiler
    BLOCK_BYREF_LAYOUT_WEAK =       (  4 << 28), // compiler
    BLOCK_BYREF_LAYOUT_UNRETAINED = (  5 << 28), // compiler

    BLOCK_BYREF_IS_GC =             (  1 << 27), // runtime

    BLOCK_BYREF_HAS_COPY_DISPOSE =  (  1 << 25), // compiler
    BLOCK_BYREF_NEEDS_FREE =        (  1 << 24), // runtime
};

用同样的操作,我们来看一下断点的值

runtime.cpp

接下来我们看一下相关的具体方法实现。

首先我们需要注意的是几个枚举值

/ 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, ... oc对象类型
    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结构体标识
};

enum {
    BLOCK_ALL_COPY_DISPOSE_FLAGS = 
        BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_BYREF |
        BLOCK_FIELD_IS_WEAK | BLOCK_BYREF_CALLER
};

block内存相关的方法有以下几个:

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
//基于copy在堆上创建一个Block或者只是单纯的增加一个已经存在的Block的引用计数,必须和Block_release配对使用来还原内存。
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
// 减少引用计数,如果是堆上的最后一个引用,恢复内存
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
//辅助函数,编译器使用,不要手动调起
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
//辅助函数,编译器使用,不要手动调起
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

_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;
    //如果是堆上的block,增加引用计数
    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.
        // block在栈上,需要copy到堆上
        // 1. 在堆上开辟同等大小的内存空间
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        // 2. 从栈上的block复制同等大小的字节内容到新开辟的堆内存中
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        // 3. 堆上block的invoke指针指向正确的位置
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 4. 重置引用计数, BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING刚好是0xffff,取反则变成0x0000
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 5. 引用计数置为1, 因为flags第1位存放的是BLOCK_DEALLOCATING,因此真实的引用计数从第2位开始。 2可以看做是二进制的10
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // 6. 调用辅助函数
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // 7. 更正isa的值为malloc block
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

latching_incr_int

static int32_t latching_incr_int(volatile int32_t *where) {
    while (1) {
        //旧的flags值
        int32_t old_value = *where;
        //如果与上掩码得到的值和掩码一样,说明引用计数达到最大,直接返回。这种情况基本不会出现。
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        //引用计数+1,因为是从第二位开始,因此是以2为单位,引用计数的+1表现为数据上的+2
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

_Block_object_assign

_Block_call_copy_helper函数最终会调用_Block_object_assign函数,该函数根据不同的flags值,在内部进行判断,执行了不同的操作。

实现如下:

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment. 当Blocks或者Block_byrefs持有objects时他们的copy辅助会用这个方法来进行赋值
//
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    //传递的是指针的地址,确保可以正确的修改对应的值
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      //捕获对象
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        //该方法为空方法
        _Block_retain_object(object);
        //一个新的指针指向object,此时object的引用计数就会增加
        *dest = object;
        break;
      //block内部捕获了另一个block
      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
        *dest = _Block_copy(object);
        break;
      //block内部捕获了__block变量或者__weak __block变量,此时需要拷贝block_byref结构体到堆上
      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;
      //以下为block_byref结构体拷贝过程中,内部的变量类型的处理,会在_Block_byref_copy方法内部调起
      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使用到的场景主要有以下几个:

  • block语法内部捕获了oc对象__block变量,将block从栈上拷贝到堆上时
  • __block变量oc对象Block_byref结构体从栈上拷贝到堆上时

_Block_byref_copy

static struct Block_byref *_Block_byref_copy(const void *arg) {
    //src表示栈上的Block_byref结构体
    struct Block_byref *src = (struct Block_byref *)arg;
    //引用计数为0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 在堆上开辟新的空间
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        // isa指针置为null,Block_byref结构体不是对象。
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        // 标识为堆结构体并且引用计数为2,一个为src->forwading, 一个为copy->forwading
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        // 复制对应的辅助函数
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            // 通过地址偏移拿到Block_byref_2结构体
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            //keep/destroy函数的最终实现也是_Block_object_assign/_Block_object_dispose,只不过传递的参数为Block_byref结构体,需要通过地址偏移来拿到对应的对象。如下:
            // 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);
            // }
            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.
            // 从copy的下一位开始,放入Block_byref_3,可以发现src-size并不是Block_byref结构体的大小,而是整体的大小
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // 引用计数不为0,表示已经复制到堆上了,增加引用计数
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

到此为止,copy相关的内容已经结束了,接下来看一下release相关的。

_Block_release

// API entry point to release a copied Block
void _Block_release(const void *arg) {
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    //全局block不需要处理
    if (aBlock->flags & BLOCK_IS_GLOBAL) return;
    //栈上的block不需要处理
    if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
    //判断需不需要释放
    if (latching_decr_int_should_deallocate(&aBlock->flags)) {
        //调用release的辅助函数
        _Block_call_dispose_helper(aBlock);
        //销毁block
        _Block_destructInstance(aBlock);
        //释放内存
        free(aBlock);
    }
}

接下来看一下判断block是否需要释放的函数

latching_decr_int_should_deallocate

// return should_deallocate?
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
    while (1) {
        int32_t old_value = *where;
        // 引用计数最大
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return false; // latched high
        }
        // 引用计数为0
        if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
            return false;   // underflow, latch low
        }
        // 引用计数-1,之前说过引用计数是从第2位开始,第一位为是否在释放的标识。因此需要-2
        int32_t new_value = old_value - 2;
        bool result = false;
        //如果引用计数刚好为1。二进制为10,即2
        if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
            //-1会导致第一位为1,即BLOCK_DEALLOCATING为1,标识在释放了
            new_value = old_value - 1;
            result = true;
        }
        //赋值
        if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
            return result;
        }
    }
}

接下来看_Block_call_dispose_helper

_Block_call_dispose_helper

// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents
void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
        // __block需要释放Block_byref结构体
        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;
        // 捕获的block需要被释放
        case BLOCK_FIELD_IS_BLOCK:
            _Block_release(object);
            break;
        // 捕获的对象需要调用release
        case BLOCK_FIELD_IS_OBJECT:
            //实现为空,block释放的时候,指向object的指针被释放,因此object引用计数也会减少
            _Block_release_object(object);
            break;
        // 堆上的Block_byref不做处理,在上面BLOCK_FIELD_IS_BYREF的case里面已经处理过了,Block_byref结构体释放后,相关指针也会被释放,对应的引用计数会自然-1,相关的对象也会被释放。
        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;
    }
}

_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;
    //如果是Block_byref结构体在堆上
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        //判断是否需要释放
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            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);
        }
    }
}