Blocks的学习
Block根据其类型可以分为三类:
而其区分的规则为:
如果没有引用局部变量,或者只引用了静态变量和全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针引用过,就是堆Block,如果没有则为栈Block。
- (void)func2 {
/**
— 全局block,没有使用局部变量,或者只使用了静态变量或者只使用了全局变量
*/
// 没有使用局部变量
NSLog(@"block0 - %@",^{});
// 使用了静态变量
void(^block1)(void) = ^{
blockInt = 3;
};
NSLog(@"block1 - %@",block1);
/**
- 堆Block 使用局部变量 并且用强指针引用过
*/
// 即使用局部变量又使用,静态变量
NSInteger i = 1;
void(^block2)(void) = ^{
NSLog(@"block %ld", i);
NSLog(@"block static %d", blockInt);
};
NSLog(@"block2 - %@",block2);
// 只使用局部变量
void(^block3)(void) = ^{
NSLog(@"block %ld", i);
};
NSLog(@"block3 - %@",block3);
// 使用强指针引用过,再使用若指针引用
void(^ __weak block4)(void) = block3;
NSLog(@"block4 - %@",block4);
/**
- 栈Block 没有被强引用过的
*/
// 没有使用强指针引用过
void (^__weak block5)(void) = ^{
NSLog(@"block %ld", i);
};
NSLog(@"block5 - %@",block5);
}
分析:
block0 : 没有使用任何变量,属于全局block。
block1 : 只使用了静态变量blockInt,属于全局block。
block2 : 使用了局部变量和静态变量,并且有被strong引用过,属于堆block
block3 : 使用了局部变量i,并且有被strong引用过,属于堆block
block4 : 虽然被weak指针引用的,但其已经被strong引用过,属于堆block
block5 : 没有被strong指针引用过。即使使用了局部变量,属于栈block
可以将block代码通过clang 重写为C++代码,看其底层实现
int object_c_origin_block() {
NSObject *obj = [[NSObject alloc] init];
void(^ block)(void) = ^ {
NSLog(@"%@",obj);
};
block();
return 0;
}
重写命令
xcrun -sdk iphoneos clang -rewrite-objc <OriginFile> -o <CppFile>
得到Block结构的定义:
struct __object_c_origin_block_block_impl_0 {
struct __block_impl impl;
struct __object_c_origin_block_block_desc_0* Desc;
NSObject *obj;
__object_c_origin_block_block_impl_0(void *fp, struct __object_c_origin_block_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
从重写的结果,我们可以得到Block的以下特点:
isa
指针指向_NSConcreteStackBlock
)__object_c_origin_block_block_impl_0
内部有obj
变量)isa
指针)在使用Block
的时候,最容易出现的问题就是循环引用,尤其是在mvvm
架构中,Controller
引用ViewModel
,ViewModel
引用Block
,有的Block
的复制在Controller
里面完成,有可能会捕获到Controller
,从而造成循环引用。
- (void)bindViewModel {
self.viewModel.refreshViewCallBack = ^(void) {
[self.tableView reloadData];
};
}
而解决循环引用有以下几种办法:
- (void)bindViewModel01 {
__weak typeof(self) weakSelf = self;
self.viewModel.refreshViewCallBack = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.tableView reloadData];
};
}
block
对象,并没有引用self
,在执行block
的时候strongSelf
的生命周期只有在block
内部,在block
内部,self
的引用计数+1,当执行完block
,引用计数-1。既没有引起循环引用,又适当延长了self
的生命周期,一举双得。
- (void)bindViewModel02 {
__block Controller *blockSelf = self;
self.viewModel.refreshViewCallBack = ^{
[blockSelf.tableView reloadData];
blockSelf = nil;
};
}
使用这种方式,同样也可以解决循环引用,但是要注意,block执行完一次,下一次执行之前记得要给blockSelf重新复制,不然会出问题,显然维护这个是非常麻烦的,所以不推荐。
- (void)bindViewModel03 {
self.viewModel.refreshViewBlcok = ^(OSMVVMViewController * _Nonnull vc) {
[vc.tableView reloadData];
};
self.viewModel.refreshViewBlcok(self);
}
通过block
的参数进行传递,同样可以解决循环引用,但是这样做的意义不大,因为block
在执行的地方,一定是需要获取到self
的,如果已经获取到self
了,就可以直接对self
操作了,再使用block
有点多余。应用并不多,只做了解。
来道经典的面试题:
- (void)func3 {
NSObject *o = [[NSObject alloc] init];
NSLog(@"CFGetRetainCount print start");
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
//1
void(^strongBlock)(void) = ^ {
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
};
strongBlock();
//3 在栈区引用一次,在堆区又引用一次
void(^ __weak weakBlock)(void) = ^{
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
};
weakBlock();
// 4栈区引用一次
void(^copyBlock)(void) = [strongBlock copy];
copyBlock();
// 4 本来就在栈上不用+1。
void(^copyBlock1)(void) = [weakBlock copy];
copyBlock1();
// 5
NSLog(@"CFGetRetainCount print end");
}
在Block源码中,有关于BlockCopy的源码:
// 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 的地址
// 运行?stack -》malloc
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;
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;
}
}
copy
的调用时机,有强指针第一次指向的时候,会调用一次copy
。而这次copy
,会把栈区的Block
拷贝到堆区。在平时使用[block copy]
的时候,也会调用。copy
的时候根据Block
的类型采取了不同的操作,如果是堆block
,只进行引用计数+1,相当于浅拷贝,如果是全局block
,直接返回,相当于不拷贝,如果是栈block,是重新开辟新的内存并创建,并且isa
指向_NSConcreteMallocBlock
,设置类型为堆block
,然后返回。在_Block_copy
源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)
的调用时为了复制栈区的Block
里面的成员变量,给堆区的Block
。
// 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
// 取得 block 中的 Block_descriptor_2
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
// 如果没有 Block_descriptor_2,就直接返回
if (!desc) return;
// 调用 desc 中的 copy 方法,copy 方法中会调用 _Block_object_assign 函数
(*desc->copy)(result, aBlock); // do fixup
}
其中最最要的代码时找到了一个copy
方法然后调用。而这个copy
方法是在desc
中。需要理解这些代码,需要借助clang
重写的c++
代码。
在重写的代码中可以看到__object_c_origin_block_hello_block_impl_0
的构造函数的调用:
__object_c_origin_block_hello_block_desc_0_DATA = {
0,
sizeof(struct __object_c_origin_block_hello_block_impl_0),
__object_c_origin_block_hello_block_copy_0,
__object_c_origin_block_hello_block_dispose_0
};
其实这里根据变量名称,就能猜个大概,__object_c_origin_block_hello_block_desc_0_DATA
就是源码中的desc
,而__object_c_origin_block_hello_block_copy_0即为(*desc->copy)
方法。所以我们要看其中拷贝成员变量的过程,需要关注__object_c_origin_block_hello_block_copy_0
的实现。
static void __object_c_origin_block_hello_block_copy_0(
struct __object_c_origin_block_hello_block_impl_0*dst,
struct __object_c_origin_block_hello_block_impl_0*src)
{
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}
这里是因为重写前的objcet-c代码里面的block
捕获了1个局部变量,只用copy一个成员变量所以只会有一句,如果捕获多个局部变量,就会有多句。例如:
static void __object_c_origin_block_hello_block_copy_0(
struct __object_c_origin_block_hello_block_impl_0*dst,
struct __object_c_origin_block_hello_block_impl_0*src)
{
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
其内部其实是调用了_Block_object_assign
方法,在源码中也可以找到_Block_object_assign
的实现:
/*******************************************************
block 可以引用 4 种不同的类型的对象,当 block 被拷贝到堆上时,需要 help,即帮助拷贝一些东西。
1)基于 C++ 栈的对象
2)Objective-C 对象
3)其他 Block
4)被 __block 修饰的变量
block 的 helper 函数是编译器合成的(比如编译器写的 __main_block_copy_1() 函数),它们被用在 _Block_copy() 函数和 _Block_release() 函数中。copy helper 对基于 C++ 栈的对象调用调用 C++ 常拷贝构造函数,对其他三种对象调用 _Block_object_assign 函数。 dispose helper 对基于 C++ 栈的对象调用析构函数,对其他的三种调用 _Block_object_dispose 函数。
_Block_object_assign 和 _Block_object_dispose 函数的第三个参数 flags 有可能是:
1)BLOCK_FIELD_IS_OBJECT(3) 表示是一个对象
2)BLOCK_FIELD_IS_BLOCK(7) 表示是一个 block
3)BLOCK_FIELD_IS_BYREF(8) 表示是一个 byref,一个被 __block 修饰的变量;如果 __block 变量还被 __weak 修饰,则还会加上 BLOCK_FIELD_IS_WEAK(16)
所以 block 的 copy/dispose helper 只会传入四种值:3,7,8,24
上述的4种类型的对象都会由编译器合成 copy/dispose helper 函数,和 block 的 helper 函数类似,byref 的 copy helper 将会调用 C++ 的拷贝构造函数(不是常拷贝构造),dispose helper 则会调用析构函数。还一样的是,helpers 将会一样调用进两个支持函数中,对于对象和 block,参数值是一样的,都另外附带上 BLOCK_BYREF_CALLER (128) bit 的信息。#疑问:调用的这两个函数是啥?BLOCK_BYREF_CALLER 里究竟存的是什么??
所以 __block copy/dispose helper 函数生成 flag 的值为:对象是 3,block 是 7,带 __weak 的是 16,并且一直有 128,有下面这么几种组合:
__block id 128+3 (0x83)
__block (^Block) 128+7 (0x87)
__weak __block id 128+3+16 (0x93)
__weak __block (^Block) 128+7+16 (0x97)
********************************************************/
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment,
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
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:
/*******
id object = ...;
[^{ object; } copy];
********/
// 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
// 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
_Block_retain_object(object);
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// 使 dest 指向的拷贝到堆上object
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
// 使 dest 指向的拷贝到堆上的byref
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
// 使 dest 指向的目标指针指向 object
*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:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
default:
break;
}
}
这里是对成员变量的类型进行了分类,如果是对象类型的,直接将对象的增加对象的引用计数,如果是Block
类型,会对该Block
也进行一次_Block_copy
操作,如果是__block
修饰的,会调用_Block_byref_copy
。_Block_byref_copy
的解析在下面。
对于__block
修饰的对象,底层会将其多封装一层。
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};
在对block
内部操作,其根本是操作他的__forwarding->obj
操作:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_qz_4pv2xnmd3g137rwtb0fpj2rr0000gn_T_OSBlockOriginFile_ce67cf_mi_1,(obj->__forwarding->obj));
(obj->__forwarding->obj) = __null;
在Block Copy
的时候,__block
修饰的对象或类型在拷贝的过程中会调用_Block_byref_copy
进行拷贝。
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
// 被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
// 原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
// arg 强转为 Block_byref * 类型
struct Block_byref *src = (struct Block_byref *)arg;
// 引用计数等于 0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 为新的 byref 在堆中分配内存
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
// 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
// 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
// one for caller 很好理解,那 one for stack 是为什么呢?
// 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 堆上 byref 的 forwarding 指向自己
copy->forwarding = copy; // patch heap copy to point to itself
// 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
src->forwarding = copy; // patch stack to point to heap copy
// 拷贝 size
copy->size = src->size;
// 如果 src 有 copy/dispose helper
if (src->flags & BLOCK_BYREF_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
// 取得 src 和 copy 的 Block_byref_2
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
// copy 的 copy/dispose helper 也与 src 保持一致
// 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
// 如果 src 有扩展布局,也拷贝扩展布局
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
// 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
copy3->layout = src3->layout;
}
// 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
// 如果 src 没有 copy/dispose helper
// 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// src 已经在堆上,就只将引用计数加 1
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
__block
和Block
类似,如果在栈区,会重新malloc
一份,进行深拷贝操作,但这两个forwarding
都会指向堆区的,如果已经在堆区,只会将其引用计数+1。
参考博客:
iOS八股文(十六)关于Block,你在第几层?