手动目录
- 循环引用
- block的类
- Block的相关信息
block本质
block如何捕获外界变量?
__block修饰的本质
block 捕获外部变量的补充- block 堆/栈 转移过程
汇编分析
源码分析- block 签名
- block的三层拷贝
_Block_copy (第一层拷贝)
block_assign (第二层拷贝)
__Block_byref_id_object_copy_131 (第三层拷贝)
_main_block_dispose_0 释放- Block总结
- GCD的Block 是否需要weak?
循环引用
一般来说 对于 不会自动release的block 为了避免循环引用,一般采用中介者模式
。
比如 __weak typeof(self) weakSelf = self
中介者模式原理:
用weak之前:
self -> block -> self
用weak之后
weakSelf -> self -> block -> weakSelf
但是weakSelf 由弱引用表来维护,不会进行计数器加减。在dealloc的时候,weakSelf被释放, 就打破了循环引用
所有的block 都可以用weak来修饰吗? 答案是否定的:可能会造成 self被提前释放
。
@property (nonatomic, strong) NSString *name;
- (void)viewDidLoad {
_name = @"asdasd";
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 这一行加不加 都会造成self被提前释放
NSLog(@"%@",strongSelf.name);
});
}
- (void)dealloc {
NSLog(@"dealloc");
}
操作:
跳转到这个界面之后,2s内进行返回。
返回的时候会进行正常的dealloc 打印,在2s后 打印 NSLog(@"%@",weakSelf.name); 的时候就出现了问题: 打印(null)。
这是为什么呢:
weakSelf是弱引用,因为dispatch_after 是到时才将block加入队列, 在block执行之前,并没有进行strongSelf对其进行持有,所有weakSelf会在dealloc的时候被释放, weakSelf、self、_name 都会被释放,所以打印就会出现null。
所以:weak并不是什么地方都可以使用的
。
在看另外一个例子:
typedef void(^HandleBlock)(void);
@property (nonatomic, copy) HandleBlock block;
// self -> block -> self (这个self 虽然没有被after copy,但是被 self.block copy)
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",self.name);
});
};
self.block();
// strong -> weakSelf -> self -> block -> strong (strong 是临时变量,block任务执行完毕,strong就被释放)
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
使用strong-weak-dance
解决引用对象可能被提前释放的问题。
block的类
block 有几种? - 6 种
在源码 libclosure-74 中列举出6种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 };
其中 上层常用的3种:
__NSGlobalBlock__
(全局)、__NSMallocBlock__
(堆)、__NSStackBlock__
(栈)
另外三种一般是系统级别去使用。
我们用代码来打印常用的三种:
种类一:全局block(block内部不捕获外部变量) <__NSGlobalBlock__: 0x10696c450>
void(^ block)(void) = ^{
NSLog@(@"a");
};
NSLog(@"block = %@",block);
id globalBlock = ^(NSString *name,NSInteger a){ //这个也是全局 因为他没有捕获外部变量
NSLog(@"globalBlock : name = %@",name);
};
种类二:堆block (block内部捕获临时变量) <__NSMallocBlock__: 0x600001dba220>
int a = 0;
void(^ block)(void) = ^{
NSLog(@"a = %@",@(a));
};
NSLog(@"block = %@",block);
种类三:栈block (捕获外部变量,并在copy之前) <__NSStackBlock__: 0x7ffeed693978>
int a = 0;
NSLog(@"block = %@",^{
NSLog(@"%@",@(a));
});
这里有需要注意的点: 上面的int a = 0; a是临时变量
1、对于种类二:如果a 是全局变量、全局静态变量、局部静态变量,二的类型是 __NSGlobalBlock__
。
2、在捕获临时变量a的时候,本身是栈block,
因为 临时变量在栈上,超过作用域 会被销毁,为了保证数据安全,系统是 将其自动进行copy操作, 会将其拷贝到堆上。这个时候,这个block变成了mallocBlock。
简单点说:
block默认是全局变量,但是在捕获了栈上 的变量(临时变量),那么它是栈block,但是栈block为了数据安全,会自动进行copy操作,将其变成堆block。
Block的相关信息
block本质
用代码进行clang (.m 文件也可以,但是clang出来的内容太长,我们用.c文件 操作)
// 创建一个 C File 文件 BlockTest.c
#include <stdio.h>
int main() {
void(^block)(void) = ^{
printf("a");
};
block();
}
使用clang命令:clang -rewrite-objc BlockTest.c
得到以下信息
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);
}
// 简化我们需要的信息
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); // 构造函数
((void (*)))(block)->FuncPtr)((__block_impl *)block); // 调用函数
// 在编译后的文件中可以找到由以下信息
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;
}
};
所以block 的本质 可以说是对象,深一层 可以说是结构体
(上面已经打印了block的类名)
block如何捕获外界变量?
还是用代码进行clang
int main() {
int a = 10
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
}
// 查看相关信息
int main() {
int a = 10;
//与上面相比 __main_block_impl_0 多了一个参数 a
void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
block)->FuncPtr)((__block_impl *)block);
}
// 结构体中, 多了一个参数a
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;
}
};
// 在执行代码中,将a进行了copy操作
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy // ------ 在这里 前面的a 和后面的a 地址不同。
printf("a = %d",a);
}
在这里考虑一个问题: a没有进行__block 修饰的情况下,进行a ++ 会是什么结果?
假设能编译通过的情况下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
a ++; // 在这里进行a++
printf("a = %d",a);
}
那么a++ 是对 int a 中的a 进行操作。和外部的a 完全没有任何关系。a++中的a 是内部的a ,在外界没有被定义(不能访问内部的a),所以是不允许的。
结论:
如何捕获变量
在block的结构体中,生成了一个新的成员变量去保存外部变量。在没有__block修饰的情况下,为何不能进行修改外部变量?
如果在block中进行修改属性,实质上是对内部成员变量的修改,但是在外部,是无法当问结构体中生成的变量。所以无法修改。
__block修饰的本质
还是用代码进行clang
int main() {
__block int a = 10;
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
}
// 简化之后
int main() {
// 结构体的初始化
__Block_byref_a_0 a = {(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
10};
// 注意第三个参数传递的是一个指针地址。
void(*block)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
block)->FuncPtr)((__block_impl *)block);
}
// 生成了一个 关于 a 的 __Block_byref_a_0 的结构体
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
// __block修饰后 block的结构体
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("a = %d",(a->__forwarding->a));
}
结论:
--block 是生成了一个 block_byref_ 的结构体 用来保存原有变量的指针 和 值
传递给block的是一个指针。
block 捕获外部变量的补充
这部分内容来源于另一片文章
其中有一部分内容讲解了 自动变量/静态变量/静态全局变量/全局变量 多种变量在block中捕获的情况
说明:
自动变量 --- 临时变量(方法内部定义的变量)
静态变量 --- 方法内部定义的static 变量
静态全局变量 --- 方法外部定义的static变量
全局变量 ---- 类的成员变量
int a = 10;
static int b = 20;
int main(int argc, char * argv[]) {
static int c = 30;
int d = 40;
__block int e = 50;
void(^block)(void) = ^{
a++; b++; c++; e++;
// d++; 内部无法操作
printf("\na = %d b = %d c = %d d = %d e = %d",a,b,c,d,e);
};
a++; b++; c++; d++; e++;
printf("a = %d b = %d c = %d d = %d e = %d",a,b,c,d,e);
block();
retrun 0;
}
这里的打印结果
外部 a = 11 b = 21 c = 31 d = 41 e = 51
内部 a = 12 b = 22 c = 32 d = 40 e = 52
因为d是在外部进行++之前就进行了值拷贝,其他的是进行指针访问/直接 访问,所以都有产生了相应的变化。
clang 转换之后的关键信息
// 简化处理
int main() {
static int c = 30;
int d = 40;
__Block_byref_e_0 e = {(void*)0,
(__Block_byref_e_0 *)&e,
0,
sizeof(__Block_byref_e_0),
50};
void(*block)(void) = (&__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
&c,
d,
(__Block_byref_e_0 *)&e,
570425344));
a++; b++; c++; d++; (e.__forwarding->e)++;
printf("a = %d b = %d c = %d d = %d e = %d",a,b,c,d,(e.__forwarding->e));
(block)->FuncPtr)((__block_impl *)block);
}
// block 新结构 🌟新增了 c(int *)、d(int)、e(__Block_byref_ *)🌟
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *c;
int d;
__Block_byref_e_0 *e; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, int _d, __Block_byref_e_0 *_e, int flags=0) : c(_c), d(_d), e(_e->__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_e_0 *e = __cself->e; // bound by ref
int *c = __cself->c; // bound by copy
int d = __cself->d; // bound by copy
a++; b++; (*c)++; (e->__forwarding->e)++;
printf("\na = %d b = %d c = %d d = %d e = %d",a,b,(*c),d,(e->__forwarding->e));
}
从上面的结果中可以知道以下信息
1、全局变量(静态/非静态) 都不参与block内部处理。 而是
直接访问外部变量
block结构体中没有新增a、b的成员变量
static void __main_block_func_0 中也没有对a、b的处理,只有对c、d、e的相应处理2、静态变量(c)、临时变量(d)、__block修饰的临时变量(e) 都会被block 编译进结构体,并
进行间接访问
-- 指针地址访问/值拷贝3、变化类型
静态变量 内部 是处理成相应的指针 c => int *c
临时变量 只是做一个值存储 d => int d
__block修饰的内部变量处理成指针 e => __Block_byref_e_0 *e4、指针地址拷贝/值拷贝
静态变量是指针地址拷贝 ----------------内部 能对外部进行修改 int *c = __cself->c; // bound by copy
__block 是指针地址拷贝 ----------------内部 能对外部进行修改 __Block_byref_e_0 *e = __cself->e; // bound by ref
临时变量是值拷贝 ------------------------内部不能对外部进行修改 int d = __cself->d; // bound by copy
再换一个情况 ,block内部使用指针
NSMutableArray *arr = [NSMutableArray new];
NSString *a = @"a";
void(^block)(void) = ^{
[arr addObject:@"1"];
// arr = [NSMutableArray new]; 这一行不被允许
};
block();
用clang转换后的结构 类似于上面的 静态变量
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *arr; // ⬅️ 重点
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, NSString *_str, int flags=0) : arr(_arr), str(_str) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *arr = __cself->arr; // bound by copy // ⬅️这里也是指针拷贝
......
}
block 捕获变量总结
- 1 、全局变量 不捕获,直接访问
- 2、非全局变量,对捕获的内容进行拷贝
===》捕获的指针,对指针进行拷贝,可以修改指针所致 的内容,不能改变指针指向(---静态变量传递给Block是内存地址值---) ----- 传入可变数组,可以改变数组内容,不能对数组重指向。
===》捕获的值,对值进行拷贝,不能修改这个值 ----- 传入 int a,内部只能使用,不能修改- 3、_block 修饰的变量。生成了一个 block_byref 的结构体 用来保存原有变量的指针 和 值
传递给block的是一个指针。
block 捕获的时机: 并不是在执行block的时候。
以下代码是用来说明 捕获时机的
NSMutableArray *arr = [NSMutableArray new]; // 1
// __block NSMutableArray *arr = [NSMutableArray new]; // 1
void(^block)(void) = ^{
[arr addObject:@"1"]; // 2
};
arr = [NSMutableArray new]; // 3
block();
NSLog(@"arr = %@",arr); // 4
// 无__block修饰的情况
在2位置, arr的地址:2 捕获了 1 的地址 也就是说 外部修改了arr的地址,内部捕获到的地址并没有随之改变
// 有__block修饰的情况
2 先捕获了1 的地址, 执行 3的时候, 2的地址随之改变。 2 其实是对 3的arr进行操作。 4打印的是 3的地址。
block 堆/栈 转移过程
汇编分析
-
无外部变量引用的block
void(^block)(void) = ^{ // ⬅️ 断点在这 }; block();
到了断点之后,下符号断点
_Block_copy
libsystem_blocks.dylib`_Block_copy: -> 0x1867f48c0 <+0>: stp x22, x21, [sp, #-0x30]! // 步骤一: 断点在这行 0x1867f48c4 <+4>: stp x20, x19, [sp, #0x10] ........ 0x1867f49a4 <+228>: ldp x22, x21, [sp], #0x30 0x1867f49a8 <+232>: ret // 步骤二: 断点在这行
步骤一:此时读寄存器x0
register read x0
并po
打印
步骤二:断点在步骤二的位置,在进行一次打印// 步骤一 打印结果 (lldb) register read x0 x0 = 0x00000001002e8080 (lldb) po 0x00000001002e8080 <__NSGlobalBlock__: 0x1002e8080> // 步骤二 打印结果 和步骤一 一致 (lldb) register read x0 x0 = 0x00000001002e8080 (lldb) po 0x00000001002e8080 <__NSGlobalBlock__: 0x1002e8080>
最后结果都是GlobalBLock
-
有外部变量引用的情况
换一个有引用外部变量的block在进行上述操作int a = 10; void(^block)(void) = ^{ NSLog(@"%d",a); }; block();
寄存器打印结果
// 步骤一 (lldb) register read x0 x0 = 0x000000016f75bdc8 (lldb) po 0x000000016f75bdc8 <__NSStackBlock__: 0x16f75bdc8> // 步骤二 (lldb) register read x0 x0 = 0x0000000283a3c630 (lldb) po 0x0000000283a3c630 <__NSMallocBlock__: 0x283a3c630>
打印结果 从 StackBlock 变成了 MallocBlock
源码分析
在源码 libclosure-74 中来找相关信息
先看block定义的结构
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE // 依赖于 BLOCK_HAS_COPY_DISPOSE (有BLOCK_HAS_COPY_DISPOSE 才会有这些 信息)
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE // 依赖于 BLOCK_HAS_SIGNATURE (有BLOCK_HAS_SIGNATURE 才会有这些 信息)
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
基本结构为Block_layout
有没有Block_descriptor_2、Block_descriptor_3,取决于 flags。
看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 是否有效的标志,程序根据他来决定是否增加或减少引用计数的值
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否拥有拷贝辅助函数(a copy helper function),决定Block_descriptor_2
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 是否拥有block C++的析构函数
BLOCK_IS_GC = (1 << 27), // runtime 标志是否有垃圾回收 ---- OS X
BLOCK_IS_GLOBAL = (1 << 28), // compiler 标志是否是全局block
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE 与BLOCK_HAS_SIGNATURE相对,判断当前block是否有签名 用于runtime时动态调用
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否有签名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 是否有扩展 决定Block_descriptor_3
/** block 捕获外部变量的类型
BLOCK_FIELD_IS_OBJECT = 3, 对象
BLOCK_FIELD_IS_BLOCK = 7, 是一个block变量
BLOCK_FIELD_IS_BYREF = 8, __block 修饰的结构体
BLOCK_FIELD_IS_WEAK = 16, __weak 修饰的变量
BLOCK_BYREF_CALLER = 128 处理Block_byref 内部对象内存的时候会加一个往外的标记,配合上面的枚举提起使用
*/
};
在源码中看 block_copy 做了那些事情
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) { // 如果内存需要自己管理 那么引用计数相应增加 最开始是0
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) { // 如果是全局 不做任何操作
return aBlock;
}
else { // 如果是栈block 进行一次拷贝
// 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_copy 中 主要有3个判断 ( 操作的内容 ,后面加了备注 ) ,其中重点在else 里面
做了2件事:
- 1、 malloc 申请内存(堆区)
struct Block_layout *result = (struct Block_layout *)malloc(aBlock->descriptor->size);
- 2、将原来的block 进行memmove (从栈区 移动到堆区) 并将结构的相关信息进行更新
result->flags |= BLOCK_NEEDS_FREE | 2; // 标记为 needs__free 并逻辑计数器为1
result->isa = _NSConcreteMallocBlock; // 从栈block 编程了 堆block
block 签名
在 方法中有签名 =====iOS 底层--Class探索和方法执行过程 ---> 3、方法 中有关于方法签名的相关内容
在block源码中也有签名信息, - 其签名 信息在BLOCK_DESCRIPTOR_3中,依赖于 flags 中的BLOCK_HAS_SIGNATURE。 直接lldb打印比较麻烦,需要计算地址偏移。
在 这篇文章中找到简便方法---- 借助 aspects。
aspects中 签名信息的方法是私有,稍作修改,去掉staic 并在。h中进行申明,这样才可以外部访问。
NSString *getMethodSignatureTypeEncoding(NSMethodSignature *methodSignature){
NSMutableString *str = @"".mutableCopy;
const char *rtvType = methodSignature.methodReturnType;
[str appendString:[NSString stringWithUTF8String:rtvType]];
for (int i = 0; i < methodSignature.numberOfArguments; i ++) {
const char *type = [methodSignature getArgumentTypeAtIndex:i];
[str appendString:[NSString stringWithUTF8String:type]];
}
return [str copy];
}
void task() {
id globalBlock1 = ^(NSString *str ,NSInteger a,NSArray *arr){
NSLog(@"globalBlock1 : name = %@",str);
};
NSMethodSignature *signature1 = aspect_blockMethodSignature(globalBlock1, NULL);
NSLog(@"%@",getMethodSignatureTypeEncoding(signature1));
}
// 打印结果 v@?@"NSString"q@"NSArray"
block 签名信息
v@?@"NSString"q@"NSArray" -- 中间省略了参数占位长度
v - 返回值类型 void - 无
@? - block 的签名
@"NSString" 第一个参数 NSString 类型 -- 表示 是一个NSString 对象
q 第二个参数 NSInteger
@"NSArray" 第三 个参数 NSArray 类型 -- 表示 是一个NSArray 对象
回顾方法签名信息
v 返回值
@ 第一个参数 一个对象
: 表示 方法(SEL)
block和方法签名有点区别
方法签名 一般不指明是什么类型 只表示是一个对象
比如 v@:
block的签名 参数中 不仅指明是一个对象,还指出对象是什么类型
比如 @"NSString"
block的三层拷贝
_Block_copy (第一层拷贝)
这个在上面栈/堆转移过程已经说过了
block_assign (第二层拷贝)
在上面转换的cpp文件中 还有一个方法需要注意
__main_block_copy_0
这里调用到 _Block_object_assign
,我们在源码中看看究竟干了什么。
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:
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*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:
*dest = object;
break;
default:
break;
}
}
根据捕获的对对象类型分别做不同的操作。其中关键的一个 BLOCK_FIELD_IS_BYREF
类型---- __block修饰的类型
_Block_byref_copy(object);
static struct Block_byref *_Block_byref_copy1(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;
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) {
........
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
........
return src->forwarding;
}
关键部分
- 1、在堆区申请一块和原来block_byref 相同大小的空间 (struct Block_byref *)malloc(src->size);
- 2、堆区的指针指向 copy copy->forwarding = copy;
- 3、原来的 block_byref(栈区) 也指向copy src->forwarding = copy;
所以 __block 修饰的变量才有修改的能力。
__Block_byref_id_object_copy_131 (第三层拷贝)
这个拷贝是对__block修饰的对象进行拷贝。拷贝到 Block_byref_2 -> BlockByrefKeepFunction byref_keep
最终是对这个对象进行了 _Block_object_assign 操作。
struct __Block_byref_str_0 {
void *__isa;
__Block_byref_str_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSString *str; // ⬅️
};
也就是对 结构体中的 NSString *str 进行memmove(拷贝操作)。
__main_block_dispose_0 释放
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
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;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
_Block_release_object(object);
break;
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;
}
}
结构很清晰,就是对不同拷贝的类型 进行release操作。
Block总结
- 1、 __block 做了什么
在编译时,对__block修饰的对象转换成一个 block_byref 的结构体,这个结构体里面保存了捕获对象的指针和值- 2、为什么__block 修饰的对象具有修改的能力?
简单点说:__block修饰的对象被从栈区拷贝到堆区。堆区是可以有程序员自己去控制、修改。
具体的过程为: block进行了三次memmove(copy),
1、block 本身的copy,将 block本身拷贝的堆区 _block_copy.
2、捕获的对象的结构体进行memmove(copy),在这个过程中,在堆区申请内存,然后原来栈区的block_byref 指向这块内存,其本身也指向这块内存,都指向同一块内存空间,所以就有了值/地址修改的能力。
3、保存的指针进行memmove(copy),同时是按照步骤二进行操作。目的是使对象可以进行值的修改。
GCD的Block 是否需要weak?
有些需要,有些不需要
- 不需要:
调度组(dispatch_group_async / dispatch_group_notify)
栅栏(dispatch_barrier_async / dispatch_barrier_sync)
dispatch_async
dispatch_sync
dispatch_after
同步和异步的是有区别的:
异步是将任务进行包装,在包装的过程中,进行(copy)、引用(invoke)、释放(call_and_release)。
同步是任务不进行copy,对于调用者, 只是 “borrows”(借用),而不是对调用者进行持有。
- 需要:
dispatch_source_timer(计时器)。
源码分析
-
dispatch_async分析
dispatch_async(dispatch_queue_t dq, dispatch_block_t work) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME; dispatch_qos_t qos; qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags); _dispatch_continuation_async(dq, dc, qos, dc->dc_flags); } ------------------------------ ❌关键在这个方法 _dispatch_continuation_init❌ DISPATCH_ALWAYS_INLINE static inline dispatch_qos_t _dispatch_continuation_init(dispatch_continuation_t dc, dispatch_queue_class_t dqu, dispatch_block_t work, dispatch_block_flags_t flags, uintptr_t dc_flags) { void *ctxt = _dispatch_Block_copy(work); // 对任务进行copy dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED; if (unlikely(_dispatch_block_has_private_data(work))) { dc->dc_flags = dc_flags; dc->dc_ctxt = ctxt; return _dispatch_continuation_init_slow(dc, dqu, flags); } dispatch_function_t func = _dispatch_Block_invoke(work) // 引用这个 任务 if (dc_flags & DC_FLAG_CONSUME) { func = _dispatch_call_block_and_release; // 释放 } return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags); } ------------------------------ void _dispatch_call_block_and_release(void *block) { void (^b)(void) = block; b(); Block_release(b); }
源码分析 async 调用步骤:
1、对block 进行 copy _dispatch_Block_copy(work);
2、对任务进行引用(invoke) _dispatch_Block_invoke(work)
3、释放这个任务 _dispatch_call_block_and_release
4、将block包装成 dispatch_continuation_t释放的条件分析
dc_flags 入参 //DC_FLAG_CONSUME = 0x004
dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
====> 拆解: dc_flags = 0x004 | (0x010 | 0x100); = 1001 0100 (276)
dc_flags & DC_FLAG_CONSUME
====> 拆解: 1001 0100 & 0000 0100 = 0x004
条件成立,会调用 _dispatch_call_block_and_release
-
dispatch_after 分析
同样通过源码static inline void _dispatch_after(dispatch_time_t when, dispatch_queue_t dq, void *ctxt, void *handler, bool block) { .... dispatch_continuation_t dc = _dispatch_continuation_alloc(); if (block) { _dispatch_continuation_init(dc, dq, handler, 0, 0); } else { _dispatch_continuation_init_f(dc, dq, ctxt, handler, 0, 0); } ..... }
源码分析:
又回到了 _dispatch_continuation_init 的调用,还是和上面一样,不过传入的参数 dc_flags 参数是0 ,调用 _dispatch_call_block_and_release 条件不成立
,这只能说明:此时的block是不能被释放的,但是延时执行是在指定时间后,将block添加的相应的线程去执行。所以:dispatch_after 的block 不是当时释放,而是指定时间后再去释放
。(这个地方源码没有分析出来)通过API说明 能找到相应信息:
The block to submit. This function performs a Block_copy and Block_release on behalf of the caller. -
dispatch_sync 分析
static inline void _dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags) { if (likely(dq->dq_width == 1)) { return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags); } .... _dispatch_introspection_sync_begin(dl); _dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))); }
源码全部看下来,没有定义block进行copy的操作。
在官方文档中看到这样一段描述:
Unlike with `dispatch_async`, no retain is performed on the target queue. Because calls to this function are synchronous, it "borrows" the reference of the caller. Moreover, no `Block_copy` is performed on the block. 大概意思就是: 与dispatch_async不同,不在目标队列上执行retain。因为对这个函数的调用是同步的,所以它“借用”了调用者的引用。此外,不在该块上执行块复制。
因为
block 不对调用者进行持有,它只是“借用”,所以不会造成循环引用。
-
dispatch_barrier_async(栅栏) 分析
void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER; dispatch_qos_t qos; qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags); _dispatch_continuation_async(dq, dc, qos, dc_flags); }
dispatch_barrier_async 类似于 dispatch_async , 将block包装起来。
dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
条件成立,会调用 _dispatch_call_block_and_release
- dispatch_group (调度组) 分析
调度组分2个:组里面的每个小任务 和 组任务完成之后的notifyvoid dispatch_group_async_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt, dispatch_function_t func) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC; dispatch_qos_t qos; qos = _dispatch_continuation_init_f(dc, dq, ctxt, func, 0, dc_flags); _dispatch_continuation_group_async(dg, dq, dc, qos); }
void dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq, dispatch_block_t db) { dispatch_continuation_t dsn = _dispatch_continuation_alloc(); _dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME); _dispatch_group_notify(dg, dq, dsn); }
dispatch_group_async 和 dispatch_group_notify 都是回归到 _dispatch_continuation_init
根据传入的参数 dc_flags
最后的结果都是回归调用 _dispatch_call_block_and_release
block 变量捕获 补充 2020.09.19
- (void)test {
void (^block)(void) = ^{
NSLog(@"self = %@",self);
};
block();
}
思考:这个block中的self 是否会被捕获?
答案: 会。 因为在test方法中,默认是有2个参数:(self、_cmd), 所以在这里的self 其实是一个临时变量。(具体编译后的结构 可通过clang命令验证)。
那再看这段代码:
@property (nonatomic, copy) NSString *name; /// 类的一个属性
- (void)test {
void (^block)(void) = ^{
NSLog(@"name = %@",_name);
};
block();
}
在这段代码中, _name 不会被捕获,但是 self会被捕获
。 使用 _name 实际上是 self->_name; 所以self被捕获 了。
网友评论