对Objective-C中Block的追探

对Objective-C中Block的追探

http://www.cnblogs.com/biosli/archive/2013/05/29/iOS_Objective-C_Block.html

最近看了很多block相关的文章,都在说block怎么用,写的都很精彩。
blogs:
Block编程值得注意的那些事儿 (使用相关)
http://www.cocoachina.com/macdev/cocoa/2013/0527/6285.html
iOS中block实现的探究(内部结构分析)
http://blog.csdn.net/jasonblog/article/details/7756763?reload
还有绪斌同学共享的(内部结构分析)
https://www.evernote.com/shard/s269/sh/23b61393-c6dd-4fa2-b7ae-306e9e7c9639/131de66a3257122ba903b0799d36c04c?noteKey=131de66a3257122ba903b0799d36c04c&noteGuid=23b61393-c6dd-4fa2-b7ae-306e9e7c9639
又看了一本关于这方面的书:
Pro Multithreading and Memory Management for iOS and OS X
http://vdisk.weibo.com/s/9hjAV

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

1、block是个什么?
简单说block就是一个“仿”对象。

在Objective-C中,类都是继承NSObject的,NSObject里面都会有个isa,是一个objc_class指针。
而block的对象,在clang的C++重写中,

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

这个block会被转化为

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__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中,对象都是在堆上声明的。
当我们运行

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

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

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

    {
        void (^testBlock) (void) =  ^{printf("看看这个block");};
        testBlock();
    }

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

    {
        void (*testBlock) (void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    }

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

我们做个实验:
Objective-C的源码(非ARC)

    block stackBlock;
    {
        int val = 10;
        stackBlock = ^{NSLog(@"val = %d", val);};
    }
    stackBlock();

转换后:

    block stackBlock;
    {
        int val = 10;
        stackBlock = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
    }
    ((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);

上面的程序运行,不崩溃。
从这个转换后的结果来看,__main_block_impl_0这个在栈上声明的对象,在“}”结束应该就被释放了,可是在下面的调用中居然还可以用?
我认为这就是运气,个人不推荐在栈释放block对象后再使用block对象。
看看__main_block_impl_0的声明

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *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;
  }
};

这个结构只做了构造函数,没有做析构函数,导致在对象弹栈的时候没有对对象内部变量赋值,所以飘在外面的地址都是野指针。
(注:clang这个命令不是全靠谱,只能作为参考,因为这个工具转换出来的C++文件无法通过编译,感觉只能作为研究参考)

3、block外传、赋值得上栈
咱们看看运气不好的时候:
准备工作,弄个新的iOS工程(非ARC的),然后在ViewController.m里定义一个blk类型。

typedef void (^blk) (void);

之后在弄个属性

@interface ViewController () {
      blk tempBlock;
}
@end

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

- (void)viewDidLoad
{
    [superviewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

    UIButton *btn = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    btn.frame = CGRectMake(100.0f, 100.0f, 100.0f, 30.0f);
    [btn setTitle: @"试试"forState: UIControlStateNormal];
    [btn addTarget: self
            action: @selector(btnClick:)
  forControlEvents:UIControlEventTouchUpInside];
    [self.viewaddSubview: btn];

    __blockint val = 10;
    tempBlock = ^{NSLog(@"val = %d", ++val);};
}

按钮点击的事件:

- (void) btnClick: (id) Sender {
    tempBlock();
}

当页面正常显示之后,点击按钮必然崩溃。
提示error: address doesn‘t contain a section that points to a section in a object file
原因就是tempBlock所指向的对象已经被回收了。
在Objective-C中,这种情况也可能是tempBlock所指的对象被autorelease了?
这样咱们吧tempBlock给retain一下不就好了么?

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的变化。
把刚才的实验改造一下:

- (void)viewDidLoad
{
    [superviewDidLoad];

     //生成按钮(略)

    NSLog(@"_NSConcreteStackBlock %@", [_NSConcreteStackBlock class]);

    __block int val = 10;
    blk stackBlock = ^{NSLog(@"val = %d", ++val);};
    NSLog(@"stackBlock: %@", stackBlock);

    tempBlock = [stackBlock copy];
    NSLog(@"tempBlock: %@", tempBlock);
}

打印出的结果:

2013-05-29 14:21:09.969 BlockTest[2070:c07] _NSConcreteStackBlock __NSStackBlock__
2013-05-29 14:21:09.970 BlockTest[2070:c07] stackBlock: <__NSStackBlock__: 0xbfffdb28>
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掉,比如:

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

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

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

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

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

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

5、说说ARC
上面的这些方法,说的都是非ARC编程的时候的注意事项,在ARC下很多规则都可以省略了。
因为在ARC下有个原则,只要block在strong指针底下过一道都会放到堆上。
看下面这个实验:

    {
        __blockint val = 10;
        __strong blk strongPointerBlock = ^{NSLog(@"val = %d", ++val);};
        NSLog(@"strongPointerBlock: %@", strongPointerBlock); //1

        __weak blk weakPointerBlock = ^{NSLog(@"val = %d", ++val);};
        NSLog(@"weakPointerBlock: %@", weakPointerBlock); //2

        NSLog(@"mallocBlock: %@", [weakPointerBlock copy]); //3

        NSLog(@"test %@", ^{NSLog(@"val = %d", ++val);}); //4
    }

得到的日志

2013-05-29 16:03:58.773 BlockTest[3482:c07] strongPointerBlock: <__NSMallocBlock__: 0x7625120>
2013-05-29 16:03:58.776 BlockTest[3482:c07] weakPointerBlock: <__NSStackBlock__: 0xbfffdb30>
2013-05-29 16:03:58.776 BlockTest[3482:c07] mallocBlock: <__NSMallocBlock__: 0x714ce60>
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作为参数返回

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

调用方

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

得到的日志:

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指针,他好,我也好。

Copyright ©2015 摇滚诗人

时间: 2024-10-13 00:01:10

对Objective-C中Block的追探的相关文章

Block存储域探析

接<Block截获自动变量实现与__block修饰符内部实现>我们继续探讨Block 留下的问题 1,__Block_byref_i_0 *__forwarding;这个指向自身的指针是什么鬼,有什么作用,什么时候用? 2,Desc_0结构体中多出来的void (*copy) void (*dispose)这两个方法有什么作用,什么时候用? Block 和 __block变量的实质 名称 实质 Block 栈上Block的结构体实例 __block变量 栈上__block变量的结构体实例 Bl

iOS中block类型大全

iOS中block类型大全 typedef的block 作为属性的block 作为变量的block 作为方法变量入参的block 作为方法参数的block 无名block 内联函数的block 递归调用的block 作为方法返回值的block 作为函数名的block(太过奇葩,完全不知道怎么用-_-!) iOS中block类型大全,码迷,mamicode.com

iOS中block的用法 以及和函数用法的区别

ios中block的用法和函数的用法大致相同 但是block的用法的灵活性更高: 不带参数的block: void ^(MyBlock)() = ^{}; 调用的时候  MyBlock(); 带参数的block: int ^(MyBlock)(int,int) = ^(int a,int b){return a+b;} 调用MyBlock(5,6); 将block当作某个类的属性的写法 typedef void (^BlockOption)(); @property (nonatomic,ass

ARC中block块作为属性的使用笔记

ARC中block块作为属性的使用笔记 block较难理解,根据在内存中的分布情况就分为3种类型,根据使用的情形又分为很多很多种.虽然用起来容易,但使用不当会造成内存泄露,虽然都是这么说,但你真的研究过为什么会泄露吗?为什么有些时候外部变量进入block的时候会导致引用计数+1呢? 本人做过MRC以及ARC的开发,但大势所趋,ARC将是以后开发的主要模式,即使有MRC也是ARC混编MRC的代码,所以,本文的block的一些使用上的心得都基于ARC的,已经不考虑MRC的了,请看官注意,MRC与AR

IOS中Block的循环引用

@interface DemoObj() @property (nonatomic, strong) NSOperationQueue *queue; @end @implementation DemoObj - (instancetype)init { self = [super init]; if (self) { self.queue = [[NSOperationQueue alloc] init]; } return self; } - (void)dealloc { NSLog(@"

iOS 中Block以及Blocks的使用

一.ios中block的使用 Block可以帮助我们组织独立的代码段,并提高复用性和可读性.iOS4在UIKit中引入了该特征.超过100个的Apple API都使用了Block,所以这是一个我们必须开始熟悉的知识. Block是什么样的? 你可以使用^操作符来声明一个Block变量,它表示一个Block的开始. int num1 = 7; int(^aBlock)(int) = ^(int num2) { return num1+nunm2; }; 在如上代码中我们将Block声明为一个变量,

iOS AFNetWorking中block执行完后再执行其它操作

需求:同时进行两次网络请求,网络请求是异步的,在网络请求成功后进行其它的操作.两个网络请求是这样,一个网络请求中block执行完之后,再进行其它操作,也是一样的原理,只是这时候不需要线程组了,只需要信号量.当然也适用于所有的block. 接下来就说下,在两次异步请求之后要做的操作. 利用线程组和信号量来完成,看代码 - (void)getData { NSString *appIdKey = @"8781e4ef1c73ff20a180d3d7a42a8c04"; NSString*

objective C中的字符串(三)

holydancer原创,如需转载,请在显要位置注明: 转自holydancer的CSDN专栏,原文地址:http://blog.csdn.net/holydancer/article/details/7343561 objective C中的字符串操作 在OC中创建字符串时,一般不使用C的方法,因为C将字符串作为字符数组,所以在操作时会有很多不方便的地方,在Cocoa中NSString集成的一些方法,可以很方便的操作字符串,下面举几个例子: 1.创建: 直接利用等号赋值 NSString *

iOS中block用法之两个界面传值问题

Block的使用有很多方面,其中传值只是它的一小部分,但是很常用更实用,下面介绍Block在两个界面之间的传值用法: 先说一下思想: 首先,创建两个视图控制器,在第一个视图控制器中创建一个Label和一个Button,其中Label是为了显示第二个视图控制器传过来的字符串, Button是为了push到第二个界面. 第二个界面的只创建一个TextField,是为了输入文字,当输入文字并且返回第一个界面的时候(第二个视图将要消失的时候),就将这个 TextFiled中的文字传给第一个界面并且显示在