iOS - block

image

三年前,第一次写关于 block 的东西,就是初识 block,了解了些皮毛,但发现,那么仅仅是 block 的冰山一角,关于 block 还有很多需要参透和理解。

block 本质

block 的本质是一个 Objective-C 对象,其内部也有 isa 指针,block 中封装了函数的调用以及函数调用环境的 Objective-C 对象。它的结构如下:

image

  1. 函数的调用相当于函数的调用地址
  2. 函数调用环境指参数,访问 block 外部的值等

一段下面的 block:

void(^block)(int a, int b) = ^(int a, int b) {
            NSLog(@"a + b = %d", a + b);
};
block(1, 2);

用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m(下同) 重写后 C++ 代码是这样的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

__block_impl 的声明:

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

__main_block_desc_0 的声明:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};

Block_size 表示 __main_block_impl_0 能占多少内存。

假如 block 内使用了外部变量,如:

int outter = 35;
void(^block)(int a, int b) = ^(int a, int b) {
    NSLog(@"outter is %d", outter);
    NSLog(@"a + b = %d", a + b);
};
block(1, 2);

本质结构为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int outter;
};

若我们在 .m 文件中自行实现这些结构体:
[图片上传失败...(image-e5da00-1555345050037)]
然后进行转换:

struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;

加断点运行后进入 LLDB 调试环境可看到 blockStruct 的信息:


image

发现 outter 已经封装到 blockStruct 的内存中去了。

我们记录下 __FuncPtr 后面的内存地址 0x0000000100000ee0,然后在 block 块内增加断点并过掉当前断点,当程序停留在 block 块内的断点的时候,然后 Debug -> Debug Workflow -> Always Show Disassembly 会看到如下界面:
[图片上传失败...(image-fe45c2-1555345050037)]
第一行 0x100000ee0 <+0>: pushq %rbp 的地址就是__FuncPtr 的地址,这说明 block 块内的代码都封装到了函数里面,这个函数的首地址(例子中的 0x0000000100000ee0)在 block 结构体的成员结构体 __block_impl 中。

深入探究

底层数据结构

main 函数中例子的代码 C++ 的实现为:

int outter = 35;
// 定义 block 变量
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 执行 block 内部代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

去除强制转换的干扰代码,简化后:

int outter = 35;
// 定义 block 变量
void(*block)(int a, int b) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 执行 block 内部代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

这里的 block 会指向什么?首先得明白 _main_block_impl_0() 会返回什么?我们在 .cpp 文件中发现该函数在 __main_block_impl_0 的结构体中:

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

该函数接收 4 个参数,flags 默认为 0,并且函数名和结构体名相同,是 C++ 中的构造函数,和 Java 的构造函数道理类似,也和 Objective-C 中的 init 方法类似,并且无任何返回。

outter(_outter) 表示传进来的 _outter 的值会赋给结构体成员变量 outter,相当于 outter = _outter

4 个传入的参数中 outter 不必多言,那么来 __main_block_func_0 是什么:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int outter = __cself->outter;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_0, outter);
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_1, a + b);
}

可见,__main_block_func_0 封装了 block 执行逻辑的函数。__main_block_func_0 对应 __main_block_impl_0 构造方法中的 void *fp, fp 赋值给了 impl.FuncPtr。这样 impl.FuncPtr 存储的就是执行逻辑的函数的地址。

在该构造方法中同时初始化了 isa 指针:

impl.isa = &_NSConcreteStackBlock;

说明 block 的类型为 _NSConcreteStackBlock

回过头我们再看传入的第二个参数 &__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)};

该处 0 赋值给了 reservedsizeof(struct __main_block_impl_0) 计算该 block 结构体的大小并将结果赋值给 Block_size

&__main_block_desc_0_DATA 对应 __main_block_impl_0 构造方法中的 struct __main_block_desc_0 *desc, 并赋值给了 Desc。换而言之 __main_block_impl_0 中的 Desc 指向的是 __main_block_desc_0 结构体变量。

所以在执行结构体的构造函数的时候,outter 为 35。倘若在外部将 outter 重新赋值,结构体中的 outter 是不会更改的。也就是说 outter 是以值传递的形式传递的。

__main_block_func_0 中:

int outter = __cself->outter; 

该步骤为取出 outter 的值(35)。

block 的变量捕获

为确保 block 能正确访问外部变量,block 有变量捕获机制,如下图:


auto: 局部变量默认是 auto 修饰的:int a = 0; 等价于 auto int a = 0;,它表示自动变量,离开作用域后自动销毁。

那么 block 中的捕获是什么意思?就是 block 内部会新增一个成员变量用来存储外部变量的值,这个过程为捕获。

auto 修饰的变量

上一节例子中的 int 型 outter 就是自动变量,默认 auto 修饰。其访问方式是值传递

static 修饰的变量

我们添加静态变量 outter2:

int outter = 35;
static int outter2 = 1210;
void(^block)(int a, int b) = ^(int a, int b) {
            NSLog(@"outter is %d", outter);
            NSLog(@"outter2 is %d", outter2);
            NSLog(@"a + b = %d", a + b);
};
block(10, 20);

运行后打印了 outter 和 outter2 的值。说明无论是 auto 修饰还是 static 修饰的外部变量,block 内部都是能捕获到的。
那么内部访问方式是否一样?重写 C++ 代码后发现:

void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter, &outter2));

outter2 是以 &outter2 传入 __main_block_impl_0 结构体的构造方法的,并且 __main_block_impl_0 中的 outter2 是:

struct __main_block_impl_0 {
  ...
  int outter;
  int *outter2;
  ...
};

发现这里的 outter2 是通过传址的方式传进去的,在打印的 C++ 实现中 outter2 是这样取值的:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_e5faae_mi_1, (*outter2));

*outter2 这样的取值方式是直接取出外面静态变量内存里的值。

Q:为什么会有这样的差异?
A:因为 auto 修饰的变量是可能自动销毁的,而 block 执行的时机未定,所以存在 block 执行内部代码的时候变量已经销毁的情况,这会导致程序的 Crash,所以外部变量需进行值传递。而 static 修饰的变量会一直存在于内存当中,不存在 block 执行的时候变量已经销毁的情况。

全局变量

我们验证全局变量的捕获机制,添加一个全局的成员变量 outter3:

int static outter3 = 1314;

并在内部打印,发现打印 1314,若在执行 block 之前修改 outter3 的值:

void(^block)(int a, int b) = ^(int a, int b) {
    NSLog(@"outter is %d", outter);
    NSLog(@"outter2 is %d", outter2);
    NSLog(@"outter3 is %d", outter3);
    NSLog(@"a + b = %d", a + b);
};
outter3 = 999;
block(10, 20);

打印得 outter3 = 999,看似和局部静态变量的道理一样,我们看下 C++ 实现得 __main_block_impl_0 结构体:

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

发现并无 outter3,在 block 内部打印的地方为:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_147262_mi_2, outter3);

也就是说,全局变量并没有捕获到 block 内部,而且,内部访问全局变量是直接访问的。

那么假如 block 中是如何捕获 self 的呢?
我们新建 Test 类:
.h:

@interface Test : NSObject

@property(nonatomic, copy) NSString* param;

- (void)test;
- (instancetype)initWithParam:(NSString*)param;

@end

.m:

@implementation Test

- (void)test {
    void(^block)(void) = ^{
        NSLog(@"====>%p", self);
    };
    block();
}

- (instancetype)initWithParam:(NSString*)param
{
    self = [super init];
    if (self) {
        self.param = param;
    }
    return self;
}

@end

外部调用 test() 方法便可执行 block,并访问内部的 self。打印:

====>0x10070be20

重写 Test.m 文件后发现其 block 结构为:

struct __Test__test_block_impl_0 {
  struct __block_impl impl;
  struct __Test__test_block_desc_0* Desc;
  Test *self;
  __Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, Test *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

发现 self 是通过指针传递进来的。而且可推导,既然能捕获,说明 self 是局部变量。
我们可看到 test() 函数的底层为:

static void _I_Test_test(Test * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

发现底层函数默认添加了两个参数:self_cmd,这也是我们为什么能在函数内可以调用到 self 和 _cmd 的原因。
若假如打印 _param 呢?运行

Test* t = [[Test alloc] initWithParam:@"something"];[t test];

发现可打印:

something

此时是捕获的 _param?错,_param 等价于 self->_param,所以捕获的还是 self。

block 的类型

block 有三种类型,亦是可以通过 class 方法或者查看 isa 指针查看其具体类型,但最终都是继承自 NSBlock

类型
_NSGlobalBlock_ 全局 block
_NSStackBlock_ 栈区 block
_NSMallocBlock_ 堆区 block

我们为探究其类型,运行:

void(^block)(void) = ^{
     NSLog(@"This is a block");
};
block();
NSLog(@"%@", [block class]);

打印:

This is a block
__NSGlobalBlock__

追加打印:

NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);

得:

__NSGlobalBlock
NSBlock
NSObject

现在得到继承链:


那么现在打印:

void(^block)(void) = ^{
            NSLog(@"This is a block");
        };
        
int num = 10;
void(^block1)(void) = ^{
    NSLog(@"The num is %d", num);
};
        
NSLog(@"%@ %@ %@", [block class], [block1 class], [^{
    NSLog(@"The num is %d", num);
} class]);

得:

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

重写后我们可看见有三个 block:__main_block_impl_0、__main_block_impl_1、__main_block_impl_2。
但奇怪的是三者得 isa 指针都指向的是 _NSConcreteStackBlock

为什么会出现这样的问题,是因为在重写命令中通过 clang 转成的 C++ 代码并不能完全代表 Objective-C 最终的底层实现。

所以我们还是按照打印的标准也判断 block 的类型,可发现, block 的存储类型和捕获外部的局部变量也有关系。

image

text 区存放的是程序代码,data 区存放的是全局变量,堆区放的是 alloc 出来的对象,动态分配内存,需要开发者手动调用,也需要开发者主动管理内存(现在有 ARC 了),栈区放的是局部变量,系统自动销毁内存。

具体的 block 类型是区分的?如下表:

block 类型 区别
_NSGlobalBlock_ 没有访问 auto 变量
_NSStackBlock_ 访问 auto 变量
_NSMallocBlock_ _NSStackBlock_ 调用了 copy

对于 NSStackBlock 的 block 存在一个问题,代码如下:

void(^block)(void);

void test() {
    int num = 35;
    block = ^{
        NSLog(@"The num is %d", num);
    };
    NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
}

运行结果为:

__NSStackBlock__
The num is -272632472

做这个实验请将内存管理改为手动(MRC):Build Setting -> Objective-C Automatic Reference Counting -> No

为何会出现 -272632472?是因为,执行过 test() 后栈区的 对应数据被回收,存在的可能就是垃圾数据,那么再访问结构体内的成员的时候得到的就是这些垃圾数字。

将上述代码稍作改动,test 内的 block 改为:

block = [^{
    NSLog(@"The num is %d", num);
} copy];

打印结果为:

__NSMallocBlock__
The num is 35

此时的 block 已经进行了 copy 操作,栈 block 变为堆 block,内存需要我们手动释放,而我并没有释放,所以打印的 num 是正确的。

产生疑惑,_NSGlobalBlock_ 类型的栈进行了 copy 操作会变成 _NSMallocBlock_ 类型吗?
去掉 block 内部对 num 的打印再来运行发现:

__NSGlobalBlock__

即使使用了 copy 操作,block 依然为 _NSGlobalBlock_ 类型。

copy 操作

由上节可知,对于 _NSStackBlock_ 类型的 block 有太多的不确定性,所以在对这种 block 使用的时候需要对其进行一次 copy 操作将栈 block 复制到堆区。

但上节的例子是基于 MRC 的环境下操作的,在 ARC 的环境下,编译器会根据情况自动讲 block 进行 copy 操作。
在 ARC 环境下执行:

void(^block)(void);

void test() {
    int num = 35;
    block = ^{
        NSLog(@"The num is %d", num);
    };
    NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
}

得:

__NSMallocBlock__
The num is 35

在以下条件下,编译器会自动将 block 进行 copy 操作:

  • block 作为返回值
  • 将 block 复制给 __strong 指针时
  • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
    如:
NSArray* arr = ...;
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];

对象类型的 auto 变量捕获

前面的例子内部捕获外部变量都是基本类型,如 int,那么对象类型的外部变量是如何捕获的?
将 Test 类,添加 NSInteger 类型的属性 num。
外部:

Test* test = [[Test alloc] init];
test.num = 35;
        
TestBlock block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};
block();

TestBlock 定义为:typedef void(^TestBlock) (void);
运行得:

The num is 35

我们稍作改动:

TestBlock block;
{
    Test* test = [[Test alloc] init];
    test.num = 35;
            
    block = ^{
         NSLog(@"The num is %ld", (long)test.num);
    };
}
NSLog(@"=====end=====");

也重写了 Test 的 dealloc() 方法打印 dealloc。我们增加断点在打印 “end” 的一行,运行发现断点处,并没有打印 Test 的 delloc 信息,也就是说,内部 {} 执行完了 Test 也没有立即被销毁。
我们将代码改成:

Test* test = [[Test alloc] init];
test.num = 35;
        
TestBlock block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};
 NSLog(@"=====end=====");

重写后发现 block 的结构体中有 Test *test 成员变量。回到修改之前的代码,在执行:

block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};

的时候,block 进行了 copy 操作成为堆区的 block,不会轻易销毁,那么意味着对 test 也是强引用持有,test 亦不会轻易被释放,所以 dealloc 信息延后打印:

=====end=====
=====dealloc=====

若是 MRC 环境(需添加 [t release] 操作,并且 dealloc 方法内须调用父类的 dealloc 方法),即使 block 还在,也会先执行 Test 的 dealloc 方法。结果为:

=====dealloc=====
=====end=====

若在 MRC 环境下改为:

block = [^{
    NSLog(@"The num is %ld", (long)test.num);
} copy];

则会达到 ARC 下同样的效果,因为进行了 copy 操作后在 block 内部相当于调用了一次 [t reatain] 操作。结果为:

=====end=====
=====dealloc=====

回到 ARC 环境,假如 Test 对象进行 __weak 修饰,则情况又有所不同:

=====dealloc=====
=====end=====

在用 __weak 修饰的情况下重写 C++ 代码会报错:

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

是因为命令需要支持 ARC 并且指定运行时系统版本,如:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m

重写成功后发现 block 结构体为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Test *__weak test;
  ...
};

test 对象为 weak 修饰,所以在离开作用域后立即释放。去掉 weak 后的结构体再用上命令重写,得到:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Test *__strong test;
  ...
};

发现 weak 默认用了 strong 修饰,所以“延长了”其寿命。
最后来个总结:

  • 当 block 在栈上,不会对 auto 变量产生强引用
  • 当 block 在堆上,会根据 auto 是否由 __strong 或者 —__weak 修饰来决定是否产生强引用 [下有说明]
  • 当 block 从堆上移除,将放弃对 auto 变量的引用,相当于进行了一次 release 操作

copy 操作后的 block 其 Desc 是有变化的:

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

原本只有 reservedBlock_size 现在又多了两个函数指针: copydisposecopy 保存的是 __main_block_copy_0,dispose 保存的是 __main_block_dispose_0
当 block 执行了 copy 操作后,这两个函数便会执行。
__main_block_copy_0 和 __main_block_dispose_0是现实:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
// 会根据 test 对象是 strong 还是 weak 修饰来决定是否对 test 对象产生强引用
_Block_object_assign((void*)&dst->test, (void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
// 对 test 对象进行释放
_Block_object_dispose((void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
函数 调用时机
copy 栈上的 block 复制到堆时
dispose 堆上的 block 被收回时

__block

我们再来新建一个例子工程:

typedef void(^TestBlock) (void);

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

        int num = 10;
        TestBlock block = ^{
            NSLog(@"The num is %d", num);
        };
        block();
    }
    return 0;
}

运行上面这段代码,结果为:

The num is 10

那么实际情况中,我们常常需要在 block 内部改变外面变量的值,在 block 内部直接修改是不允许的:

^{
    num = 35; // ✘
}

这是因为 num 的作用域属于 main 函数,而 block 内执行逻辑属于另一个函数 __main_block_func_0,是无法跨域进行修改的。

但是通过 static 修饰的局部变量是可以用这种方式修改的:

static int num = 10;
TestBlock block = ^{
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

结果为:

The num is 35

因为 static 修饰的是引用传递,block 的结构体存储的是指向 num 的指针,所以在内部修改 num 的值是可以成功的。

那么如何修改非 static 修饰的的局部变量?就是 __block 关键字。

__block int num = 10;
TestBlock block = ^{
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

结果:

The num is 35

__block 本质

__block 变量不能修饰全局变量、静态变量。并且编译器会将 __block 变量包装成一个对象。
重写 C++ 代码后发现 block 结构体 num 的成员变量和之前未用 __block 修饰的 num 有本质的区别:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  ...
};

这里的 num 为 __Block_byref_num_0 * 类型。__Block_byref_num_0 也是个结构体,其内部定义是这样的:

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

我们可推断一开始 num 的值为 10,这个值一定是存储在 __Block_byref_num_0 的成员变量 num 中。那么 __forwarding 表示什么?
首先我们看到由 __block 修饰后的 num,在 main 函数的源码中变成了:

__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};

简化版本:

__Block_byref_num_0 num = {(0,
                            &num,
                            0,
                            sizeof(__Block_byref_num_0),
                            10};

此时第一个 0 赋值给 __isa,第二个 0 赋值给 __flags,第四个参数是计算当前结构体有多大并赋值给 __size,最后 10 赋值给 num,推断得到验证。第二个参数 &num 就是 num 结构体本身,也就是说它将自身的结构体地址传递给了 __forwarding。换而言之 __forwarding 指向的是自己。

image

同时 &num 也传给了 __main_block_impl_0 的 *num

TestBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));

block 修改 num 的源码为:

// 首先拿到 __Block_byref_num_0 中的 __forwarding
__Block_byref_num_0 *num = __cself->num;
// 取得 num 再修改
(num->__forwarding->num) = 35;

倘若多加了一个对象类型的局部变量:

__block int num = 10;
__block NSObject* obj = [[NSObject alloc] init];
TestBlock block = ^{
    obj = nil;
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

num 和 obj 在底层会生成两个机构体:

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 // copy 操作
 void (*__Block_byref_id_object_copy)(void*, void*);
 // dispose 操作
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj;
};

block 结构体会有两个成员变量指向它们在这里不贴出。

我们去掉对象类型的 obj 回到最简状态,在 block() 后打印 num 的内存地址,得:

0x10051e968

这个内存地址和底层的谁有对应关系?是 __main_block_impl_0 中的 *num?还是 __Block_byref_num_0 中的 num?我们自己实现这些低层结构:


然后运行:

__block int num = 10;
TestBlock block = ^{
           
    num = 35;
    NSLog(@"The num is %d", num);
};
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
NSLog(@"%p", &num);

在最后一行加断点发现 __Block_byref_num_0 * 型 num 的地址为:0x000000010204b490,打印局部变量的 num 为 0x10204b4a8,两者并不相同。
0x000000010204b490 为 __Block_byref_num_0 * 型 num 的地址也就意味着是 __isa 的地址,那么 age 的地址是什么?
__isa 大小为 8,__forwarding 大小为 8(地址为 0x000000010204b498),__flags 大小为 4(地址为0x000000010204b4a0), __size 大小为 4(地址为0x000000010204b4a4),num 的地址为 0x000000010204b4a8。是不是很眼熟?没错 num 的地址和外部变量的 num 一样。
通过:

print/x &(blockStruct->num->num)

命令得到的打印结果和 NSLog(@"%p", &num); 得到的结果也是一样的也可以验证。

__block 内存管理

我们来看这个熟悉的例子:

int num = 0;
TestBlock block = ^{
     NSLog(@"%d", num);
};
block();

底层的 __main_block_desc_0 是没有 copydispose 两个成员函数的,但是当 num 用 __block 的时候就多了这两个函数,并在 copy 函数中调用 _Block_object_assign() 对 结构体中的 __Block_byref_num_0 *num 进行内存管理。
假如有 Block 0 和 Block 1 分别对 __block 变量引用,则:

在 ARC 环境下首先 Block 0 会 copy 到堆上,然后 __block 修饰的变量也同样会 copy 到堆上,然后进行强引用。
然后 Block 1 也会 copy 到堆上并对 __block 变量有强引用:


image

当 block 从堆上移除的时候,首先会调用内部 dispose 函数,其内部会调用 _Block_object_dispose() 函数,然后释放 __block 变量:

image

若外部是:

__block int num = 0;
__block  NSObject* obj == ...;
TestBlock block = ^{
     ...
};
block();

则底层对 int 和 obj 都会产生强引用。

_Block_byref名字_0 就是强引用

若:

 __block int num = 0;
NSObject* obj = [[NSObject alloc] init];
__weak NSObject* weakObj = obj;
TestBlock block = ^{
     ...
};
block();

则底层不会对 weakObj 产生强引用。

另,我们在 C++ 代码中看到:

__main_block_impl_0*src) {
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

8 表示 __block 修饰的变量,对应注释:BLOCK_FIELD_IS_BYREF
3 表示对象,对应注释:BLOCK_FIELD_IS_OBJECT

__block 的 __forwarding 指针

当 block 在栈上时,__forwarding 指针指向自己。那么堆上的 __forwarding 指向谁呢?答案也是自己,但是需要注意的是,经过 copy 操作后,原栈上的 __forwarding 指针指向堆上的 block,即:

image

循环引用问题

当对象对 block 本身有强引用,而 block 又对对象持有,则会引发循环引用。如:

Test* t = [[Test alloc] init];
t.num = 35;
t.block = ^{
    NSLog(@"%ld", t.num);
};

ARC

使用 __weak 和 __unsafe_unretained 解决

在 ARC 环境下可通过,__weak__unsafe_unretained 解决:

Test* t = [[Test alloc] init];
t.num = 35;
__weak Test* weakT = t;
t.block = ^{
    NSLog(@"%ld", weakT.num);
};

或者:

Test* t = [[Test alloc] init];
t.num = 35;
__weak typeof(t) weakT = t;
t.block = ^{
    NSLog(@"%ld", weakT.num);
};

对于 self 的情况也是同理:

__weak typeof(self) weakSelf = self;
image

__unsafe_unretained 同理,但 __unsafe_unretained 是不安全的,若 __weak 指向的对象销毁,则 weakXXX 会自动置为 nil但 __unsafe_unretained 不会,它还是会指向那个销毁对象的地址,所以进行访问 weakXXX 的时候很有可能产生野指针错误。

使用 __block 解决

__block 情况下的循环应用如下:

image

必须调用 block 的情况下还可以使用 __block 来解决。

__block id weakSelf = self;

并且 block 内部的 weakSelf 要职位 nil:

xxx.block = ^{
    ...
    weakSelf = nil;
};

因为一旦 weakSelf 置为 nil,三者互相“僵持不下”的状态就会打破,也就不存在循环引用的问题了。


image

MRC

使用 __unsafe_unretained 解决

同 ARC 环境的方式一样。

MRC 下不支持 __weak。

使用 __block 解决

在 MRC 环境下使用 __block 修饰的话在底层是不会对外部变量进行 retain 也就是强引用操作的,而 ARC 会。
并且不需要调用 weakSelf = nil 就可以解决循环引用的问题。

image

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容