iOS中block的探究(转)


查看完整版本: [-- iOS中block的探究 --]

CocoaChina 开发讨论区 -> iPhone开发 / iPad开发 一般讨论区 -> iOS中block的探究 [打印本页]登录 -> 注册 -> 回复主题 -> 发表主题

casual04022012-07-17 20:45

iOS中block的探究

/* ---------------------------------------------------------------------------------------------------- */
[0. Brief introduction of block]


Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。
用维基百科的话来说,Block是Apple Inc.为C、C++以及Objective-C添加的特性,使得这些语言可以用类lambda表达式的语法来创建闭包
用Apple文档的话来说,A block is an anonymous inline collection of code, and sometimes also called a "closure".
关于闭包,我觉得阮一峰的一句话解释简洁明了:闭包就是能够读取其它函数内部变量的函数
这个解释用到block来也很恰当:一个函数里定义了个block,这个block可以访问该函数的内部变量。
一个简单的Block示例如下:
int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; };
如果用Python的lambda表达式来写,可以写成如下形式:
f = lambda x, y : x if x > y else y
不过由于Python自身的语言特性,在def定义的函数体中,可以很自然地再用def语句定义内嵌函数,因为这些函数本质上都是对象。
如果用BNF来表示block的上下文无关文法,大致如下:
block_expression  ::=  ^  block_declare  block_statement
block_declare  ::=  block_return_type  block_argument_list
block_return_type ::=  return_type  |  空
block_argument_list  ::=  argument_list  |  空


/* ---------------------------------------------------------------------------------------------------- */
[1. Why block]
Block除了能够定义参数列表、返回类型外,还能够获取被定义时的词法范围内的状态(比如局部变量),并且在一定条件下(比如使用__block变量)能够修改这些状态。此外,这些可修改的状态在相同词法范围内的多个block之间是共享的,即便出了该词法范围(比如栈展开,出了作用域),仍可以继续共享或者修改这些状态。
通常来说,block都是一些简短代码片段的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调。
比如我们可以在遍历NSArray时做一些事情:

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
其中将stop设为YES,就跳出循环,不继续遍历了。
而在很多框架中,block越来越经常被用作回调函数,取代传统的回调方式。
  • 用block作为回调函数,可以使得程序员在写代码更顺畅,不用中途跑到另一个地方写一个回调函数,有时还要考虑这个回调函数放在哪里比较合适。采用block,可以在调用函数时直接写后续处理代码,将其作为参数传递过去,供其任务执行结束时回调。
  • 另一个好处,就是采用block作为回调,可以直接访问局部变量。比如我要在一批用户中修改一个用户的name,修改完成后通过回调更新对应用户的单元格UI。这时候我需要知道对应用户单元格的index,如果采用传统回调方式,要嘛需要将index带过去,回调时再回传过来;要嘛通过外部作用域记录当前操作单元格的index(这限制了一次只能修改一个用户的name);要嘛遍历找到对应用户。而使用block,则可以直接访问单元格的index。

这份文档中提到block的几种适用场合:
  • 任务完成时回调
  • 处理 消息监听回调处理
  • 错误回调处理
  • 枚举回调
  • 视图动画、变换
  • 排序


/* ---------------------------------------------------------------------------------------------------- */
[2. About __block_impl]
Clang提供了中间代码展示的选项供我们进一步了解block的原理。
以一段很简单的代码为例:


使用-rewrite-objc选项编译:


得到一份block0.cpp文件,在这份文件中可以看到如下代码片段:


从命名可以看出这是block的实现,并且得知block在Clang编译器前端得到实现,可以生成C中间代码。很多语言都可以只实现编译器前端,生成C中间代码,然后利用现有的很多C编译器后端。
从结构体的成员可以看出,Flags、Reserved可以先略过,isa指针表明了block可以是一个NSObject,而FuncPtr指针显然是block对应的函数指针。
由此,揭开了block的神秘面纱。
不过,block相关的变量放哪里呢?上面提到block可以capture词法范围内(或者说是外层上下文、作用域)的状态,即便是出了该范围,仍然可以修改这些状态。这是如何做到的呢?


/* ---------------------------------------------------------------------------------------------------- */
[3. Implementation of a simple block]
先看一个只输出一句话的block是怎么样的。


生成中间代码,得到片段如下:


首先出现的结构体就是__main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。如果是全局block,就根据变量名和出现序列进行命名。__main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是__block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。
接着出现的是__main_block_func_0函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
再下面是__main_block_desc_0结构体,其中比较有价值的信息是block大小。
最后就是main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
这里,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。同样地,还有_NSConcreteMallocBlock_NSConcreteGlobalBlock
由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。或者使用Block_copy()和Block_release()。


/* ---------------------------------------------------------------------------------------------------- */
[4. Capture local variable]
再看一个访问局部变量的block是怎样的。


生成中间代码,得到片段如下:


可以看出这次的block结构体__main_block_impl_0多了个成员变量i,用来存储使用到的局部变量i(值为1024);并且此时可以看到__cself参数的作用,类似C++中的this和Objective-C的self。
如果我们尝试修改局部变量i,则会得到如下错误:


错误信息很详细,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。
为什么不能给变量i赋值呢?
因为main函数中的局部变量i和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量i还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就……。
所以,对于auto类型的局部变量,不允许block进行修改是合理的。


/* ---------------------------------------------------------------------------------------------------- */
[5. Modify static local variable]
于是我们也可以推断出,静态局部变量是如何在block执行体中被修改的——通过指针。
因为静态局部变量存在于数据段中,不存在栈展开后非法访存的风险。


上面中间代码片段与前一个片段的差别主要在于main函数里传递的是i的地址(&i,以及__main_block_impl_0结构体中成员i变成指针类型(int *)。
然后在执行block时,通过指针修改值。
当然,全局变量、静态全局变量都可以在block执行体内被修改。更准确地讲,block可以修改它被调用(这里是__main_block_func_0)时所处作用域内的变量。比如一个block作为成员变量时,它也可以访问同一个对象里的其它成员变量。


/* ---------------------------------------------------------------------------------------------------- */
[6. Implementation of __block variable]
那么,__block类型变量是如何支持修改的呢?


我们为int类型变量加上__block指示符,使得变量i可以在block函数体中被修改。
此时再看中间代码,会多出很多信息。首先是__block变量对应的结构体:


由第一个成员__isa指针也可以知道__Block_byref_i_0也可以是NSObject。
第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个__Block_byref_i_0结构。
最后一个成员是目标存储变量i。
此时,__main_block_impl_0结构如下:


__main_block_impl_0的成员变量i变成了__Block_byref_i_0 *类型。
对应的函数__main_block_func_0如下:


亮点是__Block_byref_i_0指针类型变量i,通过其成员变量__forwarding指针来操作另一个成员变量。 :-)
而main函数如下:


通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:__Block_byref_i_0类型变量i仍然处于栈上,当block被回调执行时,变量i所在的栈已经被展开,怎么办?
在这种关键时刻,__main_block_desc_0站出来了:


此时,__main_block_desc_0多了两个成员函数:copy和dispose,分别指向__main_block_copy_0__main_block_dispose_0
当block从栈上被copy到堆上时,会调用__main_block_copy_0将__block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。


/* ---------------------------------------------------------------------------------------------------- */
本来还想继续写下去,结果发现文章有点长了。先到此。
原文链接:http://blog.csdn.net/jasonblog/article/details/7756763
Jason Lee @ Hangzhou






casual04022012-07-17 20:47
由于在坛里面获益不少,所以也将学习小结发出来分享下。
如果有什么遗漏错误地方,望不吝赐教。

xzgyb2012-07-18 08:58
多谢分享,学习!

kong6wu6wu2012-07-18 10:00
多谢分享啊 ,高深啊

longisnow2012-07-18 10:06
闭包就是能够读取其它函数内部变量的函数,这句话说的很好

solidbrain2012-07-19 10:25
block 很有意思 向前辈学习了

raphaelrong2012-07-20 17:22
原来还有这么多讲究,我原来只是当作简单的异步调用手段来理解的……从没这么深入的思考过

casual04022012-08-03 15:18
我也是出于个人兴趣了解下

casual04022012-08-03 15:18
  我不算什么前辈的……囧 也搞iOS开发没多久

安全问题是啥2012-08-04 21:53
和Delphi的内联函数很像,是不是可以这么理解?

whtoo2012-08-05 11:27
这玩意儿 从本质上讲 就是搞作用域封闭和回调时最大作用域访问权限 如果说 block完全下文无关 也不能这么讲 因为 用的时候 大家还是会给他一个求职环境 那个时候还是有上下文的啊

vincentiss2012-08-09 09:46
看不懂。。。惭愧

leinzhou2012-08-15 11:22
现在看有点深入了,回帖支持

xiaolanlianhua2012-08-31 19:54
使用block,则可以直接访问单元格的index,楼主能给个例子吗?觉得不可以吧,求交流!

liumingl2012-09-02 06:49
不错,正需要的东西。

guangzhi_4052012-09-03 18:04
学习了~

developers2012-09-17 15:36
block,mark一下

mark_dark2012-10-12 09:35
嗯,看懂了,嘿嘿

leverkusen1882012-10-12 16:36
关于block使用局部变量方面,我这边补充几点。

NSObject* obj = [[NSObject alloc] init];
    test = ^(void) {
        NSLog(@"i=====%@", obj);
    };
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), test);
    NSLog(@"count=%i", [obj retainCount]);
    [obj release];

你会发现obj的retainCount变成2了。
但如果把代码位置换一下。
NSObject* obj = [[NSObject alloc] init];
    test = ^(void) {
        NSLog(@"i=====%@", obj);
    };
    [obj release];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), test);
这样会产生访问已销毁对象的错误。

因此在调用 使用局部变量的block时,要格外小心。
而绝对禁止在一个作用域,使用一个在另一个作用域初始化,且使用了局部变量的block,例如:

void (^test)(void);   //全局block变量

//在这个函数内初始化了block
-(void)haha  {
    int i = 444;
    test = ^(void) {
        NSLog(@"i=====%i", i);
    };
}

//在这个函数内使用了该block
-(void)haha1 {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), test);
}

sunzhe2012-10-12 16:41
block 还是不会啊  

sagexy2012-10-15 11:51
mark 学习一下

zzontheway2012-12-13 14:19
goooooooood

ahopedog2012-12-13 15:07
非常好,详尽的文章,谢谢

第四个苹果2013-01-07 14:18
为什么我看不太懂啊!

mahui2013-07-16 15:05
Clang提供了中间代码,可否问一下,是怎么查看到的?

初一什么2013-10-20 15:36
用户被禁言,该主题自动屏蔽!


查看完整版本: [-- iOS中block的探究 --] [-- top --]



Powered by PHPWind v7.5 SP3 Code ©2003-2010 PHPWind 
Gzip disabled

You can contact us

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值