摘要:
我们在开发的过程中经常使用到block,不仅如此,apple的api里面也有很多使用到block,好比gcd里面也是大量使用了block。block在语法上来说比较简洁,不过还是需要注意不要引起了循环引用。
裸block:
我们先来看看最基本的block编译之后长啥样:
typedef void (^Block)();
int main(int argc, const char * argv[]) {
@autoreleasepool
{
Block b = ^(){
printf("Hello Gay");
};
}
return 0;
}
我们使用 clang-rewrite-objc filename 指令编译一下到底是什么鬼
struct __block_impl {
void *isa; //什么类型的block
int Flags; //block的一些附加信息,里面包含了何种类型的block
//以及引用计数,下面讲到copy的时候就能知道到底是干嘛的了
int Reserved; //保留位
void *FuncPtr; //函数指针
};
struct __main_block_impl_0 {
struct __block_impl impl;
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello Gay");
}
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[]) {
{
__AtAutoreleasePool __autoreleasepool;
Block b = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
}
return 0;
}
我们可以看到声明的block以结构体的方式进行了存储,从侧面上来说,block是一个指向结构体的指针。其中__block_impl结构体记录的是block的相关信息包括block的类型以及引用计数等。
block里面捕获变量之后
我们假设在block里面捕获了一个变量,看看内部会变成什么样?
typedef void (^Block)();
int main(int argc, const char * argv[]) {
@autoreleasepool
{
int i = 0;
Block b = ^(){
printf("%d",i);
};
}
return 0;
}
同样clang编译之后
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int i; //相对于未捕获变量的block来说,多了一个变量来保存值
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int i = __cself->i; // bound by copy
printf("%d",i);
}
int main(int argc, const char * argv[])
{
{
__AtAutoreleasePool __autoreleasepool;
int i = 0;
Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, i));
//注意这里的i传的是值,因此意味着在当我们在外部修改这个值的时候,你调用block打印出来的时候,这个值依旧是之前的值
}
return 0;
}
乍一看,好像跟刚才的大差不差,__main_block_impl_0里面产生了一个变量用来保存之前捕获的值。
block捕获可修改的变量之后
贴上low B代码
typedef void (^Block)();
int main(int argc, const char * argv[]) {
@autoreleasepool
{
__block int i = 0;
Block b = ^(){
printf("%d",i);
};
}
return 0;
}
我们再次编译一下👀
struct __Block_byref_i_0 {
void *__isa; //什么类型的数据
__Block_byref_i_0 *__forwarding; //这个到copy的时候就用得着了,主要是保证copy之后能够找到在堆上的那个变量
int __flags;//变量的引用计数
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref 这里跟之前不一样了,这次是用指针来保存的
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
printf("%d",(i->__forwarding->i));
}
//辅助block copy的时候,对捕获变量的存储方案
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
//辅助block release的时候,对捕获变量的释放策略
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
//这里面添加copy和dispose两个函数
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(int argc, const char * argv[]) {
{ __AtAutoreleasePool __autoreleasepool;
//这里将结构体i的__forwarding指针指向了自身
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
//570425344干嘛的?后面识别block类型的时候就能用上了
}
return 0;
}
通过编译我们可以看到,block的描述里面多了两个函数(关于copy和release)
Block_copy的实现
之前的热身内容是为了让大家思路更加清(meng)晰(bi)。
我们可以在编译器上使用Block_copy()或者在Block.h里面查看到关于Block_copy的定义
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
在runtime.c中_Block_copy函数以这种方式实现了
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
该函数内部调用了_Block_copy_internal,同样我们在runtime.c中能够查看到该函数的实现方式
先贴上几个block类型的枚举,在这里能够看到Block_private.h
enum{
BLOCK_REFCOUNT_MASK = (0xffff),
BLOCK_NEEDS_FREE = (1 << 24), //堆block
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), */* Helpers have C++ code. */*
BLOCK_IS_GC = (1 << 27),
BLOCK_IS_GLOBAL = (1 << 28), //全局block
BLOCK_HAS_DESCRIPTOR = (1 << 29)
};
_Block_copy_internal实现如下
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;
// 还记得上面的那个初始化bloc时候赋值给flags标志位的570425344吗
// 570425344 & (1 << 24) = 0 不满足跳过
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
//这边也是不满足条件的
else if (aBlock->flags & BLOCK_IS_GC) {
//此处省略若干行代码。。。
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL)
{
return aBlock;
}
//最后来到这
//isGC在runtime.c文件里面能够找到,该变量被初始化为false
// Its a stack block. Make a copy.
// 对栈block的copy
if (!isGC) {
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
//这里是重点给flags标识为加上BLOCK_NEEDS_FREE标识位
//同时增加了一个引用计数
result->flags |= BLOCK_NEEDS_FREE | 1;
//修改isa指针
result->isa = _NSConcreteMallocBlock;
if (result->flags & BLOCK_HAS_COPY_DISPOSE)
{
//如果存在copy的辅助函数,会调用该辅助函数,
//当捕获了引用的时候,显然是满足条件的。
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
else {
//此处省略。。。
}
}
至此,相信都知道了copy是怎么样一个过程了。当对一个堆上的block再次进行调用copy的时候,因为我们之前给flags打入了BLOCK_NEEDS_FREE这个值,所以最后走的是这个判定条件
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
结果就是增加引用计数,然后返回该block。
辅助copy/dispose函数
1.普通变量的copy
在前面我们说到,如果有block复制到堆上的时候,有copy辅助函数的,该函数会被调用。以__block int i = 0为例子生成的辅助函数如下
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
在此之前,先赋上block中copy辅助函数,flags标识位能够支持的类型
enum {
BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, __attribute__((NSObject)), block, ... */
BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */
BLOCK_FIELD_IS_BYREF = 8, // __block修饰的基本数据类型
BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */
BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */
};
在runtime.c里面_Block_object_assign函数的实现方式如下
void _Block_object_assign(void *destAddr, const void *object, const int flags)
{
if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER)
{
if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK)
{
_Block_assign_weak(object, destAddr);
}
else
{
_Block_assign((void *)object, destAddr);
}
}
//之前的生成的辅助函数,flags = 8 => BLOCK_FIELD_IS_BYREF
else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)
{
//在这里调用了_Block_byref_assign_copy,看下面的那个函数
_Block_byref_assign_copy(destAddr, object, flags);
}
else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK)
{
_Block_assign(_Block_copy_internal(object, flags), destAddr);
}
else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT)
{
_Block_retain_object(object);
_Block_assign((void *)object, destAddr);
}
}
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags)
{
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
if (src->forwarding->flags & BLOCK_IS_GC) {
; // don't need to do any more work
}
else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
//省略若干行
}
//因为之前block已经拷贝到对上了,所以最后走的是这边,对该变量增加了一个引用计数
else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// 在这里进行赋值操作
_Block_assign(src->forwarding, (void **)destp);
}
上面就是一个被__block修饰的基本数据类型拷贝到堆上的时候,copy函数的实际调用过程。
普通oc对象的复制
以捕获NSObject对象为栗子
辅助函数如下:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
同样是调用_Block_object_assign这个函数,不过最终走的是这个
if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT)
{
_Block_retain_object(object);
_Block_assign((void *)object, destAddr);
}
把该对象retain之后,然后再赋值,这也就是说为什么block里面的对象需要使用weak的原因。
__block修饰的oc对象的复制
还是以NSObject为栗子,辅助函数如下:
//131即为BLOCK_FIELD_IS_OBJECT|BLOCK_BYREF_CALLER
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_object_assign这个函数,最终走的是这个
if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {
if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {
_Block_assign_weak(object, destAddr);
}
else {
// 最终走的是这个
// do *not* retain or *copy* __block variables whatever they are
_Block_assign((void *)object, destAddr);
}
}
该函数说明,如果你使用了______block对oc对象进行修饰,那么仅仅只是赋值,不增加引用计数,这就是使用______block不会造成循环引用的原因。
小结
通过上面的分析,相信大家对block有了更加清晰的理解。🐶
如果你看完以上内容觉得so easy,那么你可能是大神。
如果你看完以上内容觉得啥JB玩意,那么可能是我写的太渣了。
如果文章中,有错误的地方欢迎大家提出指正
附录
本文参考了以下两篇帖子,特此奉上:
没事蹦蹦的Block实现原理
BobooO的iOS中block介绍(四)揭开神秘面纱(下)
网友评论