从iOS4开始,苹果引入了这个C语言的扩充功能“Blocks”,在一些特定的场景下也是一把利刃。我前面一篇博客中初步介绍了Blocks这个东西,主要是语法的介绍(《iOS中Blocks的介绍》)。
我曾经看见了老外的一个系列的Blocks介绍,颇有深度(A look inside blocks:Episode 1,A look inside blocks:Episode 2, A look inside blocks:Episode 3),里面深刻到汇编的层次对Blocks的实现进行了分析。不过若是象我这样对于汇编不熟悉的人确定也是不少的,理解起来十分痛苦,因而就想到从ObjC自己对Blocks进行的处理里面来入手分析,看看对于Blocks都悄悄作了什么。
2.环境
很简单,就是Xcode啦。使用的编译器是CLang,主要是利用了-rewrite-objc这个参数,把源文件转换成中间文件。这样就揭开了面纱的一角。我使用的clang编译器版本是:
Apple clang version 4.0 (tags/Apple/clang-421.0.60) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin12.5.0
Thread model: posix
转成中间文件的命令是:clang -rewrite-objc 源文件
3. 例子1
- #include <stdio.h>
-
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", val);};
-
- val = 4;
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
代码的运行结果是:
in Block():val=2
in main():val=4, val1=5
这里咱们能够看到Block对于自动变量的“快照”功能。因为转成中间文件以后发现长了不少,变成了100多行(不一样的clang版本转换出来的文件还不一样,不过实现部分代码仍是同样的),下面的代码是相关部分的节选,主要说明苹果是如何实现的。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
-
- int main(int, const charchar **);
-
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- int val;
- __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
- static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
- int val = __cself->val;
- printf("in Block():val=%d\n", val);}
-
- static struct __main_block_desc_0 {
- unsigned long reserved;
- unsigned long Block_size;
- } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, val);
-
- val = 4;
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
中间文件确实看起来复杂了很多,不过仍是有脉络可循。
看main函数的内容,里面有个函数指针blk,这个就是指向Block的指针,因此难怪Block变量的声明和函数指针如此相像(就是把*换成^),编译器转换后就是同一个东西啊。
咱们看blk这个函数指针,就是__main_block_impl_0这个结构体变量的指针,这个结构体变量此时已经存在,而后用__main_block_func_0等几个变量赋初值。咱们能够看到__main_block_impl_0这个struct中有个val这个项,而且在这里也赋值了,这就是给变量照的“快照”,因为这个变量在这里被记录了,因此不管外面的val变量如何变化,Block运行时使用的值就始终是“快照”的值了。同时咱们也注意到__main_block_impl_0这个struct中没有val1这个项,因此说明若是Block中不用到的自动变量是不会自动加入到结构体中的。
Block的运行就是运行__main_block_impl_0这个struct中的FuncPtr这个指针,这个在前面初始化的时候已经被赋值成__main_block_func_0了,因此这里也就是运行这个函数,并把本身的指针传入。这里咱们的实现很是简单,就是一句printf语句。
4.例子2
- #include <stdio.h>
-
- int main(int argc, const charchar * argv[])
- {
- int __block val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", ++val);};
-
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
这个例子把自动变量val声明成了__block变量,这样语法上Block不是对val进行“快照”,而是会直接使用val变量,同时在Block内部val可读可写,再也不是只读的了。
运行结果以下:
in Block():val=3
in main(): val=3, val1=5
一样展开成中间文件来看苹果的实现。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
-
- int main(int, const charchar **);
- struct __Block_byref_val_0 {
- voidvoid *__isa;
- __Block_byref_val_0 *__forwarding;
- int __flags;
- int __size;
- int val;
- };
-
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- __Block_byref_val_0 *val;
- __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val;
- printf("in Block():val=%d\n", ++(val->__forwarding->val));}
- static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8
-
- static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8
-
- static struct __main_block_desc_0 {
- unsigned long reserved;
- unsigned long 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 charchar * argv[])
- {
- __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 2};
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, (struct __Block_byref_val_0 *)&val, 570425344);
-
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", (val.__forwarding->val), val1);
- return 0;
- }
中间文件又变长了一些,除去咱们已经了解的部分,咱们能够看到自动变量val再也不是直接加入到__main_block_impl_0里面,而是又变成了一个__Block_byref_val_0的struct结构体的指针。
main函数里面对于val的赋值已经变成了对这样一个数据结构的赋值,第一句上就把2赋给了__Block_byref_val_0里面的val项,而后在blk这个指针初始化的时候,把__Block_byref_val_0的结构体变量指针传入__main_block_impl_0。此后全部对于自动变量val的操做都变成对val.__forwarding->val的操做。这样就解决了Block内外变量同时变化的问题(在操做同一块内存)。
这里还看见__Block_byref_val_0里面有个__forwarding项,这个项是指向自身的一根指针。在blk指针初始化的时候咱们把这个指针的值传入了__main_block_impl_0。
在__main_block_desc_0里面,多出了两个函数指针,分别用于copy和dispose,这两个函数这里也是自动生成的。
5.总结
综合前面的例子来看,Block的实现仍是借助了C语言的函数指针来实现了,对于普通的自动变量,在Block声明时会快照内容存储;对于__block变量,则是生成一个数据结构来存储,而后取代全部访问这个变量的地方。
事实上,由于Block运行时彻底可能自动变量的生命周期已经结束,因此Block对于内存的管理是很复杂的,会把内容从栈上copy到堆上(这也是copy和dispose函数的做用)。因此Block虽然威力巨大,但使用时也须要遵循必定的规则。
iOS中Block介绍(二)内存管理与其余特性
咱们在前一章介绍了block的用法,而正确使用block必需要求正确理解block的内存管理问题。这一章,咱们只陈述结果而不追寻缘由,咱们将在下一章深刻其缘由。
AD:2014WOT全球软件技术峰会北京站 课程视频发布
1、block放在哪里
咱们针对不一样状况来讨论block的存放位置:
1.栈和堆
如下状况中的block位于堆中:
- void foo()
- {
- __block int i = 1024;
- int j = 1;
- void (^blk)(void);
- void (^blkInHeap)(void);
- blk = ^{ printf("%d, %d\n", i, j);};
- blkInHeap = Block_copy(blk);
- }
-
- - (void)fooBar
- {
- _oi = 1;
- OBJ1* oj = self;
- void (^oblk)(void) = ^{ printf("%d\n", oj.oi);};
- void (^oblkInHeap)(void) = [oblk copy];
- }
2.全局区
如下状况中的block位于全局区:
- static int(^maxIntBlock)(int, int) = ^(int a, int b){return a>b?a:b;};
- - (void)fooBar
- {
- int(^maxIntBlockCopied)(int, int) =[maxIntBlock copy];
- }
- void foo()
- {
- int(^maxIntBlockCopied)(int, int) = Block_copy(maxIntBlock);
- }
须要注意的是,这里复制事后的block依旧位于全局区,实际上,复制操做是直接返回了原block对象。
2、block引用的变量在哪里
1.全局区
全局区的变量存储位置与block无关:
- static int gVar = 0;
- void foo()
- {
- static int stackVar = 0;
- }
注意:static变量是不容许添加__block标记的
2.堆栈

此时,你可能会问,当函数foo返回后,栈上的j已经回收,那么blkInHeap怎么能继续使用它?这是由于没有__block标记的变量,会被当作实参传入block的底层实现函数中,当block中的代码被执行时,j已经不是原来的j了,所谓物是人非就是这样吧~
另外,若是使用到变量j的全部block都没有被复制至heap,那么这个变量j也不会被复制至heap。
所以,即便将j++这一句放到blk()这句以前,这段代码执行后,控制台打印结果也是:1024, 1。而不是1024, 2
3、其余特性
1.复制的行为
对block调用复制,有如下几种状况:
1.对全局区的block调用copy,会返回原指针,而且这期间不处理任何东西(至少目前的内部实现是这样);
2.对栈上的block调用copy,每次会返回新复制到堆上的block的指针,同时,全部__block变量都会被复制至堆一份(屡次拷贝,只会生成一份)。
3.对已经位于heap上的block,再次调用copy,只会增长block的引用计数。
为何咱们不讨论retian的行为?缘由是并无Block_retain()这样的函数,并且objc里面的retain消息发送给block对象后,其内部实现是什么都不作。
2.objc类中的block复制
objc类实例方法中的block若是被复制至heap,那么当前实例会被增长引用计数,当这个block被释放时,此实例会被减小引用计数。
但若是这个block没有使用当前实例的任何成员,那么当前实例不会被增长引用计数。这也是很天然的道理,我既然没有用到这个instance的任何东西,那么我干吗要retian它?
咱们要注意的一点是,我看到网上有不少人说block引发了实例与block之间的循环引用(retain-cycle),而且给出解决方案:不直接使用self而先将self赋值给一个临时变量,而后再使用这个临时变量。
可是,你们注意,咱们必定要为这个临时变量增长__block标记(多谢第三篇文章回帖网友的提醒)。
这一章咱们以结果导向的方式来讲明了各类状况下,block的内存问题,下一章,我将剖析运行时库的源码,从根源阐述block的行为。也就是过程导向的方式了。
//Block
// '^'脱字符
//函数指针
int (*p)(int, int) = sum;
//调用函数
sum(3, 4);
p(2 ,4);
//声明一个Block变量
/*1*///int:返回值 ,后面两个int:参数类型
int (^block1) (int, int) = nil;
//block 变量存储的值,是block的实现部分
//实现部分返回值类型常省略
//参数变量名不能省略
//以';'结尾
block1 = ^(int a, int b){
return a + b;
};
//使用block1
int sum = block1(2, 4);
NSLog(@"%d",sum);
/*2*//////////////////////////////////
void (^block2)() = ^(){
NSLog(@"无参数 无返回值的 block2(); ");
};
block2();//调用
//3,求两个数最大值 block
int (^maxValue) (int x,int y) = ^(int a,int b){
return a > b ? a : b;
};
int max = maxValue(2,3);//调用
NSLog(@"最大值:%d",max);
//4, 将字符串转成数字的block
int (^numberFromString)(NSString *) = ^(NSString *str){
return [str intValue];
};
NSLog(@"%d",numberFromString(@"123"));//回调了 ^numberFromString 这个block
//5, 给可变数组排序的block
//5,1
NSComparator sortBlock = ^(id string1, id string2)
{
return [string1 compare:string2];
};
NSMutableArray *stringArray = [NSMutableArray arrayWithObjects:@"a", @"c", @"b", @"d",nil];
[stringArray sortedArrayUsingComparator:sortBlock];
NSLog(@"sortArray:%@", stringArray);
//5,2
[stringArray sortUsingComparator:^NSComparisonResult(id obj1,id obj2){
if ([obj1 intValue] > [obj2 intValue]) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
NSLog(@"--%@",stringArray);
//6, 取别名
typedef int (^SumBlock) (int,int);
SumBlock sumBlock1 = ^(int a,int b){
return a + b;
};
NSLog(@"%d",sumBlock1(7,8));
//7, 求阶乘 的block
int (^jiecheng)(int) = ^(int n){
int m = 1;
for (int i = 1; i <= n; i++) {
m *= i;
}
return m;
};
NSLog(@"阶乘:%d",jiecheng(3));
//8, block内部能够访问外部局部变量,可是不能修改,若要修改需加__block 修饰
__block int count = 2;
void (^sayHi)() = ^(){
count = 3;
NSLog(@"count 被修改为=%d",count);
};
sayHi();
//9,