iOS-Block全解

前言

Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。一句话来形容Blocks,带有自动变量(局部变量)的匿名函数。

Block在OC中的实现如下:

structBlock_layout {void*isa;intflags;intreserved;void(*invoke)(void*, ...);structBlock_descriptor *descriptor;/* Imported variables. */};structBlock_descriptor {unsignedlongintreserved;unsignedlongintsize;void(*copy)(void*dst,void*src);void(*dispose)(void*);};

从结构图中很容易看到isa,所以OC处理Block是按照对象来处理的。在iOS中,isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种(另外只在GC环境下还有3种使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,本文暂不谈论这3种,有兴趣的看看官方文档)

以上介绍是Block的简要实现,接下来我们来仔细研究一下Block的捕获外部变量的特性以及__block的实现原理。

研究工具:clang

为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。该命令是

clang -rewrite-objc block.c

目录

1.Block捕获外部变量实质

2.Block的copy和release

3.Block中__block实现原理

一.Block捕获外部变量实质

拿起我们的Block一起来捕捉外部变量吧。

说到外部变量,我们要先说一下C语言中变量有哪几种。一般可以分为一下5种:

自动变量

函数参数

静态变量

静态全局变量

全局变量

研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。

我们先根据这4种类型

自动变量

静态变量

静态全局变量

全局变量

写出Block测试代码。

这里很快就出现了一个错误,提示说自动变量没有加__block,由于__block有点复杂,我们先实验静态变量,静态全局变量,全局变量这3类。测试代码如下:

#importintglobal_i =1;staticintstatic_global_j =2;intmain(intargc,constchar* argv[]) {staticintstatic_k =3;intval =4;void(^myBlock)(void) = ^{        global_i ++;        static_global_j ++;        static_k ++;NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);    };    global_i ++;    static_global_j ++;    static_k ++;    val ++;NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);    myBlock();return0;}

运行结果

Block 外  global_i =2,static_global_j =3,static_k =4,val =5Block 中  global_i =3,static_global_j =4,static_k =5,val =4

这里就有2点需要弄清楚了

1.为什么在Block里面不加__bolck不允许更改变量?

2.为什么自动变量的值没有增加,而其他几个变量的值是增加的?自动变量是什么状态下被block捕获进去的?

为了弄清楚这2点,我们用clang转换一下源码出来分析分析。

(main.m代码行37行,文件大小832bype, 经过clang转换成main.cpp以后,代码行数飙升至104810行,文件大小也变成了3.1MB)

源码如下

intglobal_i =1;staticintstatic_global_j =2;struct__main_block_impl_0 {struct__block_impl impl;struct__main_block_desc_0* Desc;int*static_k;intval;  __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,int*_static_k,int_val,intflags=0) : static_k(_static_k), val(_val) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {int*static_k = __cself->static_k;// bound by copyintval = __cself->val;// bound by copyglobal_i ++;        static_global_j ++;        (*static_k) ++;NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);    }staticstruct__main_block_desc_0 {  size_t reserved;  size_t Block_size;} __main_block_desc_0_DATA = {0,sizeof(struct__main_block_impl_0)};intmain(intargc,constchar* argv[]) {staticintstatic_k =3;intval =4;void(*myBlock)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));    global_i ++;    static_global_j ++;    static_k ++;    val ++;NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);    ((void(*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);return0;}

首先全局变量global_i和静态全局变量static_global_j的值增加,以及它们被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,所以Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

接下来仔细看看自动变量和静态变量的问题。

在__main_block_impl_0中,可以看到静态变量static_k和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。

接着看构造函数,

__main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,int*_static_k,int_val,intflags=0) : static_k(_static_k), val(_val)

这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。

main里面的myBlock闭包中的__main_block_impl_0结构体,初始化如下

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

impl.isa = &_NSConcreteStackBlock;

impl.Flags = 0;

impl.FuncPtr = __main_block_impl_0;

Desc = &__main_block_desc_0_DATA;

*_static_k = 4;

val = 4;

到此,__main_block_impl_0结构体就是这样把自动变量捕获进来的。也就是说,在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。

这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。

Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

再研究一下源码,我们注意到__main_block_func_0这个函数的实现

staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {int*static_k = __cself->static_k;// bound by copyintval = __cself->val;// bound by copyglobal_i ++;        static_global_j ++;        (*static_k) ++;NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);    }

我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量val虽然被捕获进来了,但是是用 __cself->val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量val的值,依旧没法去改变Block外面自动变量val的值。

OC可能是基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。错误就是最开始的那张截图。

Variable is not assignable(missing __block type specifier)

小结一下:

到此为止,上面提出的第二个问题就解开答案了。自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。上面例子也都证明过了。

剩下问题一我们还没有解决。

回到上面的例子上面来,4种变量里面只有静态变量,静态全局变量,全局变量这3种是可以在Block里面被改变值的。仔细观看源码,我们能看出这3个变量可以改变值的原因。

静态全局变量,全局变量由于作用域的原因,于是可以直接在Block里面被改变。他们也都存储在全局区。

静态变量传递给Block是内存地址值,所以能在Block里面直接改变值。

根据官方文档我们可以了解到,苹果要求我们在自动变量前加入__block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。

总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。

先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。

#importintmain(intargc,constchar* argv[]) {NSMutableString* str = [[NSMutableStringalloc]initWithString:@"Hello,"];void(^myBlock)(void) = ^{            [str appendString:@"World!"];NSLog(@"Block中 str = %@",str);        };NSLog(@"Block外 str = %@",str);    myBlock();return0;}

控制台输出:

Block 外  str = Hello,

Block 中  str = Hello,World!

看结果是成功改变了变量的值了,转换一下源码。

struct__main_block_impl_0 {struct__block_impl impl;struct__main_block_desc_0* Desc;NSMutableString*str;  __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,NSMutableString*_str,intflags=0) : str(_str) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {NSMutableString*str = __cself->str;// bound by copy((void(*)(id, SEL,NSString*))(void*)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1);NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str);        }staticvoid__main_block_copy_0(struct__main_block_impl_0*dst,struct__main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str,3/*BLOCK_FIELD_IS_OBJECT*/);}staticvoid__main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->str,3/*BLOCK_FIELD_IS_OBJECT*/);}staticstruct__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};intmain(intargc,constchar* argv[]) {NSMutableString* str = ((NSMutableString*(*)(id, SEL,NSString*))(void*)objc_msgSend)((id)((NSMutableString*(*)(id, SEL))(void*)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0);void(*myBlock)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, str,570425344));NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str);    ((void(*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);return0;}

在__main_block_func_0里面可以看到传递的是指针。所以成功改变了变量的值。

至于源码里面的copy和dispose下一节会讲到。

改变外部变量值的第二种方式是加 __block这个放在第三章里面讨论,接下来我们先讨论一下Block的copy的问题,因为这个问题会关系到 __block存储域的问题。

二.Block的copy和dispose

OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

先来说明一下3者的区别。

1.从捕获外部变量的角度上来看

_NSConcreteStackBlock:

只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。

StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。

_NSConcreteMallocBlock:

有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制

_NSConcreteGlobalBlock:

没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

没有用到外部变量肯定是_NSConcreteGlobalBlock,这点很好理解。不过只用到全局变量、静态变量的block也是_NSConcreteGlobalBlock。举例如下:

#importintglobal_i =1;staticintstatic_global_j =2;intmain(intargc,constchar* argv[]) {staticintstatic_k =3;void(^myBlock)(void) = ^{NSLog(@"Block中 变量 = %d %d %d",static_global_j ,static_k, global_i);        };NSLog(@"%@",myBlock);    myBlock();return0;}

输出:

<__NSGlobalBlock__:0x100001050>Block中 变量 =231

可见,只用到全局变量、静态变量的block也可以是_NSConcreteGlobalBlock。

所以在ARC环境下,3种类型都可以捕获外部变量。

2.从持有对象的角度上来看:

_NSConcreteStackBlock是不持有对象的。

//以下是在MRC下执行的NSObject* obj = [[NSObjectalloc]init];NSLog(@"1.Block外 obj = %lu",(unsignedlong)obj.retainCount);void(^myBlock)(void) = ^{NSLog(@"Block中 obj = %lu",(unsignedlong)obj.retainCount);    };NSLog(@"2.Block外 obj = %lu",(unsignedlong)obj.retainCount);    myBlock();

输出:

1.Block外 obj =12.Block外 obj =1Block中 obj =1

_NSConcreteMallocBlock是持有对象的。

//以下是在MRC下执行的NSObject* obj = [[NSObjectalloc]init];NSLog(@"1.Block外 obj = %lu",(unsignedlong)obj.retainCount);void(^myBlock)(void) = [^{NSLog(@"Block中 obj = %lu",(unsignedlong)obj.retainCount);    }copy];NSLog(@"2.Block外 obj = %lu",(unsignedlong)obj.retainCount);    myBlock();    [myBlock release];NSLog(@"3.Block外 obj = %lu",(unsignedlong)obj.retainCount);

输出:

1.Block外 obj =12.Block外 obj =2Block中 obj =23.Block外 obj =1

_NSConcreteGlobalBlock也不持有对象

//以下是在MRC下执行的void(^myBlock)(void) = ^{NSObject* obj = [[NSObjectalloc]init];NSLog(@"Block中 obj = %lu",(unsignedlong)obj.retainCount);    };    myBlock();

输出:

Block 中 obj =1

由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上。

1.手动调用copy

2.Block是函数的返回值

3.Block被强引用,Block被赋值给__strong或者id类型

4.调用系统API入参中含有usingBlcok的方法

以上4种情况,系统都会默认调用copy方法把Block赋复制

但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。

copy函数把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))// Create a heap based copy of a Block or simply add a reference to an existing one.// This must be paired with Block_release to recover memory, even when running// under Objective-C Garbage Collection.BLOCK_EXPORTvoid*_Block_copy(constvoid*aBlock)    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Lose the reference, and if heap based and last reference, recover the memoryBLOCK_EXPORTvoid_Block_release(constvoid*aBlock)    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Used by the compiler. Do not call this function yourself.BLOCK_EXPORTvoid_Block_object_assign(void*,constvoid*,constint)    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Used by the compiler. Do not call this function yourself.BLOCK_EXPORTvoid_Block_object_dispose(constvoid*,constint)    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

上面是源码中2个常用的宏定义和4个常用的方法,一会我们就会看到这4个方法。

staticvoid*_Block_copy_internal(constvoid*arg,constintflags) {structBlock_layout *aBlock;constboolwantsOne = (WANTS_ONE & flags) == WANTS_ONE;// 1if(!arg)returnNULL;// 2aBlock = (structBlock_layout *)arg;// 3if(aBlock->flags & BLOCK_NEEDS_FREE) {// latches on highlatching_incr_int(&aBlock->flags);returnaBlock;    }// 4elseif(aBlock->flags & BLOCK_IS_GLOBAL) {returnaBlock;    }// 5structBlock_layout *result = malloc(aBlock->descriptor->size);if(!result)return(void*)0;// 6memmove(result, aBlock, aBlock->descriptor->size);// bitcopy first// 7result->flags &= ~(BLOCK_REFCOUNT_MASK);// XXX not neededresult->flags |= BLOCK_NEEDS_FREE |1;// 8result->isa = _NSConcreteMallocBlock;// 9if(result->flags & BLOCK_HAS_COPY_DISPOSE) {        (*aBlock->descriptor->copy)(result, aBlock);// do fixup}returnresult;}

上面这一段是Block_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程。对应有9个步骤。

void_Block_release(void*arg) {// 1structBlock_layout *aBlock = (structBlock_layout *)arg;if(!aBlock)return;// 2int32_t newCount;    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;// 3if(newCount >0)return;// 4if(aBlock->flags & BLOCK_NEEDS_FREE) {if(aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);        _Block_deallocator(aBlock);    }// 5elseif(aBlock->flags & BLOCK_IS_GLOBAL) {        ;    }// 6else{        printf("Block_release called upon a stack Block: %p, ignored\\\\n", (void*)aBlock);    }}

上面这一段是Block_release的一个实现,实现了怎么释放一个Block。对应有6个步骤。

上述2个方法的详细解析可以看这篇文章

回到上一章节中最后的例子,字符串的例子中来,转换源码之后,我们会发现多了一个copy和dispose方法。

因为在C语言的结构体中,编译器没法很好的进行初始化和销毁操作。这样对内存管理来说是很不方便的。所以就在 __main_block_desc_0结构体中间增加成员变量 void (*copy)(struct  __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime进行内存管理。

相应的增加了2个方法。

staticvoid__main_block_copy_0(struct__main_block_impl_0*dst,struct__main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str,3/*BLOCK_FIELD_IS_OBJECT*/);}staticvoid__main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->str,3/*BLOCK_FIELD_IS_OBJECT*/);}

这里的_Block_object_assign和_Block_object_dispose就对应着retain和release方法。

BLOCK_FIELD_IS_OBJECT 是Block截获对象时候的特殊标示,如果是截获的__block,那么是BLOCK_FIELD_IS_BYREF。

三.Block中__block实现原理

我们继续研究一下__block实现原理。

1.普通非对象的变量

先来看看普通变量的情况。

#importintmain(intargc,constchar* argv[]) {    __blockinti =0;void(^myBlock)(void) = ^{        i ++;NSLog(@"%d",i);    };    myBlock();return0;}

把上述代码用clang转换成源码。

struct __Block_byref_i_0 {

void *__isa;

__Block_byref_i_0 *__forwarding;

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

(i->__forwarding->i) ++;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));

}

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*/);}

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[]) {

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));

((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

return 0;

}

从源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i,0,sizeof(__Block_byref_i_0),0};

源码中是这样初始化的。__forwarding指针初始化传递的是自己的地址。然而这里__forwarding指针真的永远指向自己么?我们来做一个实验。

//以下代码在MRC中运行__blockinti =0;NSLog(@"%p",&i);void(^myBlock)(void) = [^{        i ++;NSLog(@"这是Block 里面%p",&i);    }copy];

我们把Block拷贝到了堆上,这个时候打印出来的2个i变量的地址就不同了。

0x7fff5fbff818<__NSMallocBlock__:0x100203cc0>这是Block 里面0x1002038a8

地址不同就可以很明显的说明__forwarding指针并没有指向之前的自己了。那__forwarding指针现在指向到哪里了呢?

Block里面的__block的地址和Block的地址就相差1052。我们可以很大胆的猜想,__block现在也在堆上了。

出现这个不同的原因在于这里把Block拷贝到了堆上。

由第二章里面详细分析的,堆上的Block会持有对象。我们把Block通过copy到了堆上,堆上也会重新复制一份Block,并且该Block也会继续持有该__block。当Block释放的时候,__block没有被任何对象引用,也会被释放销毁。

__forwarding指针这里的作用就是针对堆的Block,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。然后堆上的变量的__forwarding再指向自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。

所以在__main_block_func_0函数里面就是写的(i->__forwarding->i)。

这里还有一个需要注意的地方。还是从例子说起:

//以下代码在MRC中运行__blockinti =0;NSLog(@"%p",&i);void(^myBlock)(void) = ^{        i ++;NSLog(@"Block 里面的%p",&i);    };NSLog(@"%@",myBlock);    myBlock();

结果和之前copy的例子完全不同。

0x7fff5fbff818<__NSStackBlock__:0x7fff5fbff7c0>**0x7fff5fbff818

Block在捕获住__block变量之后,并不会复制到堆上,所以地址也一直都在栈上。这与ARC环境下的不一样。

ARC环境下,不管有没有copy,__block都会变copy到堆上,Block也是__NSMallocBlock。

感谢@酷酷的哀殿 指出错误,感谢@bestswifter 指点。上述说法有点不妥,详细见文章末尾更新。

ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。

MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上,block也只是__NSStackBlock,这个时候__forwarding指针就只指向自己了。

至此,文章开头提出的问题一,也解答了。__block的实现原理也已经明了。

2.对象的变量

还是先举一个例子:

//以下代码是在ARC下执行的#importintmain(intargc,constchar* argv[]) {    __blockidblock_obj = [[NSObjectalloc]init];idobj = [[NSObjectalloc]init];NSLog(@"block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);void(^myBlock)(void) = ^{NSLog(@"***Block中****block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);    };    myBlock();return0;}

输出

block_obj = [ ,0x7fff5fbff7e8] , obj = [ ,0x7fff5fbff7b8]Block****中********block_obj = [ ,0x100f000a8] , obj = [ ,0x100f00070]

我们把上面的代码转换成源码研究一下:

struct__Block_byref_block_obj_0 {void*__isa;__Block_byref_block_obj_0 *__forwarding;int__flags;int__size;void(*__Block_byref_id_object_copy)(void*,void*);void(*__Block_byref_id_object_dispose)(void*);idblock_obj;};struct__main_block_impl_0 {struct__block_impl impl;struct__main_block_desc_0* Desc;idobj;  __Block_byref_block_obj_0 *block_obj;// by ref__main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,id_obj, __Block_byref_block_obj_0 *_block_obj,intflags=0) : obj(_obj), block_obj(_block_obj->__forwarding) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {  __Block_byref_block_obj_0 *block_obj = __cself->block_obj;// bound by refidobj = __cself->obj;// bound by copyNSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_1,(block_obj->__forwarding->block_obj) , &(block_obj->__forwarding->block_obj) , obj , &obj);    }staticvoid__main_block_copy_0(struct__main_block_impl_0*dst,struct__main_block_impl_0*src) {_Block_object_assign((void*)&dst->block_obj, (void*)src->block_obj,8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj,3/*BLOCK_FIELD_IS_OBJECT*/);}staticvoid__main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->block_obj,8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->obj,3/*BLOCK_FIELD_IS_OBJECT*/);}staticstruct__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};intmain(intargc,constchar* argv[]) {    __attribute__((__blocks__(byref))) __Block_byref_block_obj_0 block_obj = {(void*)0,(__Block_byref_block_obj_0 *)&block_obj,33554432,sizeof(__Block_byref_block_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject*(*)(id, SEL))(void*)objc_msgSend)((id)((NSObject*(*)(id, SEL))(void*)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};idobj = ((NSObject*(*)(id, SEL))(void*)objc_msgSend)((id)((NSObject*(*)(id, SEL))(void*)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_0,(block_obj.__forwarding->block_obj) , &(block_obj.__forwarding->block_obj) , obj , &obj);void(*myBlock)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, obj, (__Block_byref_block_obj_0 *)&block_obj,570425344));    ((void(*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);return0;}

首先需要说明的一点是对象在OC中,默认声明自带__strong所有权修饰符的,所以main开头我们声明的

__blockidblock_obj = [[NSObjectalloc]init];idobj = [[NSObjectalloc]init];

等价于

__blockid__strongblock_obj = [[NSObjectalloc]init];id__strongobj = [[NSObjectalloc]init];

在转换出来的源码中,我们也可以看到,Block捕获了__block,并且强引用了,因为在__Block_byref_block_obj_0结构体中,有一个变量是id block_obj,这个默认也是带__strong所有权修饰符的。

根据打印出来的结果来看,ARC环境下,Block捕获外部对象变量,是都会copy一份的,地址都不同。只不过带有__block修饰符的变量会被捕获到Block内部持有。

我们再来看看MRC环境下的情况,还是将上述代码的例子运行在MRC中。

输出:

block_obj = [ ,0x7fff5fbff7e8] , obj = [ ,0x7fff5fbff7b8]Block****中********block_obj = [ ,0x7fff5fbff7e8] , obj = [ ,0x7fff5fbff790]

这个时候block在栈上,__NSStackBlock__,可以打印出来retainCount值都是1。当把这个block copy一下,就变成__NSMallocBlock__,对象的retainCount值就会变成2了。

总结:

在MRC环境下,__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。

而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象,所以才会产生循环引用的问题!

最后

关于Block捕获外部变量有很多用途,用途也很广,只有弄清了捕获变量和持有的变量的概念以后,之后才能清楚的解决Block循环引用的问题。

再次回到文章开头,5种变量,自动变量,函数参数 ,静态变量,静态全局变量,全局变量,如果严格的来说,捕获是必须在Block结构体__main_block_impl_0里面有成员变量的话,Block能捕获的变量就只有带有自动变量和静态变量了。捕获进Block的对象会被Block持有。

对于非对象的变量来说,

自动变量的值,被copy进了Block,不带__block的自动变量只能在里面被访问,并不能改变值。

带__block的自动变量 和 静态变量 就是直接地址访问。所以在Block里面可以直接改变变量的值。

而剩下的静态全局变量,全局变量,函数参数,也是可以在直接在Block中改变变量值的,但是他们并没有变成Block结构体__main_block_impl_0的成员变量,因为他们的作用域大,所以可以直接更改他们的值。

值得注意的是,静态全局变量,全局变量,函数参数他们并不会被Block持有,也就是说不会增加retainCount值。

对于对象来说,

在MRC环境下,__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。

而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象。

请大家多多指点。

更新

在ARC环境下,Block也是存在__NSStackBlock的时候的,平时见到最多的是_NSConcreteMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 __NSStackBlock__ 类型的 block 转换为 __NSMallocBlock__ 类型。

举例如下:

#importintmain(intargc,constchar* argv[]) {    __blockinttemp =10;NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});return0;}

输出

<__NSStackBlock__:0x7fff5fbff768>

这种情况就是ARC环境下Block是__NSStackBlock的类型。

作者:一缕殇流化隐半边冰霜

链接:http://www.jianshu.com/p/ee9756f3d5f6

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 911评论 1 3
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,330评论 2 26
  • Block基础回顾 1.什么是Block? 带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言...
    Bugfix阅读 6,744评论 5 61
  • 摘要block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD...
    西门吹雪123阅读 903评论 0 4
  • 摘要 Blocks是C语言的扩充功能, iOS 4中引入了这个新功能“Blocks”,那么block到底是什么东西...
    CholMay阅读 1,152评论 2 10