Block深入理解

Block深入理解

数据结构定义

block_layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:

  1. isa 指针,所有对象都有该指针,block的本质是对象isa指向下面三种block类。
  2. flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
  3. reserved,保留变量。
  4. invoke,函数指针,指向具体的 block 实现的函数调用地址。
  5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
  6. variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

问题

1、为什么block不能修改外部变量的值?即:写操作不对外部变量生效?加上__block就可以生效了?
2、UIView的animation方法block中引用self会导致循环引用吗?self会在block执行完后释放吗?
3、GCD中持有self会导致循环引用吗?

解答问题

将问题分解为以下几部分一一作答

问题研究的是那种类型的block
block内为什么不能修改block外部的变量
最优解及原理
其他几种解法
修改外部变量的必要条件“将auto从栈copy到堆”
将auto变量封装为结构体(对象)

block类型

__NSGlobalBlock__ 没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__调用了copy

每种类型的block调用copy后结果如下

Block的类 原block存储区 调用copy
_NSConcreteGlobalBlock 数据区.data区 什么也不做
_NSConcreteStackBlcok 从栈复制到堆
_NSConcreteMallocBlock 引用计数加一

在ARC环境下以下情况会自动将栈上的block复制到堆上:

block作为函数值返回
将block赋值给__strong指针时
block作为Cocoa API中方法名含有using block的方法参数时
block 作为 GCD API的方法参数时

block为什么不能修改block外部变量

block不允许修改外部变量的值,block本质是一个对象,花括号区域是对象内部的函数,变量进入花括号时,本质是进入了另一个函数区域,改变了作用域。如果不加上这样的限制,当在函数内想声明一个同名变量,是允许还是不允许呢?只有加上这样的限制,才能实现这种场景。
所以Apple在编译器层加了限制,当block内部试图修改自动变量时会直接报错。
block怎样保证能正常访问外部变量
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m查看对应的C++代码

为了保证block内部能访问外部变量,block有一个变量捕获机制,将外部变量复制到了block的结构体中,保存在variables中。

全局变量: 不捕获,可直接访问
局部变量:

  1. auto: 捕获到内部,如果是值类型,值传递,如果是对象,引用计数加1
  2. static: 基本数据类型也是指针传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef void(*CWBlock)(void);


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

(*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 argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

static int a = 1;

CWBlock block = ((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;
}

block实现修改外部变量的最优解

两种内存开销大的方法:

  1. 加static(放在静态存储区/全局初始化区):捕获变量的指针。
  2. 将变量设为全局变量。全局变量是无视作用域的,所以可以在block内修改
    最优解:局部变量加__block关键字
  3. __block 可以解决block内无法修改auto变量值的问题
  4. __block不能修饰全局变量、静态变量
  5. ARC中无论是否添加__block,block中的auto变量都会被从栈上copy到堆上

编译器会将__block修饰的变量包装成一个结构体(对象),在结构体中新建一个同名auto变量,block内部捕获改结构体指针,将结构体copy到堆上,在block中使用自动变量时,使用指针指向的结构体中的自动变量,于是就达到了修改外部变量的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
typedef void(*CWBlock)(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);
}
//编译后的copy方法a的struct会被copy到堆里
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), 1};

CWBlock block = ((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;
}

UIView Animation 会造成循环引用吗

1
2
3
4
5
6
7
8
9
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];

[UIView animateWithDuration:2 delay:3 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.animationView.frame = CGRectMake(200, 200, 200, 200);
} completion:^(BOOL finished) {

}];
}

在 viewWillDisappear 的时候,执行一个延迟 3s 的动画,是否会像 dispatch_after 一样,等待动画执行完成,再释放 self?

实验结果是不会的,self 能正常释放。因为以上代码其实等同于:

1
2
3
4
5
6
7
8
9
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];

[UIView beginAnimations:@"ID" context:NULL];
[UIView setAnimationDuration:2];
[UIView setAnimationDelay:3];
self.animationView.frame = CGRectMake(200, 200, 200, 200);
[UIView commitAnimations];
}

block 的作用就是简化动画提交的代码,而动画实际提交给了 CATransaction,当 delay 的时间到时,再进行执行。所以在这里,block 中的代码只是一个瞬时操作(如果在在 block 中 NSLog,可以发现 NSLog 是立马执行的),而实际 view 的状态,是被保存在了 self.animationView.layer.modelLayer 中。当 view removeFromSuperView 时,animation 就会被移除,[view release],所以不会出现动画执行完后,再释放 view 的问题。

参考文档:

谈Objective-C block的实现
Objective-C Block 分析
技术清谈
从对象持有到 UIView Animation