Block真的难,笔者静下心来读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,读的时候顺便记录下来本身的心得,方便之后再翻回,也但愿能带给你们一些帮助。html
本文将以一个菜dog的角度,从 Block 不截获变量、截获变量不修改、截获并修改变量 、 截获对象 四个层次 浅浅探究Block的实现。编程
Block的语法就不回顾了,很差记Block语法能够翻这篇How Do I Declare A Block in Objective-C?。数组
转成C++ 的源代码学习,笔者加了适当的注释方便理解。bash
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
retrun 0;
}
复制代码
将转为多线程
// block中通用的成员变量 结构体
// 文章后面的代码再也不给出,但都有用到
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 表明Block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成员变量
struct __main_block_desc_0* Desc;// block 的大小
// 构造函数
__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;
}
};
// 本来的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("Block\n");
}
// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
// 声明定义block
// 用到了构造函数方法
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
/*
至关于如下
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
栈上生成的结构体实例的指针,赋值给变量blk。
*/
// 调用block
// 第一个参数为 blk_>FuncPtr,即C函数
// 第二个参数为 blk自己
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/*
至关于如下
普通的C函数调用
(*blk->impl.FuncPtr)(blk);
*/
return 0;
}
复制代码
即把本来的代码块,转到一个C函数中。而且建立一个 表明Block 的结构体,最后一个构造函数,Block对象把函数和成员绑定起来。框架
和以上区别在于,Block结构体中的成员变量多了截获的自动变量,而且构造函数参数也是。less
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed.val = %d\n";
blk();
return 0;
}
复制代码
将转为函数
// 跟上面同样
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 表明Block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成员变量
struct __main_block_desc_0* Desc;// block 的大小
// 截获的自动变量
// 结构体中有名字同样的成员变量
const char *fmt;
int val;
// 构造函数
// 参数多了截获的自动变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本来的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
/*
结构体初始化以下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d\n";
val = 10;
*/
return 0;
}
复制代码
根据以上,咱们知道截获变量后,实质上是Block结构体中有一个成员变量存了起来。调用Block时,是访问取结构体成员变量,而不是外面的局部变量。学习
Block不容许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了做用域。在几个做用域之间进行切换时,若是不加上这样的限制,变量的可维护性将大大下降。又好比我想在block内声明了一个与外部同名的变量,此时是容许呢仍是不容许呢?只有加上了这样的限制,这样的情景才能实现。因而栈区变成了红灯区,堆区变成了绿灯区。ui
iOS Block不能修改外部变量的值,指的是栈中指针的内存地址。下面举几个例子理解。
int val = 0;
void (^blk)(void) = ^{
val = 1;
};
复制代码
如下没问题
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
复制代码
如下编译报错
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
复制代码
如下编译错误
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
复制代码
需改为指针
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
复制代码
那么Block 要怎么修改变量呢?
C 中有一个变量,容许Block改写值。
例子
int global_val = 1;// 全局变量
static int static_global_val = 2;// 静态全局变量
int main()
{
static int static_val = 3;// 静态变量
void (^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
}
return 0;
}
复制代码
转换后
int global_val = 1;
static int static_global_val = 2;
// 表明Block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成员变量
struct __main_block_desc_0* Desc;
// 成员变量只多了静态变量,缘由在后面分析
int *static_val;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackblock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本来的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static__val = __cself->static_val;
global_val *= 1;
static_global_val *= 2;
(*static_val) *=3;
}
// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
static int static_val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
return 0;
}
复制代码
为何成员变量只多了静态变量呢?
这就要先了解 iOS 内存区域。 iOS-MRC与ARC区别以及五大内存区
全局区又分为 BSS段 和 数据段(data)。
BSS段:BSS段(bss segment)一般是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)一般是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
但不一样的是C++中,不区分有没有初始化,都放到一块去。
再回到刚刚的代码上,为何block结构体中成员变量只多了静态变量呢?
int global_val = 1;// 全局变量
static int static_global_val = 2;// 静态全局变量
static int static_val = 3;// 静态变量
复制代码
关于它们的区别——全局变量/静态全局变量/局部变量/静态局部变量的异同点
静态局部变量虽然程序运行时一直存在,但只对定义本身的函数体始终可见。
编译后,调用block实质上是在 一个新定义的函数 中访问静态局部变量,不能直接访问,因此须要保存其指针。而全局变量能够访问到,因此没有加到成员变量中。
int main()
{
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
return 0;
}
复制代码
转换后
// 变量将会变成的结构体
// 即val不是int类型,变成此结构体实例
struct __Block_byref_val_0 {
void *__isa;// __block变量转化后所属的类对象
__Block_byref_val_0 *__forwarding;//指向__block变量自身的指针,后面解释
int __flags;// 版本号
int __size;// 结构体大小
int val;// 本来的int数值
};
// 表明Block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成员变量
struct __main_block_desc_0* Desc;// block 的大小
__Block_byref_val_0 *val;// val转成成员变量,类型为结构体
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->_forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本来的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_val_0 *val = __cself->val;
// 这里经过__forwarding赋值?后面解释
(val->__forwarding->val) = 1;
}
// 当Block从栈复制到堆时
// 经过此函数把截获的__block变量移到堆或者引用数+1
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
// 当Block从堆被废弃时
// 经过此函数把截获的__block变量引用数-1
// 至关于对象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
// 计算block大小的结构体
// 该结构体有两个函数
// copy 和 dispose
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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()
{
/*
val变成了__Block_byref_val_0结构体实例
*/
__Block_byref_val_0 val = {
0,// isa指针
&val,//forwarding成员,指向本身
0,// 版本号
sizeof(__Block_byref_val_0),
10 //原来int val的值
};
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
复制代码
从main函数中,咱们能够发现,Block转换成Block的结构体类型**__main_block_impl_0的自动变量,__block变量val转换为block变量的结构体类型__Block_byref_val_0**的自动变量。它们都在栈上。因此Block的isa指针指向NSConcreteStackBlock。
除了NSConcreteStackBlock,还有两种类型 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。
类 | 设置对象的存储域 |
---|---|
NSConcreteStackBlock | 栈 |
NSConcreteGlobalBlock | 全局区 |
NSConcreteMallocBlock | 堆 |
void (^blk)(void) = ^{printf("Global Block\n");};
int main
{
return 0;
}
复制代码
另外只要没有截获自动变量,Block类型就是NSConcreteGlobalBlock。
先来理解为何有个forwarding指向本身。
试想,Block若是截获了自动变量,而后移到堆上,在别的做用域调用(很常见)。若是__block变量在栈上已经释放了,Block访问__block变量会失败。因此系统须要在Block变成NSConcreteMallocBlock时,截获的__block变量也复制到堆上。
Block何时会复制到堆上呢?
__block变量的配置存储域 | Block从栈赋值到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
当Block从栈复制到堆时,__block变量的forwarding 会从新指向其在堆中的内存地址。
这样,不管是在Block语法中、Block语法外使用__block变量,仍是__block变量配置在栈上或对上,均可以顺利地访问同一个__block变量。
笔者在书上刚看到这句话时,有点晕,后来想了一段时间应该是如下意思,若是有误,欢迎大神批斗。
以下代码,有注释
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;// 转换为++(val.__forwarding->val);即(栈上的val).__forwarding->val,最终指向堆上的val
blk();// 转换为++(val.__forwarding->val);即(堆上的val).__forwarding->val,最终指向堆上的val
NSLog(@"%d", val);
复制代码
blk_t blk;
{
id array = [[NSMutablArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
复制代码
还记得上面提到的截获变量不修改,转为C++,Block结构体中的成员变量多了截获的自动变量。
这里,变量做用域结束时,理论上array被废弃,但执行输出结果为数组count123。
这意味着array超出做用域而存在。
会不会也是Block结构体中的成员变量多了截获的自动变量呢?
转换为C++后
struct __main_block_impl_0 {
struct __block_impl impl;// Block通用的成员变量
struct __main_block_desc_0* Desc;// Block的大小
// 指向数组的成员变量
id __strong array;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本来的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
// 当Block从栈复制到堆时
// 经过此函数把截获的对象引用数+1
// 至关于retain
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
// 当Block从堆被废弃时
// 经过此函数把截获的对象release引用数-1
// 至关于对象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
// 计算block大小的结构体
// 该结构体有两个函数
// copy 和 dispose
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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
};
复制代码
调用block转换以下。
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
// 构造函数
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
// 调用,第一个参数为blk自己,第二个参数为id类型对象
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
复制代码
能够看到,和猜想同样,Block结构体中确实多了一个id __strong array;
咱们知道,咱们写的C语言结构体不能带有__strong修饰符的变量。缘由是编译器不知道什么时候进行C语言结构体的初始化和废弃操做。
可是OC运行时库能把握Block从栈复制到堆以及堆上的Block被废弃的时机,所以Block用结构体中能够管理好。
那么同时用__block 和 __strong 修饰的对象呢?
上面提到过__block int val,val将变为一个结构体,对象也同样。
__block id obj = [[NSObject alloc] init];
// 至关于__block id __strong obj = [[NSObject alloc] init];
复制代码
转换为
// 对象将会变成的结构体
struct __Block_byref_obj_0 {
void *__isa;// __block变量转化后所属的类对象
__Block_byref_val_0 *__forwarding;//指向对象自身的指针,后面解释
int __flags;// 版本号
int __size;// 结构体大小
void (*__Block_byref_id_object_copy)(void*, void*);// retain对象
void (*__Block_byref_id_object_dispose)(void*);// release对象
__strong id obj;//指向对象
};
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_byref_obj_0结构体实例
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
复制代码
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
__block id __weak array2 = array;
blk =[^(id obj) {
[array2 addObject:obj];
NSLog(@"array count = %ld", [array2 count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
复制代码
输出结果为数组数目0。
这是因为array在做用域结束时被释放、废弃,nil被赋值在array2中。
结论:Block中持有weak声明的对象,对象引用数不会增长。
这种Block的类型是NSConcreteGlobalBlock。这种Block把代码块内容转到一个C函数中,Block结构体较简单。
这种Block的结构体中截获的变量会变成成员变量。
截获的对象也会变成成员变量(内存语义相同),Block复制到堆上时会调用__main_block_copy_0
,废弃时调用__main_block_dispose_0
,对捕获的强引用对象引用数形成影响。
而且构造函数、调用的C函数都会用到截获的变量。
Block结构体中没有全局变量和全局静态变量,由于能够直接用。但Block结构体会保存局部静态变量的指针。
val会变成一个结构体__Block_byref_val_0
,其成员变量__forwarding
指向自己。
当Block从栈复制到堆时,会调用__main_block_copy_0
,val会经过_Block_object_assign
引用数+1。
当Block销毁,会调用__main_block_dispose_0
,val会经过_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF)
引用数-1。
对象会变成一个结构体__Block_byref_obj_0
,其成员变量__strong id obj
指向对象,其成员变量__forwarding
指向自己。
若是是强引用对象,Block会经过__Block_byref_id_object_copy_131
,和__Block_byref_id_object_dispose_131
内部引用和释放对象。弱引用不对对象生命周期产生影响。
到底何时才须要在ObjC的Block中使用weakSelf/strongSelf
Block属性中内存语义用copy 仍是strong?
在ARC下,这两种效果都会把Block 从栈上压到堆上。但事实上,copy更接近Block的本质。
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 能够把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 仍是 strong 效果是同样的,但写上 copy 也无伤大雅,还能时刻提醒咱们:编译器自动对 block 进行了 copy 操做。若是不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操做”,他们有可能会在调用以前自行拷贝属性值。这种操做多余而低效。你也许会感受我这种作法有些怪异,不须要写依然写。若是你这样想,实际上是你“日用而不知”。