第十四章 块对象

什么是块对象

C编译器和GCD

块对象(block Object)不是Objective-C而是C语言的功能实现。在其他编程语言中,它与闭包(closure)功能相同。

块对象的定义

块对象的参数列和主体部分的书写方法与普通函数相同。主体中如果有return,就可以定义返回值块。格式如下:

^(参数列){主体}

从^开始到参数列,主体最后的大括号,这一段记述称为块对象的块句法(block literal)。实际上,块句法并不被用于在内存中分配的块对象,它只是编写代码时的一种表达用语。

块对象本身常用于带入到变量后评估,或被作为函数或方法的参数传入等。此时,变量或参数的类型声明和函数指针使用相同的书写方法。只是函数指针声明中使用“*”,而块对象使用“^”。

块对象和类型声明

同函数指针一样,为了简化书写,可以使用typedef简化声明。如:

typedef int (^myBlockType) (int);

使用该类型后,声明函数func就可以使用下面的方式:

void func(myBlockType block);

如果块对象没有参数,参数列则可以设置为(void),此外也可以将参数列连同括号全部省略,或者只保留括号。

此外,剋通过_ _BLOCKS_ _宏来检查当前系统环境下是否可使用块对象。根据编译的不同条件,即可区别可以使用块对象时的情况和不可以使用块对象的情况。

块对象中的变量行为

块对象只在块句法中保存自动变量的值。

块对象就是把可以执行的代码和代码中可访问的变量“封装”起来,使得之后可以做进一步处理的包。而闭包这个称呼本身就是把变量等执行环境封装起来的意思。我们把闭包引用,读取外部变量称为捕获(capture)。

总结:

块句法主题中,除块句法内部的局部变量和形参外,还包括当前位置处可以访问的变量。这些变量中有外部变量,还有包含块句法的代码块内可以访问的局部变量。

从块对象内部可以直接访问外部变量和静态变量(static变量),也可以直接改变变量的值。

在包含块句法的代码块内可访问的局部变量中,书写句法块时自动变量(栈内变量)的值会被保存起来,然后再被访问。

所以,即使自定变量最初的值发生了变化,块对象在使用时也不会知道。

自动变量的值可以被读取但是不能被改变。

自动变量为数组时,会发生编译错误。

简言之,在块对象中虽然可以使用可访问的变量,但自动变量的话就只能读取复制值。换言之,自动变量在运行时就相当于const修饰的变量。

排序函数和块对象

块对象可以实现和函数指针相同的功能。使用函数指针时,需要写不同的函数来应对各种不同的功能,此外,为了给函数传递需要的附加信息,往往还要使用多余的参数和外部变量,而这些都违背了编写代码要尽可能独立易懂的原则。

通过灵活使用块对象,我们就可以将这样的函数或方法实现为易读,灵活的书写方式。

块对象的构成

块对象的实例和生命周期

在编译块句法时,会生成存放必要信息的内存地址(实为结构体)和函数。变量中带入的以及向函数传入的实参,实际上就是这片内存区域的指针。

在函数外部的块句法被编译后,块对象的内存区域就同外部变量一样被配置在了静态数据区中。

执行包含块句法的函数时,和自动变量相同,块对象的内存区域会在栈上得到分配。因此这些块对象的生命周期也和自动变量相同,只在函数执行期间存在。

#include

voidpr(int(^block)(void)) {

printf("%d\n", block());

}

int(^g)(void) = ^{return100; };

voidfunc1(intn) {

int(^b1)(void) = ^{returnn; };

pr(b1);

g = b1;

// assign the local block

}

voidfunc2(intn) {

inta =10;

int(^b2)(void) = ^{

returnn * a; };

pr(b2);

}

intmain(void)

{

pr(g);

func1(5);

func2(5);

pr(g);  //

会发生于运行时错误

return0;

}

块对象将要保存的自动变量的信息复制到了内存区域。该内存区域也包含了评估块对象时所执行的函数指针等的信息。

即使反复执行块句法处的代码,也不会每次都为块对象动态分配一片新的内存区域。但是,被复制到内存区域中的自动变量的值每次都会更新。另一方面,含块句法的函数在递归调用时,同自动变量相同,块对象就会在栈上保存多个内存区域。

总结:

块句法写在函数外面时,只在静态数据区分配一片内存区域给块对象。这片区域在程序执行期会一直存在。

块句法写在函数内时,和自动变量一样,块对象的内存区域会在执行包含块对象的函数时被保存在栈上。该区域的生命周期就是在函数运行期间。

此外,在现在的实现中,当函数内的块句法不包含自动变量时,就没必要复制值,所以块对象会被设置在静态数据区。但因为实现方法可能改变,应该避免编写具有这种依赖关系的程序。

应该避免的编码模式

上面注释处代码之所以会发生运行时错误,是因为栈上生成的块对象在生命周期外是不能被使用的。

证明块对象只有一个实体的实例:

voidfunc1(void) {

int i;

int (^blocks1[10])(void);

for(i =0; i <10; i++)

blocks1[i] = ^{returni; };

for(i =0; i <10; i++)

pr(blocks1[i]);

}

如果想为数组的各个元素代入不同的块对象,就必须要进行下一节中所说的复制。但是,使用ARC时操作是不同的。

块对象的复制

有一个函数可以复制块对象到新的堆区域。通过使用该功能,即使是函数内的块对象也能独立于栈被持续使用。此外,还有一个函数可以释放不需要的块对象。

Block_copy(block)

参数为栈上的块对象时,返回堆上复制的块对象。否则(参数为静态数据区或为堆上的块对象)则不进行复制而直接将参数返回,但会增加参数的块对象的引用计数。

Block_release(block)

减少参数块对象的引用计数,减到0时释放块对象的内存区域。

使用这些函数时,源文件中需要添加头文件Block.h

如前所述,堆上分配的块对象使用引用计数来管理。即使在使用垃圾回收的情况下,也必须成对调用Block_copy和Block_release。

指定特殊变量_ _block

通过_ _block修饰的变量有如下功能:

1.函数内块句法引用的_ _block变量是块对象可以读取的变量。同一个变量作用域内有多个块对象访问时,他们之间可以共享_ _block变量的值。

2._ _block变量不是静态变量,它在块句法每次执行块句法时获取变量的内存区域。也就是说,同一个块(变量作用域)内的块对象以及它们之间共享的_ _block变量是在执行时动态生成的。

3.访问_ _block变量的块对象在被复制后,新生成的块对象也能共享_ _block变量的值。

4.多个块对象访问一个_ _block变量时,只要有一个块对象存在着,_ _block变量就会随之存在。如果访问_ _block变量的块对象都不在了,_ _block也会随之消失。

因为可能会涉及到实现,这里省略了对_ _block变量行为的说明。但有一点,随着块对象的复制,_ _block的内存位置会发生变化。而且不要写使用指针来引用_ _block变量的代码。

Objective和块对象

方法定义和块对象

块对象作为方法参数传递时参数类型的指定方法:

现在假如有一个块对象:BOOL(^block) (int,int) = ^(intindex,intlength){...;};

使用该块对象作为参数的方法setBlock:声明如下。“(BOOL(^)(int,int)) ”为参数类型

- (void)setBlock: (BOOL(^)(int,int)) block;

类型部分中也可以写上形式参数名。

在OC中使用块对象时,在块句法内也可以写消息等OC的语法元素。

作为Objective-C对象的块对象

OC程序在编译运行时,块对象会成为OC的对象来执行操作。

有一点需要注意,retainCount方法返回的引用计数结果是不正确的。

ARC和块对象

使用ARC和不使用ARC时,块对象的操作是有区别的。

在ARC中,需要保存块对象时,编译器会自动插入copy操作。具体的说,就是在被带入强访问变量以及被作为return的返回值返回的时候。这些情况下,程序不需要显式地执行块对象的副本。

但是,作为方法参数传入的块对象是不会自动执行copy的。而且,当块对象声明为属性值时,属性选项一般会指定(copy)。

不要使用Block_copy和Block_release。因为已经定义了(void *)型参数指针,所以ARC不能推测所有者。

_ _block变量的行为不同。不使用ARC时,_ _block变量只带入值,示情况可能会悬空指针。ARC中因为有_ _strong修饰符修饰_ _block变量,使其作为强访问变量来使用,因此就不会成为悬空指针。

对象内变量的行为

介绍块句法内使用块对象时的行为,特别是引用计数。

void(^cp)(void);

- (void)someMethod {

idobj = ...;

intn =10;

void(^block)(void) = ^{ [obj calc:n]; };

...

cp = [block copy];

}

如下图a所示,块对象在栈上生成,自动变量obj和n可以在块内使用。obj引用任意实例对象时,块对象内使用的变量obj也会访问同一个对象。这时,变量的引用计数不会发生改变。

接下来块对象自身被复制,并在堆区域中生成了新的块对象。(图b)。这里,实例对象的引用计数加1,由于方法执行结束后自动变量obj也会消失,因此引用计数加1就使得块对象成为了所有者。实例对象不是被复制,而是被共享,不只从块对象,从哪都可以发送消息。

需要注意的是在某个类方法内的块句法被书写了同一类的实例变量这种情况。如下面这个例子,假设ivar为包含方法someMethod的类实例变量。

void(^cp)(void);

- (void)someMethod {

intn =10;

void(^block)(void) = ^{ [ivar calc:n]; };

...

cp = [block copy];

}

这种情况下,当块对象被复制时,self的引用计数将加1,而非ivar。如下图所示,方法的引用参数self在堆上分配。在上例中,self好像没有出现在块句法中,我们可以按下面方式理解:

^{ [self->ivar calc:n]; };

块句法内的实例变量为整数或者实数时也是一样的,self的引用计数也会增加。也就是说,当与self对等的对象不存在时,所有的实例变量都将不能访问。

块对象和对象的关系总结如下:?

方法定义内的块句法中存在实例变量时,可以直接访问实例变量,也可以改变其值。

方法定义内的块句法中存在实例变量时,如果在栈上生成的块对象的副本,retain就会被发送给self而非实例变量,引用计数器的值也会加1.实例变量的类型不一定非得是对象。

块句法内存在非实例变量的对象时,如果在栈上生成某个块对象的副本,包含的对象就会接受到retain,引用计数器的值也会增加。

已经复制后,堆区域中某个块对象即使收到copy方法,结果也只是块对象自身的引用计数器加1.包含的对象的引用计数器的值不变。

复制的块对象在被释放时,也会向包含的对象发送release。

ARC中使用块对象时的注意事项?

使用ARC开发软件时需要注意不要写死循环代码。使用块对象时,相关对象可能会被自动保存,这时也许就会产生死循环。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值