Block 分析

来源:互联网 发布:公需课挂机软件2017 编辑:程序博客网 时间:2024/06/08 07:05

摘录:http://www.cnblogs.com/goahead-yingjun/articles/4478425.html
参考:http://www.cocoachina.com/bbs/read.php?tid=152222

前言

Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事。需要注意的是由于Objective-C在iOS中不支持GC机制,使用Block必须自己管理内存,而内存管理正是使用Block坑最多的地方,错误的内存管理 要么导致return cycle内存泄漏要么内存被提前释放导致crash。 Block的使用很像函数指针,不过与函数最大的不同是:Block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,Block不仅 实现函数的功能,还能携带函数的执行环境。

可以这样理解,Block其实包含两个部分内容

Block执行的代码,这是在编译的时候已经生成好的;
一个包含Block执行时需要的所有外部变量值的数据结构。 Block将使用到的、作用域附近到的变量的值建立一份快照拷贝到栈上。

Block与函数另一个不同是,Block类似ObjC的对象,可以使用自动释放池管理内存(但Block并不完全等同于ObjC对象)

Block的类型与内存管理

根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。

NSGlobalBlock:类似函数,位于text段(不在堆栈中);
NSStackBlock:位于栈内存,函数返回后Block将无效;
NSMallocBlock:位于堆内存。

1、NSGlobalBlock

block在自己的环路(代码块)中不会抓取任何外部变量,它不需要在在运行的引用其他外包变量,便不会在运行的时候将block压入栈中,而会作为一个NSGlobalBlock编译。它既不是栈也不是堆,而是代码片段的一部分。所以它不管在什么环境下(MRC、ARC)都能正常运行。

如下,我们可以通过是否引用外部变量识别,未引用外部变量即为NSGlobalBlock。

void exampleA() {    //create a NSGlobalBlock    float (^sum)(float, float) = ^(float a, float b){        return a + b;    };    NSLog(@"block is %@", sum); //block is <__NSGlobalBlock__: 0x47d0>}

2.NSStackBlock和NSMallocBlock

如果block有引用外部的局部变量,那么该block便会被压入调用它的函数的栈中,编译器会将其编译为NSStackBlock类型。当其调用它的函数结束时,栈会被清空,同时在栈中的block也会被清除,如果此时有其他函数调用此block便会野指针报错。要解决这个问题,需要手动对block进行copy,通过copy后,并设为自动释放,会将copy分配到堆中,并交由最近释放池负责释放,如:

[[stackBlock copy] autorelease]

但是,在ARC环境下,block会默认分配到堆中,作为一个自动释放的NSMallocBlock(即不受栈被清空的影响)。

代码如下:

typedef void (^dBlock)();dBlock exampleB_getBlock(){    char b = 'B';    void (^block)() = ^{        printf("%c\n", b);    };    NSLog(@"block:%@",block);    dBlock block_copy=[[block copy] autorelease];    NSLog(@"block_copy:%@",block_copy);//    return block_copy;    return block;}void exampleB() {    dBlock block=exampleB_getBlock();    block();}

在MRC环境中,运行后,打印为

block:<__NSStackBlock__: 0xbffbff68>block_copy:<__NSMallocBlock__: 0x78e66430>

说明block经过copy后,被分配到了堆上。
当返回block时,因为是NSStackBlock,所以block();野指针报错。
当返回block_copy时,因为是NSMallocBlock,运行正常。

如果在ARC环境下,block不经过copy,便默认是NSMallocBlock,直接返回block就可以了。

PS

NSMallocBlock只需要对NSStackBlock进行copy操作就可以获取,但是retain操作就不行,会在下面说明
Block的copy、retain、release操作

不同于NSObjec的copy、retain、release操作:

  • Block_copy与copy等效,Block_release与release等效;
  • 对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
  • NSGlobalBlock:retain、copy、release操作都无效;
  • NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry
    addObject:stackBlock],(补:在arc中不用担心此问题,因为arc中会默认将实例化的block拷贝到堆上)在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock
    copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy]
    autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。
  • NSMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;
  • 尽量不要对Block使用retain操作。

Block对外部变量的存取管理

如果block对外部变量做了引用,那他们之间会有什么影响呢?

基本数据类型 的引用

1、局部变量

void exampleC() {    char b = 'B';    NSLog(@"%p,%c",&b,b);    void (^block)() = ^{        NSLog(@"%c",b);    };    block();//修改前    b='C';    NSLog(@"%p,%c",&b,b);    block();//修改后}

打印:

0xbffa8f8b,B0xbffa8f6c,B0xbffa8f8b,C0xbffa8f6c,B

执行后block两次打印结果为两个B,说明,对于基本数据类型的局部变量,当block对其引用的时候,会创建下该变量的一份快照(新建一个同类型的变量,并保存相同值),变量后面值的改变不会影响快照的值,在创建block的时候本质上已经分为两个独立的变量了。
注意:局部变量(无__block修饰)的快照,是只读的,不能在block中修改其值,包括指针变量也不能修改其引用地址。

2.静态变量 全局变量

void exampleC() {    static int num=1;    NSLog(@"%p,%d",&num,num);    void (^block)() = ^{       NSLog(@"%p,%d",&num,num);    };    block();//修改前    num=2;    NSLog(@"%p,%d",&num,num);    block();//修改后}

执行后打印了:

0x647f0,10x647f0,10x647f0,20x647f0,2

变量地址都是一样,说明没有创建快照,而是直接引用,因为全局变量或静态变量在内存中的地址是固定的,Block在读取该变量值的时候是直接从其所在内存读出,所以获取到的是最新值。

3、__block修饰的变量
被__block修饰的变量称作Block变量。 Block变量等效于全局变量、或静态变量。当block位于栈区时,block变量使用的都是相同变量,而当block位于堆区时,block变量一样会创建快照,使用快照代替原变量,具体看下面会讲到。

OC对象 的引用

iOS中,oc对象本质上是一个指向结构体的指针变量。 block对oc对象的引用,其本质是对其指针地址的引用,因此,block引用时,实际上是对指针变量做了一份快照(新创建一个指针变量,指向相同的地址)。所以,获取到的oc对象取决于快照(指针变量)指向的值,如果其指向的值发生变化,block引用到的值自然也跟着变了。下面看例子:

void exampleC() {    NSString *str=[NSString stringWithFormat:@"%@",@"str"];    NSLog(@"%p,%p,%@",&str,str,str);    void (^block)() = ^{        NSLog(@"%p,%p,%@",&str,str,str);        NSLog(@"-----------------");    };    block();//修改前    str=[NSString stringWithFormat:@"%@",@"newStr"];    NSLog(@"%p,%p,%@",&str,str,str);    block();//修改后}

执行后,打印

0xbffd7f84,0x7b7942b0,str0xbffd7f74,0x7b7942b0,str-----------------0xbffd7f84,0x7b896950,newStr0xbffd7f74,0x7b7942b0,str-----------------

从结果看,创建的指针变量str与在block内引用的str快照的变量地址是不同的0xbffd7f84和0xbffd7f74,说明,在block在引用该对象的时候,创建了一个相同类型的oc对象快照(指针变量),保存相同的值,因为是指针,所以值为引用地址0x7b7942b0。当修改str时,其指针指向新对象地址0x7b896950,但快照依然指向了0x7b7942b0,因此还是打印出了str字符串。

如果你硬要问,oc对象的处理方式不是用指针变量来理解,那我就验证一下给你看:

void exampleC() {    char b = 'B';    char *pb=&b;    NSLog(@"%p,%p,%c",&pb,pb,*pb);    void (^block)() = ^{        NSLog(@"%p,%p,%c",&pb,pb,*pb);        NSLog(@"-----------------");    };    block();//修改前    *pb='C';    NSLog(@"%p,%p,%c",&pb,pb,*pb);    block();//修改后}

打印结果:

0xbffe0f84,0xbffe0f8b,B0xbffe0f6c,0xbffe0f8b,B-----------------0xbffe0f84,0xbffe0f8b,C0xbffe0f6c,0xbffe0f8b,C-----------------

验证以上oc对象引用的说法。block的快照就是变量类型相同的是两个变量,保存相同的值,如果是oc对象就引用相同对象(地址)。

依托上面的论证,oc对象变化的时候,如果改变引用对象的地址,block返回到的就一直是最新的值。这种情况在使用oc可变对象的时候会出现,如NSMutableArray、NSMutableString等等。代码如下:

void exampleC() {    NSMutableArray *list=[NSMutableArray arrayWithArray:@[@"a",@"b",@"c"]];//    NSMutableString *mutStr=[NSMutableString stringWithFormat:@"abc"];    NSLog(@"%p,%p,%@",&list,list,list);    void (^block)() = ^{        NSLog(@"%p,%p,%@",&list,list,list);        NSLog(@"-----------------");    };    block();//修改前    //[mutStr appendString:@"123"];    [list removeObjectAtIndex:0];    NSLog(@"%p,%p,%@",&list,list,list);    block();//修改后}

执行后结果为

0xbffcff7c,0x7b063c10,(    a,    b,    c)0xbffcff6c,0x7b063c10,(    a,    b,    c)-----------------0xbffcff7c,0x7b063c10,(    b,    c)0xbffcff6c,0x7b063c10,(    b,    c)-----------------

从打印结果可看出,虽然其快照与外部变量不同,但可变对象在修改后,其引用地址是不会改变,一直都是0x7b063c10,因此,block一直都能引用到最新的值。因为不会改变快照的引用地址,所以,是可以在block中修改可变oc对象的

Block 对 OC对象 的持有

block对于objc对象的内存管理较为复杂,当block被分配到堆区的时候,其对引用到的外部变量的持有也会发生变化,如果处理不好,就会造成循环引用等问题。 这里要分static静态变量、 global全局变量 、local局部变量、 block变量分析,还要区分非arc和arc环境的影响。

  1. MRC环境下
    代码如下:
@interface ViewController : UIViewController{    NSObject *instanceObj;}@endNSString* __globalObj = nil;@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    [self exampleD];}- (void)exampleD{    static NSString *__staticObj;    __globalObj = [[NSString alloc] initWithFormat:@"global"];    instanceObj = [[NSString alloc] initWithFormat:@"instance"];    __staticObj = [[NSString alloc] initWithFormat:@"static"];    NSString* localObj = [[NSString alloc] initWithFormat:@"local"];    __block NSString* blockObj =[[NSString alloc] initWithFormat:@"block"];    NSLog(@"-----------原变量-------------");    NSLog(@"%p,%p,%@",&__globalObj,__globalObj, __globalObj);    NSLog(@"%p,%p,%@",&__staticObj,__staticObj, __staticObj);    NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);    NSLog(@"%p,%p,%@",&localObj,localObj, localObj);    NSLog(@"%p,%p,%@",&blockObj,blockObj, blockObj);    NSLog(@"---------------------------------");    NSLog(@"self:%d",[self retainCount]);    dBlock aBlock = ^{        NSLog(@"%p,%p,%@",&__globalObj,__globalObj, __globalObj);        NSLog(@"%p,%p,%@",&__staticObj,__staticObj, __staticObj);        NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);        NSLog(@"%p,%p,%@",&localObj,localObj, localObj);        NSLog(@"%p,%p,%@",&blockObj,blockObj, blockObj);    };    NSLog(@"----------被block引用后的变量---------");    aBlock();    NSLog(@"--------------------------------");    NSLog(@"self:%d",[self retainCount]);    aBlock = [[aBlock copy] autorelease];    NSLog(@"self:%d",[self retainCount]);    NSLog(@"-------block被分配堆区后引用的变量---------");    aBlock();    NSLog(@"--------------------------------");    NSLog(@"\n-------block被分配堆区后引用的变量计数-------------");    NSLog(@"globalObj:%d", [__globalObj retainCount]);    NSLog(@"staticObj:%d", [__staticObj retainCount]);    NSLog(@"instanceObj:%d", [instanceObj retainCount]);    NSLog(@"localObj:%d", [localObj retainCount]);    NSLog(@"blockObj:%d", [blockObj retainCount]);}@end

运行后,打印

-----------原变量-------------0x2f9d4,0x79066450,global0x2f9d8,0x79062eb0,static0x790e256c,0x79062e90,instance0xbffd2f94,0x790628f0,local0xbffd2f90,0x790e3d20,block---------------------------------self:25----------block引用后的变量---------0x2f9d4,0x79066450,global0x2f9d8,0x79062eb0,static0x790e256c,0x79062e90,instance0xbffd2f60,0x790628f0,local0xbffd2f90,0x790e3d20,block--------------------------------self:25self:26-------block被分配堆区后引用的变量---------2016-04-20 16:06:23.827 BlockTest[3567:234612] 0x2f9d4,0x79066450,global2016-04-20 16:06:23.827 BlockTest[3567:234612] 0x2f9d8,0x79062eb0,static2016-04-20 16:06:23.827 BlockTest[3567:234612] 0x790e256c,0x79062e90,instance2016-04-20 16:06:23.827 BlockTest[3567:234612] 0x78f692e8,0x790628f0,local2016-04-20 16:06:23.827 BlockTest[3567:234612] 0x78f68ab8,0x790e3d20,block---------------------------------------block被分配堆区后引用的变量计数-------------globalObj:1staticObj:1instanceObj:1localObj:2blockObj:1

从结果分析可看出,

  • globalObj和staticObj在内存中的位置是确定的,所以Block
    copy时不会retain对象。而blockObj的作用类型跟globalObj和staticObj相似,也不会retain。
  • localObj在Block copy时,系统自动retain对象,增加其引用计数,所以局部变量,记得release一次。
  • _instanceObj在Block copy时也没有直接retain _instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量。

从内存上讲,在block copy的时候,是重新在堆区开辟一块内存空间,将block保存进去,将NSStackBlock变为NSMallocBlock。
我们都知道,在block引用外包oc的时候,会创建一份oc变量(指针变量)快照,其中只对localObj创建快照,而其他类型是直接使用同一个变量。
为了保证堆区block中的快照(指针变量)指针引用到的localObj和instanceObj有效,避免因栈区清空导致localObj和instanceObj被销毁造成野指针,因此,编译器在copy block的时候,会对其引用到的localObj和instanceObj的self作一次retain。而globalObj、staticObj和blockObj则不存在这种问题。

PS:
blockObj与globalObj、staticObj不同的是,在block未被copy到堆区的时候,在block里blockObj是直接使用的,但是当block被copy时,blockObj其实也会另外做一份快照,blockObj在block copy前后,blockObj变量地址是不同的,只是引用地址相同而已,但是跟局部变量的快照不同,它可以修改其值(引用地址)的。所以其作用是跟globalObj、staticObj一样的。

  1. ARC环境下

在arc环境下,引用外部变量的block会被自动分配到堆区(相当于默认帮我们做了copy)。由于arc中没有retain,retainCount的概念。只有强引用和弱引用的概念。当一个变量没有__strong的指针指向它时,就会被系统释放。因此我们可以通过为变量设置一个week弱引用来检测引用对象,一旦引用对象被释放,week就会为nil, 下面代码来测试:

__weak NSString *globalObj_week;__weak NSString *instanceObj_week;__weak NSString *staticObj_week;__weak NSString *localObj_week;__weak NSString *blockObj_week;- (void)exampleD{    static NSString *__staticObj;    __globalObj = [[NSString alloc] initWithFormat:@"global"];    instanceObj = [[NSString alloc] initWithFormat:@"instance"];    __staticObj = [[NSString alloc] initWithFormat:@"static"];    NSString* localObj = [[NSString alloc] initWithFormat:@"local"];    __block NSString* blockObj =[[NSString alloc] initWithFormat:@"block"];    NSLog(@"-----------原变量-------------");    NSLog(@"%p,%p,%@",&__globalObj,__globalObj, __globalObj);    NSLog(@"%p,%p,%@",&__staticObj,__staticObj, __staticObj);    NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);    NSLog(@"%p,%p,%@",&localObj,localObj, localObj);    NSLog(@"%p,%p,%@",&blockObj,blockObj, blockObj);    NSLog(@"---------------------------------");    globalObj_week=__globalObj;    instanceObj_week=instanceObj;    staticObj_week=__staticObj;    localObj_week=localObj;    blockObj_week=blockObj;    dBlock aBlock = ^{        NSLog(@"%p,%p,%@",&__globalObj,__globalObj, __globalObj);        NSLog(@"%p,%p,%@",&__staticObj,__staticObj, __staticObj);        NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);        NSLog(@"%p,%p,%@",&localObj,localObj, localObj);        NSLog(@"%p,%p,%@",&blockObj,blockObj, blockObj);        NSLog(@"--------------week--------------");        NSLog(@"globalObj_week:%@",globalObj_week);        NSLog(@"instanceObj_week:%@",instanceObj_week);        NSLog(@"staticObj_week:%@",staticObj_week);        NSLog(@"localObj_week:%@",localObj_week);        NSLog(@"blockObj_week:%@",blockObj_week);    };    __globalObj=nil;    instanceObj=nil;    __staticObj=nil;    localObj=nil;    blockObj=nil;    NSLog(@"-------block被分配堆区后引用的变量---------");    aBlock();    NSLog(@"--------------------------------");}

执行结果:

-----------原变量-------------0xee9d8,0x7d06f2d0,global0xee9f0,0x7d06e680,static0x7be7221c,0x7d0706e0,instance0xbff13f90,0x7d06e690,local0xbff13f88,0x7d06f410,block----------------------------------------block被分配堆区后引用的变量---------0xee9d8,0x0,(null)0xee9f0,0x0,(null)0x7be7221c,0x0,(null)0x7be74ed8,0x7d06e690,local0x7be74ef8,0x0,(null)--------------week--------------globalObj_week:(null)instanceObj_week:(null)staticObj_week:(null)localObj_week:localblockObj_week:(null)--------------------------------

从结果可看出,最后只有localObj未被释放掉,说明在block被分配堆区的时候,局部变量的快照变量会对引用对象再做一次强引用。
成员变量instanceObj不做快照不再做强引用,是因为block已经对self做了一次强引用(与MRC同理),这会引发另外一个问题“循环引用”。

PS:
在arc环境下的blockObj,在block分配到堆区的时候跟MRC环境一样会创建一个可修改的快照,但该快照会对blockObj引用的对象作强引用,原变量blockObj销毁(block分配到堆区后,你所使用的变量blockObj其实是新建的快照,其变量地址与原blockObj变量地址是不一样的),所以,在创建block后,blockObj=nil其实操作的是快照,结果就把引用对象销毁了blockObj_week:(null)

注意:
这里对oc对象赋值的时候可不能使用objec=@"objec";的方法赋值,因为@"objec"是常量(不在堆栈),常量是不遵循arc原则的,即创建后就一直存在,不管有多少强引用,它都不会被销毁。如下代码:

    NSString* couObj =@"1";    __weak NSString *wobj=couObj;    couObj=nil;    NSLog(@"%@",wobj);

执行后还是会打印1

Block的循环引用

根据上面的结论, 因为block在拷贝到堆上的时候,会retain(强引用)其引用的外部变量,那么如果block中如果引用了他的宿主对象self(使用其成员变量或者显式使用了self),如果这时self的成员变量或者属性又对block做持有,那就会引起循环引用,如下:

//ARC:myblock为strong//MRC:myblock为copyself.myblock = ^{        [self doSomething];        //或者 NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);  };

或者

//ARC_myblock = ^{        [self doSomething];        //或者 NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);  };
  1. 在MRC环境下

如果self的成员变量或者copy属性对block做了持有,如

@property (nonatomic,copy) dBlock myBlock;dBlock aBlock = ^{        [self doSomething];        NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);    };    self.myBlock=aBlock;    //或者_myBlock=[aBlock copy];

这里,copy属性的myBlock 对block做了持有。说到这,要搞清楚,只有在堆区的block即NSMallocBlock才会对里面的外部对象做持有,对外部对象做持有才有可能出现循环引用的问题,如果这里block是没经过copy的,只是NSStackBlock,NSStackBlock会因栈清空而释放掉,则不存在循环引用问题。所以,这里讨论的block是NSMallocBlock。
这里self的myBlock属性对在堆区的aBlock做了持有,而aBlock中有引用了self或者成员变量,所以aBlock又对self做了持有,造成循环。
要解决这个问题就必须打破循环,我们可以从aBlock中对self或者成员变量的引用入手,让aBlock间接使用self,而不对其做retain,代码做如下修改:

__block typeof(instanceObj) week_instanceObj=instanceObj;__block typeof(self) weekself=self;    dBlock aBlock = ^{        [weekself doSomething];       NSLog(@"%p,%p,%@",&week_instanceObj,week_instanceObj, week_instanceObj);    };

使用__block来间接引用,上面“Block 对 OC对象 的持有”MRC环境分析提过,__block变量在block copy中,使用的是week_instanceObj的快照,该快照引用对象与instanceObj一样,但不做retain,不改变引用计数。

  1. ARC环境下
    以下代码会造成循环引用:
@property (nonatomic,strong) dBlock myBlock;dBlock aBlock = ^{        [self doSomething];        NSLog(@"%p,%p,%@",&instanceObj,instanceObj, instanceObj);    };    self.myBlock=aBlock;    //或者_myBlock=aBlock;

因为arc下,成员变量默认都是strong,引用外部对象的block会自动分配到堆区,为NSMallocBlock。所以,当成员变量或strong属性对blcok做强引用,便形成循环。
要解决循环引用可使用arc中的弱引用weak来间距引用,代码修改:

 __weak NSString* week_instanceObj=instanceObj; __weak typeof(self) weekself = self;    NSLog(@"week_instanceObj:%p,%p,%@",&week_instanceObj,week_instanceObj, week_instanceObj);    dBlock aBlock = ^{        [weekself doSomething];        NSLog(@"week_instanceObj:%p,%p,%@",&week_instanceObj,week_instanceObj, week_instanceObj);    };

这时可不能跟MRC一样时候__block来间距引用,因为__block对象在分配到堆区时,创建出来取代原变量的快照依然会对self做强引用。__weak对象会在block分配到堆区时创建一份快照,该快照对引用对象依然是弱引用。

总结

block类型:取决于block是否引用了外部变量。未引用外部变量的为NSGlobalBlock
NSGlobalBlock:未引用外部变量,不在堆栈中,独立的程序块。
NSStackBlock:引用外部变量 ,位于栈内存,函数返回后Block将无效,存在于MRC环境,通过copy可转为NSMallocBlock分配到堆区,在ARC下自动转换;
NSMallocBlock:NSStackBlock copy得来, 位于堆内存。

对外部变量的引用,除了静态变量 、全局变量、NSStackBlock中的block变量外,会对引用的变量做一份快照,保存创建快照时相同的值,oc对象(指针变量)保存相同引用地址。

关于将block copy到堆区后(或者arc自动copy),对引用的变量的持有变化:
在MRC下,只有局部变量的引用对象计数会+1(快照retain),对成员变量或self的引用也会使self计数+1,__block变量在copy后会取代原变量,但不会再对引用对象做retain。
在ARC下,局部变量会再强引用一次(快照也是强引用),对__block变量不改变强引用个数(快照变量会取代原变量),对成员变量或self的直接引用会使对self再强引用一次。
不论是MRC还是ARC下,当block分配到堆区后,其创建出来的快照的释放转为自动释放,只要block还在,其快照的引用就在。

1 0