OC高级编程——深入block,如何捕获变量,如何存储在堆上

 

首先先看几道block相关的题目

这是一篇比较长的  博文 ,前部分是block的测试题目,中间是block的语法、特性,block讲解block内部实现和block存储位置,请读者耐心阅读。  具备block基础的同学,直接调转到block的实现

下面列出了五道题,看看能否答对两三个。主要涉及block栈上、还是堆上、怎么捕获变量。  答案在博文最后一行

//-----------第一道题:--------------
void exampleA() {
char a = 'A';
^{ printf("%c\n", a);};
}
A.始终能够正常运行 B.只有在使用ARC的情况下才能正常运行
C.不使用ARC才能正常运行 D.永远无法正常运行
//-----------第二道题:答案同第一题--------------
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{printf("%c\n", b);}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
//-----------第三道题:答案同第一题--------------
void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{printf("C\n");}];
}
void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
//-----------第四道题:答案同第一题--------------
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{printf("%c\n", d);};
}
void exampleD() {
exampleD_getBlock()();
}
//-----------第五道题:答案同第一题--------------
typedef void (^eBlock)();
eBlock exampleE_getBlock() {
char e = 'E';
void (^block)() = ^{printf("%c\n", e);};
return block;
}
void exampleE() {
eBlock block = exampleE_getBlock();
block();
}

注:以上题目摘自:CocoaChina论坛  点击打开链接

block概要

什么是block

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。  命名就是工作的本质,函数名、变量名、方法名、属性名、类名和框架名都必须具备。而能够编写不带名称的函数对程序员来说相当有吸引力。

例如:我们要进行一个URL的请求。那么请求结果以何种方式通知调用者呢?通常是经过代理(delegate)但是,写delegate本身就是成本,我们需要写类、方法等等。

这时候,我们就用到了block。block提供了类似由C++和OC类生成实例或对象来保持变量值的方法。像这样使用block可以不声明C++和OC类,也没有使用静态变量、静态全局变量或全局变量,  仅用编写C语言函数的源码量即可使用带有自动变量值的匿名函数。

其他语言中也有block概念。

block的实现

block的语法看上去好像很特别,但实际上是作为极为普通的C语言代码来处理的。这里我们借住clang编译器的能力:具有转化为我们可读源代码的能力。

控制台命令是:  clang -rewrite-objc 源代码文件名。

int main(){
void (^blk)(void) = ^{printf("block\n");};
blk();
return 0;
}

经过  clang -rewrite-objc  之后,代码编程这样了(简化后代码,读者可以搜索关键字在生成文件中查找):

struct __block_impl{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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)}; struct __main_block_impl_0{
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
}
static struct __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("block\n");
}
int main(){
struct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
(*blk->impl.FuncPtr)(blk);
}

很多结构体,很多下划线的变量和函数名。我们一个个来:

__block_impl

:更像一个block的基类,所有block都具备这些字段。

__main_block_impl_0

:block变量。

__main_block_func_0

:虽然,block叫,匿名函数。但是,这个函数还是被编译器起了个名字。

__main_block_desc_0

:block的描述,注意,他有一个实例__main_block_desc_0_DATA

上述命名是有规则的:main是block所在函数的名字,后缀0则是这个函数中的第0个block。由于上面是C++的代码,可以将__main_block_impl_0的结构体总结一下,得到如下形式:

__main_block_impl_0{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;
}

总结:所谓block就是Objective-C的对象

截获自动变量值

int val = 10;
void (^blk)(void) = ^{printf("val=%d\n",val);};
val = 2;
blk();

上面这段代码,输出值是:val = 10.而不是2.  block截获自动变量的瞬时值 。因为block保存了自动变量的值,所以在执行block语法后,即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。

尝试改写block中捕获的自动变量,将会是编译错误。我更喜欢把这个理解为:block捕获的自动变量都将转化为const类型。不可修改了

解决办法是将自动变量添加修饰符 __block;那么如果截获的自动变量是OC对象呢

^{[array addObject:obj];};

这么写是没有问题的,因为array是一个指针,我们并没有改变指针的值。这个也可以解释下面的问题

const char text[] = "hello";

这样会编译错误。为何?

这是因为捕获自动变量的方法并没有实现C语言数组类型

。可以通过指针代替:const char *text= "hello";

那么这个block的对象结构是什么样呢,请看下面:

__main_block_impl_0{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;
int val;
}

这个val是如何传递到block结构体中的呢?

int main(){
struct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,val);
}

注意函数调用最后一个参数,即val参数。

那么函数调用的代码页转化为下面这样了.这里的cself跟C++的this和OC的self一样。

static struct __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("val=%d\n",__cself-val);
}

所以,block捕获变量更像是:函数按值传递。

__block说明符

前面讲过block所在函数中的,捕获自动变量。但是不能修改它,不然就是编译错误。但是可以改变全局变量、静态变量、全局静态变量。

其实这两个特点不难理解:  第一、为何不让修改变量:这个是编译器决定的。理论上当然可以修改变量了,只不过block捕获的是自动变量的副本,名字一样。为了不给开发者迷惑,干脆不让赋值。道理有点像:函数参数,要用指针,不然传递的是副本。

第二、可以修改静态变量的值。静态变量属于类的,不是某一个变量。所以block内部不用调用cself指针。所以block可以调用。

解决block不能保存值这一问题的另外一个办法是使用__block修饰符。

__block int val = 10;
void (^blk)(void) = ^{val = 1;};

该源码转化后如下:

struct __block_byref_val_0{
void *__isa;
__block_byref_val_0 *__forwarding;
int _flags;
int __size;
int val;
}

__main_block_impl_0中自然多了__block_byreg_val_0的一个字段。注意:__block_byref_val_0结构体中有自身的指针对象,难道要

_block int val = 10;这一行代码,转化成了下面的结构体

__block)byref_val_0 val = {0,&val,0,sizeof(__block_byref_val_0),10};//自己持有自己的指针。

它竟然变成了结构体了 。之所以为啥要生成一个结构体,后面在详细讲讲。反正不能直接保存val的指针,因为val是栈上的,保存栈变量的指针很危险。

block存储区域

这就需要引入三个名词:

● _NSConcretStackBlock

● _NSConcretGlobalBlock

● _NSConcretMallocBlock

正如它们名字说的那样,说明了block的三种存储方式:栈、全局、堆。__main_block_impl_0结构体中的isa就是这个值。

【要点1】如果是定义在函数外面的block是global的,另外如果函数内部的block但是,没有捕获任何自动变量,那么它也是全局的。比如下面这样的代码:

typedef int (^blk_t)(int);
for(...){
blk_t blk = ^(int count) {return count;};
}

虽然,这个block在循环内,但是blk的地址总是不变的。说明这个block在全局段。

【要点2】一种情况在非ARC下是无法编译的:

typedef int(^blk_t)(int);

blk_t func(int rate){

return ^(int count){return rate*count;}

}

这是因为:block捕获了栈上的rate自动变量,此时rate已经变成了一个结构体,而block中拥有这个结构体的指针。即如果返回block的话就是返回局部变量的指针。而这一点恰是编译器已经断定了。在ARC下没有这个问题,是因为ARC使用了autorelease了。

【要点3】有时候我们需要调用block 的copy函数,将block拷贝到堆上。看下面的代码:

-(id) getBlockArray{
int val =10;
return [[NSArray alloc]initWithObjects:
^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);},nil];
} id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t){obj objectAtIndex:0};
blk();

这段代码在最后一行blk()会异常,因为数组中的block是栈上的。因为val是栈上的。解决办法就是调用copy方法。

【要点4】不管block配置在何处,用copy方法复制都不会引起任何问题。在ARC环境下,如果不确定是否要copy block尽管copy即可。ARC会打扫战场。

注意:在栈上调用copy那么复制到堆上,在全局block调用copy什么也不做,在堆上调用block 引用计数增加

【注意】本人用Xcode 5.1.1 iOS sdk 7.1 编译发现:并非《Objective-C》高级编程这本书中描述的那样

int val肯定是在栈上的,我保存了val的地址,看看block调用前后是否变化。输出一致说明是栈上,不一致说明是堆上。

typedef int (^blkt1)(void) ;
-(void) stackOrHeap{
__block int val =10;
int *valPtr = &val;//使用int的指针,来检测block到底在栈上,还是堆上
blkt1 s= ^{
NSLog(@"val_block = %d",++val);
return val;};
s();
NSLog(@"valPointer = %d",*valPtr);
}

在ARC下——block捕获了自动变量,那么block就被会直接生成到堆上了。  val_block = 11 valPointer = 10

在非ARC下——block捕获了自动变量,该block还是在栈上的。  val_block = 11 valPointer = 11

调用copy之后的结果呢:

-(void) stackOrHeap{
__block int val =10;
int *valPtr = &val;//使用int的指针,来检测block到底在栈上,还是堆上
blkt1 s= ^{
NSLog(@"val_block = %d",++val);
return val;};
blkt1 h = [s copy];
h();
NSLog(@"valPointer = %d",*valPtr);
}

----------------在ARC下>>>>>>>>>>>无效果。  val_block = 11 valPointer = 10

----------------在非ARC下>>>>>>>>>确实复制到堆上了。  val_block = 11 valPointer = 10

用这个表格来表示

/*当block捕获了自动变量时候

------------------------------------------------------------------

|     where  block stay  |       ARC     |       非ARC   |

-------------------------------------------------------------------

|                 copy          |       heap     |     heap         |

------------------------------------------------------------------

|             no copy         |      heap     |      stack        |

------------------------------------------------------------------

*/

__block变量存储区域

当block被复制到堆上时,他所捕获的对象、变量也全部复制到堆上。

回忆一下block捕获自动变量的时候,自动变量将编程一个结构体,结构体中有一个字段叫__forwarding,用于指向自动这个结构体。那么有了这个__forwarding指针,无论是栈上的block还是被拷贝到堆上,那么都会正确的访问自动变量的值。

截获对象

block会持有捕获的对象。编译器为了区分自动变量和对象,有一个类型来区分。

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src){
_Block_objct_assign(&dst->val,src->val,BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src){
_block_object_dispose(src->val,BLOCK_FIELD_IS_BYREF);
}

BLOCK_FIELD_IS_BYREF代表是变量。BLOCK_FIELD_IS_OBJECT代表是对象

【__block变量和对象】

__block修饰符可用于任何类型的自动变量

【__block循环引用】

根据上面讲的内容,block在持有对象的时候,对象如果持有block,会造成循环引用。解决办法有两种:

1. 使用__weak修饰符。id __weak obj = obj_

2. 使用__block修饰符。__block id tmp = self;然后在block中tmp = nil;这样就打破循环了。这个办法需要记得将tmp=nil。不推荐!

文章开头block测试题答案:ABABB

int val = 10;
void (^blk)(void) = ^{printf("val=%d\n",val);};
val = 2;
blk();

上面这段代码,输出值是:val = 10.而不是2.  block截获自动变量的瞬时值 。因为block保存了自动变量的值,所以在执行block语法后,即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。

尝试改写block中捕获的自动变量,将会是编译错误。我更喜欢把这个理解为:block捕获的自动变量都将转化为const类型。不可修改了

解决办法是将自动变量添加修饰符 __block;那么如果截获的自动变量是OC对象呢

^{[array addObject:obj];};

这么写是没有问题的,因为array是一个指针,我们并没有改变指针的值。这个也可以解释下面的问题

const char text[] = "hello";

这样会编译错误。为何?

这是因为捕获自动变量的方法并没有实现C语言数组类型

。可以通过指针代替:const char *text= "hello";

 赞一个  收 藏
 
我来评几句

OC高级编程——深入block,如何捕获变量,如何存储在堆上的更多相关文章

  1. 《C#高级编程》学习笔记----c#内存管理--栈VS堆

    本文转载自Netprawn,原文英文版地址 尽管在.net framework中我们不太需要关注内存管理和垃圾回收这方面的问题,但是出于提高我们应用程序性能的目的,在我们的脑子里还是需要有这方面的意识 ...

  2. UNIX环境高级编程——线程同步之条件变量以及属性

    条件变量变量也是出自POSIX线程标准,另一种线程同步机制.主要用来等待某个条件的发生.可以用来同步同一进程中的各个线程.当然如果一个条件变量存放在多个进程共享的某个内存区中,那么还可以通过条件变量来 ...

  3. 跟着老男孩一步步学习Shell高级编程实战

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://oldboy.blog.51cto.com/2561410/1264627 本sh ...

  4. C#高级编程笔记之第三章:对象和类型

    类和结构的区别 类成员 匿名类型 结构 弱引用 部分类 Object类,其他类都从该类派生而来 扩展方法 3.2 类和结构 类与结构的区别是它们在内存中的存储方式.访问方式(类似存储在堆上的引用类型, ...

  5. 解读经典《C#高级编程》第七版 Page68-79.对象和类型.Chapter3

    前言 新年好,本篇开始进入第三章,<对象和类型>,深刻理解C#的对象,对于使用好.Net类库非常重要. 01 类和结构 从使用角度看,结构和类的区别很小,比如,将结构定义转换为类,只需要将 ...

  6. (转)跟着老男孩一步步学习Shell高级编程实战

    原文:http://oldboy.blog.51cto.com/2561410/1264627/  跟着老男孩一步步学习Shell高级编程实战 原创作品,允许转载,转载时请务必以超链接形式标明文章 原 ...

  7. C#高级编程笔记2016年10月12日 运算符重载

    1.运算符重载:运算符重重载的关键是在对象上不能总是只调用方法或属性,有时还需要做一些其他工作,例如,对数值进行相加.相乘或逻辑操作等.例如,语句if(a==b).对于类,这个语句在默认状态下会比较引 ...

  8. 【转】apue《UNIX环境高级编程第三版》第一章答案详解

    原文网址:http://blog.csdn.net/hubbybob1/article/details/40859835 大家好,从这周开始学习apue<UNIX环境高级编程第三版>,在此 ...

  9. C#高级编程第9版 阅读笔记(一)

    一.前言 C# 简洁.类型安全的面向对象的语言. .NET是一种在windows平台上编程的架构——一种API. C#是一种从头开始设计的用于.NET的语言,他可以利用.NET Framework及其 ...

随机推荐

  1. 移动端视网膜(Retina)屏幕下1px边框线 解决方案

    原因: 因为Retine屏的分辨率始终是普通屏幕的2倍,1px的边框在devicePixelRatio=2的retina屏下会显示成2px. 但在IOS8中,已经支持0.5px了,那就意味着, 在de ...

  2. UART接口基本知识

    Universal asynchronous transciever即同一异步收发器,也就是我们平时所说的串口,是一种最简单,最基本的通信接口. 通信接口按照不同的标准有不同的分类,常见的有同步或异步 ...

  3. effective OC2.0 52阅读笔记(二 对象、消息、运行期)

    第二章:对象.消息.运行期 6 理解属性这一概念 总结:OC解决硬编码偏移量问题的做法,一种方案是把实例变量当做一种存储偏移量所用的特殊变量,交由类对象保管,偏移量会在运行期查找,叫做稳固的“应用程序 ...

  4. 工具第二天 cocoaPods 私有库的创建

    之前介绍了cocoaPods的安装与使用,今天简单谈一下 自己的私有库运用cocoaPods依赖. cd到需要做库的工程目录下 创建一个podspec文件 创建:pod spec create 名称 ...

  5. 链表操作----将单链表向右旋转 K 个位置

    给定一个单链表,设计一个算法实现链表向右旋转 K 个位置. 举例: 给定 1->2->3->4->5->6->NULL, K=3 则     4->5-> ...

  6. SecureCRT的背景和文字颜色的修改

    options->;session options->;emulation->;terminal选择linux(相应的服务器系统)ansi color 打上钩options-> ...

  7. Linux下安装Android Studio(ubuntu)

    一. 安装Android Studio 1. 添加源,按回车键继续 sudo apt-add-repository ppa:paolorotolo/android-studio 2. 更新源 sudo ...

  8. 九度OJ 1214 寻找丑数【算法】

    题目地址:http://ac.jobdu.com/problem.php?pid=1214 题目描述: 把只包含因子2.3和5的数称作丑数(Ugly Number).例如6.8都是丑数,但14不是,因 ...

  9. FormSheet式模态视图,点击模态视图外隐藏模态视图的方法

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  10. Manacher&#39;s Algorithm(马拉车算法)

    ## 背景 该算法用于求字符串的最长回文子串长度. ## 参考文章 >[最长回文子串——Manacher 算法](https://segmentfault.com/a/1190000003914 ...