Block的分类
Block一共有6种类,常见的有三种。
void (^block)(void) = ^{
NSLog(@"1233");
};
上面这种block的打印结果为<__NSGlobalBlock__: 0x10cbfe088>
,即为全局的block。
NSLog(@"%@",^{
NSLog(@"1233 - %d",a);
});
这样直接打印block打印出来的是<_NSStackBlock__: 0x7ffeeb763440>_
,即为栈block。
而如果将block赋值给block变量后,栈block就会被拷贝到堆上,成为了堆block,就像下面的这种block。
int a = 10;
void (^block)(void) = ^{
NSLog(@"1233 - %d",a);
};
这种block的打印结果为<__NSMallocBlock__: 0x600002cd8090>
,即为堆block。
上面这三种block就是我们在开发中常见的三种block,除此之外还有三种系统的block。我们可以在block的源码中看到下面六中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 };
Block的循环引用
self.name = @"HelloBlock";
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
我们知道上面的代码会引起循环引用。因为形成了self--->block--->self的循环引用。
我们可以通过__weak来打破这种闭环。
self.name = @"HelloBlock";
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@",weakSelf.name);
};
self.block();
添加了__weak后虽然不会引起循环引用了。但是如果我们像下面代码一样进行了延迟调用,然后在延迟期间,我们退出了当前的viewController,self(即当前的ViewController)就会释放,然后打印weakSelf.name的结果就成为了null。
self.name = @"HelloBlock";
__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
});
};
self.block();
延迟打印的结果是null,这不是我们期望的结果。我们希望block打印完成后再释放self,这样既不会循环引用,也可以获取到正常的name的值。
我们只需要在block里面加一个strongSelf,就可以解决。这样的话,block执行完成后才会给strongSelf发送release消息,strongSelf释放;然后viewController才会释放。这种方式属于中介者模式,使用到了weakSelf和strongSelf作为中介者。
self.name = @"HelloBlock";
__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();
我们还可以通过下面的这种中介者模式来防止循环引用,添加一个vc,然后在block执行完成后手动置为nil,从而打破了循环引用的闭环。但是这种方式一定要执行block,如果不执行的话,还是会导致循环引用。
self.name = @"HelloBlock";
__block ViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
我们还可以通过block传参的形式来防止循环引用问题,代码如下:
self.block = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
再次我们总结一下防止循环引用的三种方式:
- __weak;
- 中介者模式,在block执行完成后手动置为nil;
- block传参;
Block的底层探索
- 1、Block的本质是什么?
- 2、Block为什么需要调用?
- 3、Block自动捕获外界变量;
- 4、__block的原理;
block.c文件中内容如下
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("HelloBlock");
};
block();
return 0;
}
然后我们在终端中切换到该文件目录下,使用clang命令来探索一下这段代码对应的编译器底层的代码
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk block.c
这样我们就得到了一个block.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.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("HelloBlock");
}
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(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
block->FuncPtr(block);
return 0;
}
该文件中的main函数对应我们block.c中的main函数。就是简单的申明block和调用block。
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
这句为block的声明,通过上面的代码可知,其中的__main_block_impl_0
是一个struct结构体,上面的代码就是构建了一个结构体。所以Block的本质就是结构体。构建结构体的时候传递了两个参数,一个是__main_block_func_0
,另一个是__main_block_desc_0_DATA
。其中的__main_block_func_0
是一个打印的函数,也就是我们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;
}
};
将__main_block_func_0
保存在了FuncPtr属性中。调动Block的时候就是通过FuncPtr来调用的
block->FuncPtr(block);
所以Block的本质是结构体。block定义的时候将block函数保存到结构体中,然后block调用的时候,从结构体中取出函数进行调用。
我们在block.c文件中添加一个外界变量a,然后在block中访问该变量a,然后再通过clang命令生成对应的block.cpp文件,关键的内容如下:
// 申明Block时初始化的结构体
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;
//保存block的函数
impl.FuncPtr = fp;
Desc = desc;
}
};
// block函数内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("HelloBlock - %d",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;
//生命block
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
//调用block
block->FuncPtr(block);
return 0;
}
main函数后多了一个a变量,然后创建结构体的时候将a作为参数传递了进去,在结构体的构建方法的最后有个:a(_a)
,就是讲参数_a赋值给了__main_block_impl_0结构体的属性a。
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;
//保存block的函数
impl.FuncPtr = fp;
Desc = desc;
}
在block调用的函数中,将结构体属性a通过值拷贝赋值给了一个新的变量a。所以这样没法直接对外面我们自己的变量a进行操作。
// block函数内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("HelloBlock - %d",a);
}
至此我们知道了block是怎么捕获外界变量的:通过生成一个属性来保存变量值。
下面我们将外界变量a换成__block修饰,然后在block里面进行修改。代码如下
int main(){
__block int a = 10;
void(^block)(void) = ^{
a++;
printf("HelloBlock - %d",a);
};
block();
return 0;
}
对应的clang生命的c++代码如下:
// 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;
}
};
//block对应函数的调用
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)++;
printf("HelloBlock - %d",(a->__forwarding->a));
}
// __block变量对应结构体的定义
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
int main(){
// __block变量生成的结构体
__Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
10
};
// block的声明
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
// block的调用
block->FuncPtr(block);
return 0;
}
可见,__block修饰的变量a被操作成了一个结构体__Block_byref_a_0
,该结构体中保存了变量a的地址和值。在block的回调函数中将结构体a的指针赋值给了a变量,然后通过a->__forwarding->a
访问到结构体中保存的外面变量a的地址,然后实现对外面变量a的++操作。
//block对应函数的调用
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)++;
printf("HelloBlock - %d",(a->__forwarding->a));
}
__block修改外面变量的原理:为对应的__block变量生成了一个结构体,该结构体中保存了外界变量的指针和值,传递了一个指针递给给block函数调用。从而可以实现修改外面变量。
block源码分析
我们可以在openSource上找到block的开源源码libclosure。下面来看下源码内容:
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor; //
// imported variables
};
// 可选
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
Block_layout结构体就是block的结构。Block_descriptor_2和Block_descriptor_3是block的可选属性,block中是否存在这两个属性需要由Block_layout结构体中的flags属性来决定。
flags的定义如下:
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
其中的BLOCK_HAS_COPY_DISPOSE
用来标识是否存在Block_descriptor_2;;BLOCK_HAS_SIGNATURE
用来标识是否存在Block_descriptor_3;BLOCK_IS_GLOBAL
用来标识是否存在为全局的block。
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
如果flags & BLOCK_HAS_COPY_DISPOSE为假,Block_descriptor_2返回为NULL。否则就可以通过Block_descriptor_1进行内存偏移访问到Block_descriptor_2.
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
如果flags & BLOCK_HAS_SIGNATURE为假,Block_descriptor_3返回为NULL。否则就可以通过Block_descriptor_1和Block_descriptor_2进行内存偏移访问到Block_descriptor_3。
block由栈到堆
下面我们使用Debug--Debug Workflow--Always show Disassembly来看下汇编,看下Debug怎么从StackBlock变成MallocBlock的。我们在block的地方打上断点。
然后打开汇编调试,运行就会定位到汇编代码:
image.png
我们看到汇编中有个objc_retainBlock,我们按住control,然后Step into跳转进去。此时使用LLDB命令,
register read x0
(读取x0的时候应该使用真机调试),读取到block为GlobalBlock。image.png
然后我们将block代码改为访问外界变量a的block。
int a = 10;
void (^block1)(void) = ^{
NSLog(@"LG_Block - %d", a);
};
block1();
然后重新使用汇编调试,跳转到objc_retainBlock,打印x0信息,可以看到此时block变成了StackBlock类型
image.png
继续往下走,会走到Block_copy函数,此方法应该是拷贝block的方法,我们在该方法的最下面的return的地方打个断点,跳转到这个地方,然后read此时的x0(此时的x0即为返回值)。可以看到此时的x0变成了mallocBlock。
image.png
block签名
下面我们看下Block的签名信息
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires 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
};
我们从closure源码可以看到,block的signature签名位于Block_descriptor_3中,而Block_descriptor_3可以根据 Block_layout中的flags判断是否存在。下面我们通过汇编来追踪下signature的内容。
最简单的方法,我们直接打印block也可以看到signature的内容:
我们还可以通过地址偏移来追踪signature的内容
image.png
上图中第一个x/4gx是打印的block结构体对象的内存地址,其中第四段是Block_descriptor_1的地址,然后接着我们使用x/4gx打印Block_descriptor_1的内容地址,通过flags可以知道Block_descriptor_2不存在,只存在Block_descriptor_1和Block_descriptor_3。其中的
0x0000000000000020
就是Block_descriptor_1,0x000000010078f34c
就是Block_descriptor_3。 因为在Block_descriptor_3中第一个变量就是signature,所以直接就可以打印出signature了。现在我们知道了Block的签名形式为
@?
的形式,@代表对象,?代表未知的。就是指的block对象。
block的拷贝
// 栈 -> 堆 研究拷贝
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) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {//全局的block不需要拷贝,直接返回
return aBlock;
}
else {
// 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;
}
}
我们closure源码中_Block_copy
的方法内容很简单,就是判断如果是全局的block的话不需要拷贝,直接返回。否则就是栈block,在堆区创建一个,然后把invoke、flags、description等都拷贝过去,然后将isa设置为MallocBlock。这样就完成了block本身的拷贝。
__block变量的堆拷贝
__block NSString *lg_name = [NSString stringWithFormat:@"cooci"];
void (^block1)(void) = ^{ // block_copy
lg_name = @"LG_Cooci";
NSLog(@"LG_Block - %@",lg_name);
};
block1();
切换到文件目录下,使用clang命令xcrun -sdk iphoneSimulator clang -rewrite-objc main.m
,就会生成main.cpp文件。在cpp文件中我们发现下面的与copy相关的函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->lg_name, (void*)src->lg_name, 8/*BLOCK_FIELD_IS_BYREF*/);
}
然后调用了_Block_object_assign
函数。我们到closure源码中查找这个函数
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;
default:
break;
}
}
switch判断,BLOCK_FIELD_IS_OBJECT为对象;BLOCK_FIELD_IS_BLOCK为block;BLOCK_FIELD_IS_BYREF为__block修饰的变量。之前我们研究了_Block_copy也就是block的拷贝。下面我们看下其他的拷贝。
如果是BLOCK_FIELD_IS_BLOCK,也就是对象类型,调用了_Block_retain_object
,但是该方法中什么都没有做,因为如果是对象类型的话会交给ARC来处理。
如果是BLOCK_FIELD_IS_BYREF类型的话,也就是__block修饰的结构体变量,调用了_Block_byref_copy
方法。
static struct Block_byref *_Block_byref_copy(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;
// 问题 - __block 修饰变量 block具有修改能力
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) {
// ...... 省略了一些代码
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
使用 malloc在堆区拷贝了一份,然后赋值过去。有一点需要注意的是:
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
不管是原来的__block结构体变量还是拷贝的__block结构体变量,都指向了copy的对象。这样就对应上了__block对象可以修改外界变量的部分。因为他们都指向了同一块堆区的地址。
在堆__block结构体进行拷贝的方法中调用了下面的这句代码,从而完成了对__block结构体中变量进行了拷贝。
(*src2->byref_keep)(copy, src);
src中的byref_keep函数定义如下
void(*BlockByrefKeepFunction)(struct Block_byref*, struct Block_byref*);
src2的类型是Block_byref结构体。他的定义类型与Block结构体的定义类似,下面我们找打它的byref_keep函数位于Block_byref_2中,也就是第5个参数。
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
我们到clang生成的cpp中找到对应的Block_byref_lg_name_0的第5个参数,即__Block_byref_id_object_copy_131。
__Block_byref_lg_name_0 lg_name = {
(void*)0,
(__Block_byref_lg_name_0 *)&lg_name,
33554432,
sizeof(__Block_byref_lg_name_0),
__Block_byref_id_object_copy_131, == keep
__Block_byref_id_object_dispose_131,
};
__Block_byref_id_object_copy_131``的定义如下,它又调用了我们上面分析的_Block_object_assign,传递的参数通过地址偏移找到了__block结构体中保存的外界___block变量。然后进行拷贝操作。最终完成了__block变量的修改。
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
总结:Block的三层拷贝:1、Block结构体的拷贝;2、__block结构体的拷贝;3、__block结构体中的__block变量的拷贝 ;
上面讲述了block的copy过程,block的释放过程和拷贝过程类似。
网友评论