Block本质是什么
Block的本质从两方面体现:
- Block本质也是一个OC对象,因为它的内部也有
isa
指针;- Block是封装了函数调用以及函数调用环境的OC对象。
本质一:Block也是一个OC对象
简单的命令行程序:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^alBlock)(void) = ^{
printf("I'm a Block!");
};
alBlock();
}
return 0;
}
在终端cd
到main.m
所在的文件夹,执行clang -rewrite-objc main.m
即可得到编译后的main.cpp
。去除一些类型转换,并把代码结构微调:
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就存储着isa指针,表明Block所属的类信息
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("I'm a Block!");
}
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[]) {
/// 调用结构体的构造函数初始化alBlock
void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
(alBlock->FuncPtr)(alBlock);
return 0;
}
alBlock指向__main_block_impl_0
类型的结构体实例的首地址,其中(alBlock->impl).isa
就表明了alBlock所属的类型。
本质二:封装了函数调用以及函数调用环境
1.封装了函数调用
(alBlock->impl).FuncPtr
就是一个函数指针,它指向Block代码块的首地址,在上面的例子中输出I'm a Block!的代码块儿就对应于__main_block_func_0
这个静态函数。执行alBlock时通过(alBlock->impl).FuncPtr
调用对应的函数即可。
2.封装了函数调用环境
有了函数要执行的操作,剩下的就是操作要是使用的数据了。函数调用环境(上下文)通常指Block操作的数据,这里的数据包含:1. Block的入参
、2. Block捕获的外部变量
。
其中Block的入参很好理解就是函数的参数,比如:
void (^sayHelloTo)(NSString *) = ^(NSString *someone) {
NSLog(@"Hello %@", someone);
};
其中someone就是Block的形参,调用时sayHelloTo(@"World!")
中的@"World!"
就是入参。
而Block捕获外部变量的方式分为值捕获和引用捕获。
Block的值捕获特性
修改命令行程序:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger autoIns = 14;
void (^alBlock)(void) = ^{
printf("%d", (int)autoIns);
};
autoIns += 2;
alBlock();/// 控制台输出14并不是16
}
return 0;
}
再次执行clang -rewrite-objc main.m
,得到:
/// Block被定义成_NSConcreteStackBlock类的对象
struct __main_block_impl_0 {
struct __block_impl impl; /// Block的部分信息--> isa:所属类型、FuncPtr:函数指针、Flags:标志位...
struct __main_block_desc_0* Desc; /// Block其它部分的(描述)信息
NSInteger autoIns; /// Block捕获的值(定义Block时外部变量autoIns的瞬时值)
/// 构造函数,此次是使用入参NSInteger类型的_autoIns直接初始化结构体成员变量autoIns
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _autoIns, int flags=0) : autoIns(_autoIns)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/// Block对应的C函数,默认的一个参数为Block本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
NSInteger autoIns = __cself->autoIns; // bound by copy ///Block捕获的值
printf("%d", (int)autoIns); /// 对捕获的值执行的操作
}
/// Block其它部分的(描述)信息
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; /// block大小
}
static struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
NSInteger autoIns = 14;
void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, autoIns);/// 传递值
autoIns += 2;
(alBlock->FuncPtr)(alBlock);
return 0;
}
在内存中大致是这样的:
初始化alBlock
时autoIns
是值传递,在alBlock内部一直都是14并和外部的autoIns
脱离了联系,这就是无论在调用alBlock
之前如何改变autoIns
的值,控制台始终输出14的原因。
Block的引用捕获特性
再次修改命令行程序:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger autoIns = 14;
void (^alBlock)(void) = ^{
auto += 4;
printf("%d", (int)autoIns);
};
autoIns += 2;
alBlock();/// 控制台输出20
}
return 0;
}
执行clang -rewrite-objc main.m
,得到:
/**
* NSInteger -> __Block_byref_autoIns_0
* __block修饰的变量将被重新定义为一个结构体变量,这是结构体的定义
*/
struct __Block_byref_autoIns_0 {
void *__isa; /// 初始为0
__Block_byref_autoIns_0 *__forwarding; ///指向同类型结构体的指针
int __flags;
int __size; /// 结构体的大小
NSInteger autoIns;/// 被重新定义为一个结构体之前的值
};
/**
* Block被定义成_NSConcreteStackBlock类的对象
*/
struct __main_block_impl_0 {
struct __block_impl impl; /// Block的部分信息--> impl.isa:所属的类型、impl.FuncPtr:函数指针、impl.Flags:标志位...
struct __main_block_desc_0* Desc; /// Block其它部分的(描述)信息--> Desc->Block_size:Block的大小、Desc->copy:Block的辅助函数1、Desc->dispose:Block的辅助函数2
__Block_byref_autoIns_0 *autoIns; // by ref /// Block捕获的值的指针
/// 结构体构造函数,参数--> fp:Block对应的函数、desc:描述信息、_autoIns:指针指向__Block_byref_autoIns_0结构体、flags:默认等于0。
/// autoIns(_autoIns->__forwarding)是成员变量初始化列表,之所以叫列表是因为可以有很多项,以逗号分隔。此处使用入参_autoIns的__forwarding成员初始化自身的autoIns。
/// 成员变量初始化列表多用于初始化常量成员,因为常量不能赋值只能初始化
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_autoIns_0 *_autoIns, int flags=0) : autoIns(_autoIns->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/**
* Block的部分信息对应结构体,此部分定义在main.cpp 62左右,其余的末尾。
*/
struct __block_impl {
void *isa; /// isa指针表明Block身份
int Flags;
int Reserved;
void *FuncPtr; /// 使用Block时调用的函数的指针
};
/**
* Block对应的C函数,入参为Block本身
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_autoIns_0 *autoIns = __cself->autoIns; // bound by ref /// 要操作的值,从入参Block中取
(autoIns->__forwarding->autoIns) += 4; /// 执行操作
printf("%d", (int)autoIns); /// 执行操作
}
/**
* Blcok从栈上拷贝到堆上时调用该方法将捕获的外部变量也拷贝到堆上
*/
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->autoIns, (void*)src->autoIns, 8/*BLOCK_FIELD_IS_BYREF*/);
}
/**
* 释放Block时调用该方法释放结构体捕获的值所占用的空间
*/
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->autoIns, 8/*BLOCK_FIELD_IS_BYREF*/);
}
/**
* Block其它部分的(描述)信息
*/
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; /// Block的大小
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
};
/// __main_block_desc_0_DATA 初始化结构体变量的时候用到。
/// 此处源码是和结构体的定义连着写的,这里把定义和初始化一个静态变量分开:
static struct __main_block_desc_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[]) {
/// 初始化一个结构体变量autoIns用于包装之前的整型变量autoIns
__Block_byref_autoIns_0 autoIns = {
(void*)0,
(__Block_byref_autoIns_0 *)&autoIns,
0,
sizeof(__Block_byref_autoIns_0),
14
};
/// 调用结构体的构造函数初始化一个Block结构体变量alBlock
void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &autoIns, 570425344);
/// 通过类型转换拿到FuncPtr并调用,完整的写法应该是(alBlock->impl).FuncPtr,因为存放函数指针的impl成员变量在结构体的起始位置,所以可以直接转换
(((__block_impl *)alBlock)->FuncPtr)(alBlock);
/// 以后即使在block外部也使用包装后的结构体变量autoIns
(autoIns.__forwarding->autoIns) += 2;
printf("%d", (int)(autoIns.__forwarding->autoIns));
}
先上图:
定义Block的结构体
__main_block_impl_0
大致由3部分组成:
- 红色部分:
__block_impl
Block通用部分的信息:*isa``*FuncPtr
... - 橙色部分:
__main_block_desc_0
描述Block的其它信息:Block_size``copy``dispose
... - 浅黄色部分:
__Block_byref_autoIns_0
Block捕获的变量。
上一节初始化alBlock
时传递的是autoIns
的值,这次则是autoIns
的地址(当然,此时autoIns
已封装成一个结构体变量)。若在alBlock
外对autoIns
进行修改,alBlock
内部通过指针访问到的也是修改过的值,而且也可以通过指针实现赋值操作。这就像C语言中swap
函数的形式参数是指针一样:swap(int *a, int *b)
。
Block捕获不同种类变量的方式
作用域 | static/auto | 捕获方法 |
---|---|---|
局部变量 | {auto int i;} | 捕获值,加__block 的话先封装成对象然后捕获其引用 |
局部静态变量 | { static int i;} | 捕获引用 |
全局变量 | int i | 无需捕获直接访问 |
全局静态变量 | static int i | 无需捕获直接访问 |
Blcok的拷贝
上两节中的Block的isa指针都指向了_NSConcreteStackBlock
:impl.isa = &_NSConcreteStackBlock;
但当在main函数中打断点可知__isa = (Class) __NSMallocBlock__
:
这是开启ARC的缘故,运行时ARC自动为我们做
[alBlock copy]
的操作,将栈上的Block拷贝到了堆上。MAC下是这样的:Block拷贝时函数的调用链如下:
(1)
[alBlock copy]
(2)
_Block_copy(alBlock)
(3)
_Block_copy_internal(alBlock, 570425344)
在
_Block_copy_internal
中,针对不同类型的Block它的操作也不尽相同,源码如下:
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
/// mallocBlock增加引用计数后返回
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
/// GlobalBlock直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
/// StackBlock复制到堆上
// 1
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 2
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 3
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 4
result->isa = _NSConcreteMallocBlock;
// 5
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
[stackBlock copy]
做的事情大致会做如下操作:
- 在堆heap上申请同大小的内存空间;
- 将栈数据复制过去;
- 修改新Block的引用计数和标志位,
BLOCK_NEEDS_FREE
表明Block需要释放,在release以及再次拷贝时会用到; - 修改isa的指向;
- 若Block中有copy函数,那么就调用copy函数来拷贝Block捕获的外部变量。
[mallocBlock copy]
如果Block的flags中有BLOCK_NEEDS_FREE
标志,则表明这个Block已经在堆上了(从栈中拷贝到堆时添加的标志),就执行latching_incr_int
操作,其功能就是让Block的引用计数加1。所以堆中Block的拷贝只是单纯地改变了引用计数然后返回它。
[globalBlock copy]
对于全局Block,函数没有做任何操作,直接返回了传入的Block。
ARC下Blcok会被拷贝的场景
- 当Block调用copy方法时,如果Block在栈上,会被拷贝到堆上;
- 当Block作为函数返回值返回时,编译器自动将Block作为
_Block_copy
函数,效果等同于Block直接调用copy方法; - 当Block被赋值给
__strong id
类型的对象或Block的成员变量时,编译器自动将Block作为_Block_copy
函数,效果等同于Block直接调用copy方法; - 当Block作为参数被传入方法名带有usingBlock的Cocoa Framework方法或GCD的API时。这些方法会在内部对传递进来的Block调用copy或
_Block_copy
进行拷贝.
Blcok拷贝到堆上对捕获的外部变量的影响
- 若Block捕获的是值,则没有影响;(内外的变量已脱离联系)
- 若Block捕获的是引用,则引用的结构体(对外部变量的封装)也会被拷贝到堆上。
autoIns的地址由栈上的高地址0x7ffeefbff500变成了堆上的低地址0x10056af78。
被捕获的“对象”的拷贝过程的调用链如下:
(1)
_Block_copy(alBlock)
(2)
_Block_copy_internal(alBlock, 570425344)
(3)
__main_block_copy_0(copiedBlock, alBlock)
(4)
_Block_object_assign(&(copiedBlock->autoIns), aBlock->autoIns, 8)
(5)
_Block_byref_assign_copy(&(copiedBlock->autoIns), aBlock->autoIns, 8)
最后一步_Block_byref_assign_copy()
函数的源码如下:
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
/// Block_byref和__Block_byref_autoIns_0的前4个成员的类型都是一样的,内存空间排列一致。多的向少的转换
/// 堆中Block的autoIns指针的指针,因为要改变autoIns指针的指向所以要使用二级指针
struct Block_byref **destp = (struct Block_byref **)dest;
/// 源数据,栈中Block的autoIns指针,指向栈中被捕获的对象
struct Block_byref *src = (struct Block_byref *)arg;
//printf("_Block_byref_assign_copy called, byref destp %p, src %p, flags %x\n", destp, src, flags);
//printf("src dump: %s\n", _Block_byref_dump(src));
if (src->forwarding->flags & BLOCK_IS_GC) {
; // don't need to do any more work
}
else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
//printf("making copy\n");
// src points to stack
bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
// if its weak ask for an object (only matters under GC)
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
/// 堆中拷贝的forwarding指向它自己
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
/// 栈中的forwarding指向堆中的拷贝
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (isWeak) {
copy->isa = &_NSConcreteWeakBlockVariable; // mark isa field so it gets weak scanning
}
/// 如果被捕获的对象也定义有copy和dispose函数则调用,
/// 注意和_Block_copy_internal中类似的判断做区分,
/// _Block_copy_internal:result->flags中的result指Block本身
/// 此处src->flags中的src指上述Block捕获的对象
if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
copy->byref_keep = src->byref_keep;
copy->byref_destroy = src->byref_destroy;
(*src->byref_keep)(copy, src);
}
else {
// just bits. Blast 'em using _Block_memmove in case they're __strong
_Block_memmove(
(void *)©->byref_keep,
(void *)&src->byref_keep,
src->size - sizeof(struct Block_byref_header));
}
}
// already copied to heap /// 如果src->forwarding已经指向堆区那么增加堆拷贝对象的引用计数。
else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// assign byref data block pointer into new Block
/// 此时src->forwarding指向堆区,把堆区对象的首地址复制给destp,也即修改堆中Block的autoIns指针的指向,使其指向堆中被捕获的对象。
/// *destp = src->forwarding
_Block_assign(src->forwarding, (void **)destp);
}
这个函数做的事情可以归纳为:
- 使用
_Block_allocator
在堆上创建新对象; - 对新对象前四个成员
isa
、flags
、forwarding
、size
赋值;对源对象的forwarding
成员重新赋值,指向新对象; - 根据情况调用
_Block_memmove()
或(*src->byref_keep)()
出处理剩余的成员; -
给堆上Block的autoIns指针重新赋值,指向新对象。
总结
示例代码中Block捕获的都是基本数据类型。
-
值捕获
说明Block内仅使用它的值不会做修改,那么Block被定义是就在内部存储一个一摸一样的值,这个值和外部的值完全脱离的联系;即使Block被拷贝的堆上也不会对之前外部的值有任何影响。 -
引用捕获
说明Block内部试图修改它的值,而外部也要能修改它的值,那么这个值就会被封装成一个结构体,Block内部通过结构体的地址访问它,外部直接使用这个结构体;Block被拷贝到堆上时,结构体也会被拷贝到堆上,此时autoIns.forward->autoIns
就是堆空间的那个整型值了。
其它
封装成结构体只是达到Block内外访问一致的一种方法,也可以通过直接在Block内存一份变量的地址的方式达到这种效果,就像局部静态变量那样:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger autoIns = 14;
NSInteger *address_autoIns = &autoIns;
void(^alBlock)(void) = ^{
NSInteger *pInt = address_autoIns;
*pInt += 4;
printf("%ld", *pInt);
};
autoIns += 2;
alBlock();
}
return 0;
}
同样输出是20,但这种方式无法处理Block的被拷贝到堆上的情况,Block的生命周期长于autoIns,当autoIns在栈上被销毁时再通过地址访问它就会出现异常。局部静态变量一经创建就一直存在所以不会有这个问题,和全局静态变量的区别就是作用域有区别,在作用域外只能通过地址访问,而全局静态变量可以直接访问。