Block
Block定义以及表达式
在iOS开发中针对于Objective-C我们经常提到Block,对于Swift来说就是闭包,今天主要是探索Block,所以先有个疑问,什么是Block?为什么要用Block?
首先Block是一个OC对象,其内部也是isa指针,Block封装了函数实现以及函数上下文的OC对象
使用Block是为了将函数的调用以及实现合并到一起
Block的声明:返回值(^Block名称)(参数列表){Block回调实现}
Block的调用:Block名称(Block参数)
Block分类
在我们程序运行过程中,Block的使用是非常广泛的,比如数组遍历enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {}
,masonry等很多优秀三方库中也会发现Block的身影,所以我们探索下的使用场景以及在什么场景下Block是什么类型
在我们iOS系统中Block分为6种:
_NSConcreteStackBlock:栈Block
_NSConcreteMallocBlock:堆Block
_NSConcreteGlobalBlock:全局Block
_NSConcreteAutoBlock:在GC环境下,当对象被
__weak,__block
修饰,并且从栈复制到堆时,block会被标记为该模式_NSConcreteFinalizingBlock:在GC环境下,当block被复制时,如果block有ctors&dtors时,则转换为该模式
_NSConcreteWeakBlockVariable:与
_NSConcreteFinalizingBlock
反之则转换为该模式
Block的结构
Block作为一个OC的对象,是一个结构体类型,其结构如下:
struct Block_layout {
void *isa;
volatile int32_t flags;
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
};
知识点:
1、通过上面Block的结构体,就能得出我们之所以说Block也是OC对象的原因,因为Block内部也有isa指针
2、其中用到了一个修饰符
volatile
这个在系统结构体里面很常见,这个修饰符的意思就是告诉编译器,我修饰的属性,不需要你去优化,包括存储的空间也不需要你去优化。而且volatile
修饰符保证了不同线程对这个属性进行操作的时候的可见性,即一个线程修改了变量的值,这个新值对其他线程来说是立即可见的。
3、GlobalBlock:位于全局区,在Block内部不使用外部变量,或者只使用静态变量和全局变量
4、MallocBlock:位于堆区,在Block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy修饰的变量
5、StackBlock:位于栈区,与MallocBlock一样,可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者Copy修饰的变量
Block源码探索
首先下载block源码,然后我们打开项目后,然后创建一个TARGET
,然后打开main.m
文件,在里面申明并调用简单的block,代码如下:
int main() {
void(^block0)(void) = ^{
NSLog(@"==================\n");
};
block0();
}
然后在block申明处添加断点,同时打开汇编模式,运行程序,然后进入断点后,点击step over
然后会发现进入了_Block_copy
函数,然后打开源码,找到_Block_copy
函数的实现,代码如下:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 如果 arg 为 NULL,直接返回 NULL
if (!arg) return NULL;
// The following would be better done as a switch statement
// 强转为 Block_layout 类型
aBlock = (struct Block_layout *)arg;
// 获取Block签名
const char *signature = _Block_descriptor_3(aBlock)->signature;
// 如果现在已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 就只将引用计数加 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
// block 现在在栈上,现在需要将其拷贝到堆上
// 在堆上重新开辟一块和 aBlock 相同大小的内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
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
// 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// copy 方法中会调用做拷贝成员变量的工作
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// isa 指向 _NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
在这个函数中主要进行以下操作:
1、进行强转,变成
Block_layout
结构体对象。2、然后调用
Block_descriptor_3
函数获取签名信息,在这个函数内部首先判断是否有签名信息,用到了Block给构体中flags
属性,这个属性与BLOCK_HAS_SIGNATURE
(1左移30位)进行或运算,然后判断结果是否为0,如果结果为0,那么表示没有签名信息,直接返回null,如果不为0,获取Block的descriptor
指针,然后指针偏移Block_descriptor_1
结构体大小,然后继续判断flags
属性或运算BLOCK_HAS_COPY_DISPOSE
(1左移25位)得到的结果是否为0,如果不为0,指针再次偏移Block_descriptor_2
结构体大小,最后得到Block_descriptor_3
的地址,然后进行强转后返回Block_descriptor_3
对象,这样外部就可以直接访问到Block_descriptor_3
结构体对象的属性。3、然后判断Block是否在堆上,如果在堆上Block的引用计数加1,然后返回Block对象
4、如果Block是全局Block,不做任何处理直接返回Block对象
5、如果Block是栈区Block,那么先在堆上开辟一块和Block大小相同的空间,如果开辟空间失败,直接返回null,如果开辟成功,直接将block的数据全部复制到新开辟的堆上block,重新定义调用指针。然后将堆上block的flags中的BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0,然后标记堆block并且引用计初始化为1,然后调用
_Block_call_copy_helper
方法,复制栈block的属性到堆block上,修改堆block的isa指针为堆block类型。
Block实现以及变量捕获
我们都知道Block会捕获外部变量,那么是怎么捕获的,Block在编译时发生了什么?怎么找到外部变量的?怎么找到Block块的实现的?下面我们通过编译cpp文件去探索一下Block。
1、在Block中不使用任何变量或者常量,然后使用clang -rewrite-objc main.m -o main.cpp
将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:
编译前:
#import
int main() {
void(^block0)(void) = ^{
NSLog(@"==================\n");
};
block0();
}
编译后(删除额外代码后):
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块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_ac9b9b_mi_0);
}
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() {
// Block的申明
void(*block0)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
// Block的调用
(block0->FuncPtr)(block0);
}
先看下main函数,在main函数中,Block的申明以及调用。
Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用
__main_block_impl_0
结构体的初始化方法,传入两个参数,一个是Block块实现函数,一个是Block信息。在
__main_block_func_0
函数中很简单就是打印日志。在
__main_block_impl_0
结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock
栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。
2、在Block中使用外部局部变量,然后使用clang -rewrite-objc main.m -o main.cpp
将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:
编译前:
#import
int main() {
int a = 10;
void(^block0)(void) = ^{
NSLog(@"==================%d\n",a);
};
block0();
}
编译后(删除额外代码后):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_5f4e61_mi_0,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 a = 10;
void(*block0)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
(block0->FuncPtr)(block0);
}
先看下main函数,在main函数中,Block的申明以及调用。
Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用
__main_block_impl_0
结构体的初始化方法,传入三个参数,一个是Block块实现函数,一个是Block信息,一个是局部变量a
传入了局部变量a的值。在
__main_block_impl_0
结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock
栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息,最重要的是__main_block_impl_0
中定义了一个属性a,在析构函数中将对外部变量的值赋值给了a。在
__main_block_func_0
函数中定义了一个临时变量a然后使用__cself->a
进行赋值,然后进行打印a的值,而且这块定义的a是一个全新的局部变量,不是外部定义的局部变量a,这也就是为什么在Block申明之后,Block调用之前修改局部变量的值,不影响Block块内部输出的值的原因,因为Block在捕获局部变量的时候传递了一个值,所以当编译器编译时就已经捕获了局部变量的值,并且赋值给了内部属性a,然后在Block实现中再次赋值给了Block块内部的局部变量a,所以外部不管怎么修改,都不影响Block内部捕获的值。
3、在Block中使用外部全局变量,然后使用clang -rewrite-objc main.m -o main.cpp
将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:
编译前:
#import
int a = 10;
int main() {
void(^block0)(void) = ^{
NSLog(@"==================%d\n",a);
};
block0();
}
编译后(删除额外代码后):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
int a = 10;
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_aac8d9_mi_0,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() {
void(*block0)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
(block0->FuncPtr)(block0);
}
先看下main函数,在main函数中,Block的申明以及调用。
Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用
__main_block_impl_0
结构体的初始化方法,传入两个参数,一个是Block块实现函数,一个是Block信息。在
__main_block_impl_0
结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock
栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。在
__main_block_func_0
函数中直接使用了全局变量,并不是捕获值,因为在__main_block_impl_0
结构体中并没有申明属性,也没有对全局变量进行操作,而是直接使用全局变量,所以这就是为申明全局变量在Block申明后修改会影响Block内部的原因。
4、在Block中使用外部进过__block
修饰的局部变量,然后使用clang -rewrite-objc main.m -o main.cpp
将main.m文件编译成main.cpp文件。下面先看下编译前以及编译后的代码:
编译前:
#import
int main() {
__block int a = 10;
void(^block0)(void) = ^{
NSLog(@"==================%d\n",a);
};
block0();
}
编译后(删除额外代码后):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_kl73wv292blcg8h8zbxlhlfh0000gn_T_main_379bf1_mi_0,(a->__forwarding->a));
}
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() {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*block0)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
(block0->FuncPtr)(block0);
}
先看下main函数,在main函数中,Block的申明以及调用。
__block int a = 10;
经过编译后变成了__Block_byref_a_0
结构体对象,通过结构体的析构函数将a的地址
以及值都进行捕获。
Block前半部分定义和编译前的代码是一模一样的,然后后半部分就是调用
__main_block_impl_0
结构体的初始化方法,传入四个参数,一个是Block块实现函数,一个是Block信息,一个是__Block_byref_a_0
对象的地址,一个是flags。在
__main_block_impl_0
结构体中的初始化方法中对结构体进行了赋值,isa指向_NSConcreteStackBlock
栈Block,flags标志数据是对Block的信息存储,里面有Block的释放标志、引用计数、是够拥有拷贝函数、是否拥有析构函数、是否有垃圾回收、是否全局Block、是否有签名、是否扩展等信息。desc存储了Block的大小等信息。在
__main_block_desc_0_DATA
中对捕获的值进行了拷贝,而且是地址拷贝,也就是让a的引用计数+1防止提前释放在
__main_block_func_0
函数中通过定义__Block_byref_a_0
结构体对象然后获取值,然后访问结构体对象中局部变量a的值进行操作。
堆Block
虽然通过上面的源码编译查看后Block的isa都是指向栈Block,但是在程序运行中,我们通过断点调试查看的时候,发现Block只要没有被copy或者捕获使用__block
修饰的变量,都是_NSConcreteGlobalBlock
全局Block,copy出来的Block以及捕获了使用__block
修饰的变量时,就变成了_NSConcreteMallocBlock
堆Block。具体如下图所示: