对Objective-C中Block的追探

 转自biosli的博客

 

最近看了很多block相关的文章,都在说block怎么用,写的都很精彩。

blogs:

Block编程值得注意的那些事儿 (使用相关)

 

iOS中block实现的探究(内部结构分析)

 

还有绪斌同学共享的(内部结构分析)

 

又看了一本关于这方面的书:Pro Multithreading and Memory Management for iOS and OS X

 

觉得是可以总结一下我对block理解的时候了。

注:上面提供的资料有很多有用的背景知识比如block怎么用,什么时候应该加上__block关键字声明变量,怎么解决循环引用,什么是堆,什么是栈等等,大家写的都比我好,我就不复制粘贴了。下面的文字是我的一些个人理解,如果有不对的地方还请指正。

 

1、block是个什么?

简单说block就是一个“仿”对象。

 

在Objective-C中,类都是继承NSObject的,NSObject里面都会有个isa,是一个objc_class指针。

而block的对象,在clang的C++重写中

  
  
  
  
  1. ^int(){printf("val"); return 111;}; 

这个block会被转化为

  
  
  
  
  1. struct __block_impl { 
  2.   void *isa; 
  3.   int Flags; 
  4.   int Reserved; 
  5.   void *FuncPtr; 
  6. }; 
  7.  
  8. struct __testBlock_block_impl_0 { 
  9.   struct __block_impl impl; 
  10.   struct __testBlock_block_desc_0* Desc; 
  11.   __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) { 
  12.     impl.isa = &_NSConcreteStackBlock; 
  13.     impl.Flags = flags; 
  14.     impl.FuncPtr = fp; 
  15.     Desc = desc; 
  16.   } 
  17. }; 

__testBlock_block_impl_0是block结构,他的第一个属性也是一个结构__block_impl,而第一个参数也是一个isa的指针。

 

在运行时,NSObject和block的isa指针都是指向在对象一个4字节。

 

NSObject及派生类对象的isa指向Class的prototype,而block的isa指向了_NSConcreteStackBlock这个指针。

 

就是说当一个block被声明的时候他都是一个_NSConcreteStackBlock类的对象。

 

2、block对象的生存期

通常在Objective-C中,对象都是在堆上声明的。

当我们运行

  
  
  
  
  1. NSString *str = [[NSString alloc] init]; 

的时候,这个NSString就在堆上挂上号了,直到release的时候,引用计数减为0,这个对象才会被干掉。

 

再看一下block的内部实现,当我们实现

  
  
  
  
  1.       void (^testBlock) (void) =  ^{printf("看看这个block");}; 
  2.       testBlock(); 
  3.   } 

的时候,在clang中会被转换为

  
  
  
  
  1.      void (*testBlock) (void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); 
  2.      ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock); 
  3.  } 

由此可见这个block是在栈上声明的,这就是说当block超过了这个“}”,这个block对象就会被回收。

 

我们做个实验:

Objective-C的源码(非ARC)

  
  
  
  
  1. block stackBlock; 
  2.    { 
  3.        int val = 10; 
  4.        stackBlock = ^{NSLog(@"val = %d", val);}; 
  5.    } 
  6.    stackBlock(); 

转换后:

  
  
  
  
  1. block stackBlock; 
  2.     int val = 10; 
  3.     stackBlock = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val); 
  4. ((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock); 

上面的程序运行,不崩溃。

从这个转换后的结果来看,__main_block_impl_0这个在栈上声明的对象,在“}”结束应该就被释放了,可是在下面的调用中居然还可以用?

我认为这就是运气,个人不推荐在栈释放block对象后再使用block对象。

看看__main_block_impl_0的声明

  
  
  
  
  1. struct __main_block_impl_0 { 
  2.   struct __block_impl impl; 
  3.   struct __main_block_desc_0* Desc; 
  4.   int val; 
  5.   __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) { 
  6.     impl.isa = &_NSConcreteStackBlock; 
  7.     impl.Flags = flags; 
  8.     impl.FuncPtr = fp; 
  9.     Desc = desc; 
  10.   } 
  11. }; 

这个结构只做了构造函数,没有做析构函数,导致在对象弹栈的时候没有对对象内部变量赋值,所以飘在外面的地址都是野指针。

 

(注:clang这个命令不是全靠谱,只能作为参考,因为这个工具转换出来的C++文件无法通过编译,感觉只能作为研究参考)

 

3、block外传、赋值得上栈

咱们看看运气不好的时候:

准备工作,弄个新的iOS工程(非ARC的),然后在ViewController.m里定义一个blk类型。

  
  
  
  
  1. typedef void (^blk) (void); 

之后在弄个属性

 

  
  
  
  
  1. @interface ViewController () { 
  2.       blk tempBlock; 
  3. @end 

在viewDidLoad里面加上一个按钮,并声明一个block指针付给tempBlock

  
  
  
  
  1. - (void)viewDidLoad 
  2.     [superviewDidLoad]; 
  3. // Do any additional setup after loading the view, typically from a nib. 
  4.      
  5.     UIButton *btn = [UIButton buttonWithType: UIButtonTypeRoundedRect]; 
  6.     btn.frame = CGRectMake(100.0f, 100.0f, 100.0f, 30.0f); 
  7.     [btn setTitle: @"试试"forState: UIControlStateNormal]; 
  8.     [btn addTarget: self 
  9.             action: @selector(btnClick:) 
  10.   forControlEvents:UIControlEventTouchUpInside]; 
  11.     [self.viewaddSubview: btn]; 
  12.      
  13.     __blockint val = 10; 
  14.     tempBlock = ^{NSLog(@"val = %d", ++val);}; 

按钮点击的事件:

  
  
  
  
  1. - (void) btnClick: (id) Sender { 
  2.     tempBlock(); 

当页面正常显示之后,点击按钮必然崩溃。提示error: address doesn't contain a section that points to a section in a object file

 

原因就是tempBlock所指向的对象已经被回收了。

 

在Objective-C中,这种情况也可能是tempBlock所指的对象被autorelease了?这样咱们吧tempBlock给retain一下不就好了么?

  
  
  
  
  1. tempBlock = [^{NSLog(@"val = %d", ++val);} retain]; 

结果依旧,可问题出在哪呢?

 

我认为这就是block是一个“仿”对象的造成的,从之前的分析来看,block的isa指向的不是object_class,而是_NSConcreteStackBlock,我想这个prototype里重新定义了咱们熟悉的retain/copy/release等NSObject所定义的函数。

 

retain这个函数在_NSConcreteStackBlock这个类的定义中,不会对指针做任何操作,所以才不会有影响。(同样release也是一样)

 

在block的各种使用说明中,都有一条,当block要作为参数外传、赋值时都要调用copy,咱们对比一下copy前后block的变化。

 

把刚才的实验改造一下:

  
  
  
  
  1. - (void)viewDidLoad 
  2.     [superviewDidLoad]; 
  3.  
  4.      //生成按钮(略) 
  5.      
  6.     NSLog(@"_NSConcreteStackBlock %@", [_NSConcreteStackBlock class]); 
  7.      
  8.     __block int val = 10; 
  9.     blk stackBlock = ^{NSLog(@"val = %d", ++val);}; 
  10.     NSLog(@"stackBlock: %@", stackBlock); 
  11.      
  12.     tempBlock = [stackBlock copy]; 
  13.     NSLog(@"tempBlock: %@", tempBlock); 

打印出的结果:

  
  
  
  
  1. 2013-05-29 14:21:09.969 BlockTest[2070:c07] _NSConcreteStackBlock __NSStackBlock__ 
  2. 2013-05-29 14:21:09.970 BlockTest[2070:c07] stackBlock: <__NSStackBlock__: 0xbfffdb28> 
  3. 2013-05-29 14:21:09.970 BlockTest[2070:c07] tempBlock: <__NSMallocBlock__: 0x756bf20> 

在经过copy之后,对象的类型从__NSStackBlock__变为了__NSMallocBlock__

 

在Objective-C的设计中,我没见过copy一回把对象的类型也给变了的,再次说明了block是一种特殊的对象。

 

大家应该注意到__block标记的变量了吧,这个变量会随着block对象上堆一块上堆,这个部分上面的blogs和书中都有讲解,我就不叙述了。

 

另外还有一种类型block的类型__NSGlobalBlock__,当block里面没有局部变量的时候会block会变为这个类型,这个类型的retain/copy/release全都不会对对象有影响,可以当做静态block理解。

 

__NSMallocBlock__对象再次copy,不会再产生新的对象而是对原有对象进行retain。

经过实验几个block类型的retain/copy/release的功能如下(非ARC环境):

 4、事情还没完

当一个block对象上堆了,他的声明周期就和一个普通的NSObject对象的方法一样了(这个应该是__NSMallocBlock__这个类的设计参考了NSObject对象的设计)

 

作为一个合格的Objective-C程序员,见到copy应该就想到release。

 

在非ARC环境下,copy了block后一定要在使用后release,不然会有内存泄露,而且泄露点是在系统级,在Instruments里跟不到问题触发点,比较上火。

我在这里想探讨的另外一个问题是设计原则,对于一个对象,当外传的时候我们都会想着把对象autorelease掉,比如:

  
  
  
  
  1. - (NSArray *) myTestArray { 
  2.     NSArray *array = [[NSArray alloc] initWithObjects: @"a", @"b", @"c", nil]; 
  3.     return [array autorelease]; 

同样,我们在向外传递block的时候一定也要做到,传给外面一个在堆上的,autorelease的对象。

  
  
  
  
  1. - (blk) myTestBlock { 
  2.     __blockint val = 10; 
  3.     blk stackBlock = ^{NSLog(@"val = %d", ++val);}; 
  4.     return [[stackBlock copy] autorelease]; 

第一步,copy将block上从栈放到堆上,第二步,autorelease防止内存泄露。

 

同样,有时我们会去将block放到别的类中做回调,如放到AFNetworking中的回调。这时根据统一的设计原则,我们也应该给调用对象一个堆上的autorelease的对象。

 

总之,在把block对象外传的时候,我们要传出一个经过copy,再autorelease的block在堆上的__NSMallocBlock__对象。(个人观点,block是模仿NSObject对象发明的,就不要让调用方做与其他对象不一样的事)

 

5、说说ARC

上面的这些方法,说的都是非ARC编程的时候的注意事项,在ARC下很多规则都可以省略了。

因为在ARC下有个原则,只要block在strong指针底下过一道都会放到堆上。

看下面这个实验:

  
  
  
  
  1.         __blockint val = 10; 
  2.         __strong blk strongPointerBlock = ^{NSLog(@"val = %d", ++val);}; 
  3.         NSLog(@"strongPointerBlock: %@", strongPointerBlock); //1 
  4.          
  5.         __weak blk weakPointerBlock = ^{NSLog(@"val = %d", ++val);}; 
  6.         NSLog(@"weakPointerBlock: %@", weakPointerBlock); //2 
  7.          
  8.         NSLog(@"mallocBlock: %@", [weakPointerBlock copy]); //3 
  9.          
  10.         NSLog(@"test %@", ^{NSLog(@"val = %d", ++val);}); //4 
  11.     } 

得到的日志

  
  
  
  
  1. 2013-05-29 16:03:58.773 BlockTest[3482:c07] strongPointerBlock: <__NSMallocBlock__: 0x7625120> 
  2. 2013-05-29 16:03:58.776 BlockTest[3482:c07] weakPointerBlock: <__NSStackBlock__: 0xbfffdb30> 
  3. 2013-05-29 16:03:58.776 BlockTest[3482:c07] mallocBlock: <__NSMallocBlock__: 0x714ce60> 
  4. 2013-05-29 16:03:58.777 BlockTest[3482:c07] test <__NSStackBlock__: 0xbfffdb18> 

分析一下:

strong指针指向的block已经放到堆上了。

 

weak指针指向的block还在栈上(这种声明方法只在block上有效,正常的weak指针指向堆上对象,直接就会变nil,需要用strong指针过一道,请参考ARC的指针使用注意事项)。

 

第三行日志同非ARC一样,会将block从栈移动到堆上。

 

最后一行日志,说明在单独声明block的时候,block还是会在栈上的。

 

在ARC下的另外一种情况,将block作为参数返回

  
  
  
  
  1. - (__unsafe_unretained blk) blockTest { 
  2.     int val = 11; 
  3.     return ^{NSLog(@"val = %d", val);}; 

调用方

  
  
  
  
  1. NSLog(@"block return from function: %@", [self blockTest]); 

得到的日志:

  
  
  
  
  1. 2013-05-29 16:09:59.489 BlockTest[3597:c07] block return from function: <__NSMallocBlock__: 0x7685640> 

分析一下:

在ARC环境下,当block作为参数返回的时候,block也会自动被移到堆上。

 

在ARC下,只要指针过一下strong指针,或者由函数返回都会把block移动到堆上。

所以在将block传给回调方之前过一下strong指针,就可以满足我刚才阐述的设计原则。

 

总结:

上面的文字介绍了:

1、block在Objective-C环境下的结构

     block是一个“仿”对象

2、block声明的生存期

    栈上声明对象是会被回收的,如果要长期持有block对象请把她移到堆上

3、从栈到堆的转换时机

    栈上的block什么时候会在执行copy的时候移动到堆上,block可以有三种类型

4、我个人理解的一些设计准则

    给调用方一个堆上的,被autorelease的block对象。

5、在ARC下的一些注意事项

    过一下strong指针,他好,我也好。

 

你可能感兴趣的:(Objective-C,block)