《Objective-C编程全解》 读书笔记 第十四章 块对象




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

问题1 块类型变量


  • 返回值类型是指当块被调用时返回值的类型。
  • 块变量名带有一个脱字符(^)前缀并且被封装在括号中。
  • 参数类型列表被封装在括号中并且位于块名称的后面,其中的参数类型以逗号分隔。
  • 如果块变量声明中没有参数,那么应该将该变量的参数类型列表设置为void(封装在括号中)。
  • 没有返回值的块变量的声明中将其返回值类型设置为void。
  • 块变量可以被用作函数和方法的参数。在这类情况下,通过创建类型定义(通过typedef语句)为块类型提供别名,从而起到简化块类型变量名称的作用。

方法声明中的块变量 

typedef int (^AdderBlock)(int);@interface Calulator : NSObject- (int)process:(int)count withBlock:(AdderBlock)adder;

问题2 块类型常量


  • 使用块常量表达式可以定义对块的引用。该表达式以^开头,后跟返回值类型、参数列表和封装在花括号中的语句集合(块的主体)。

块常量表达式 

^int (int addend){  return addend + 1;}

没有设置返回值类型的块常量表达式 

^(int addend){  return addend + 1;}

不带参数的块常量定义 

^{  NSLog(@"Hello, World!");}

问题3 块的声明和定义

可以将块常量赋予相应的块变量定义。

块的声明和定义 

int (^oneParamBlock)(int);oneParamBlock = ^(int param1){  return param1 * param1;}// 定义了一个名为oneParamBlock的块变量,并将一个块常量赋予了它。

将块声明和定义组合在一起 

int (^incBlock) (int) = ^(int addend){  return addend + 1;}// 块常量表达式的参数类型和返回值类型必须符合相应的块变量定义。

问题4 块语法元素的比较

块语法元素 块变量声明 块常量表达式
^ 标识一个块变量声明的开始,位于变量名称之前。 标识一个块常量表达式的开始。
名称 块变量的名称是必选项 块常量表达式没有名称
返回值类型 在块变量声明中返回值类型是必选项。没有返回值的块变量会将返回值类型设置为void。 在块常量表达式中返回值类型是可选项。
参数 在块变量声明中,参数类型列表是必选项。如果块变量没有参数,必须将参数类型列表声明为void。 在块常量表达式中,参数列表是可选项。

问题5 块常量表达式的定义和调用

可以使用一条语句定义并调用块表达式。注意,块表达式是匿名的,因此调用操作符会接收带有符合“块表达式的参数列表”的匿名(即不带名称的)函数。

定义和调用块表达式 

^(NSString *user){  NSLog(@"Greetings, %@!", user);}(@"Earthling");

问题6 将块常量表达式用作调用方法的参数

还可以像处理匿名函数一样,以内嵌方式定义块表达式,从而将其用作函数或方法的参数。

将块常量表达式用作调用方法的参数 

Calculator calc = [Calculator new];int value = [calc process:2 withBlcok:^(int addend){                                        return addend + 1;                                      }];

问题7 块是闭包

块是一个闭包,一个允许访问在其常规范围外部声明的局部变量的函数。块参数通过以下特性提高了变量的可见性:

  • 对词汇范围的支持。
  • __block关键字可以应用于块外部但仍处于同一词汇范围的变量。通过该特性可以修改块内部的变量。
  • 访问实例变量。在对象的方法实现代码中定义的块可以访问该对象中的实例变量。
  • 导入参数。通过头文件导入的常数变量在块常量表达式中是可见的。

词汇范围

块常量表达式可以访问在同一词汇范围内声明的变量。

使用块通过词汇范围访问局部变量 

int myVar = 10void (^logValueBlock)(void) = ^{    NSLog(@"Variable value = %d", myVar);  };  logValueBlock();}// 局部变量myVar是在定义logValueBlock块的范围内被声明的,而且其声明位于块常量表达式之前,因而可以在这个块常量表达式中使用。

因局部变量的声明在块常量表达式之后,块对局部变量的访问是非法的 

void (^logValueBlock)(void) = ^{    // 错误    NSLog(@"Variable value = %d", myVar);  };  int myVar = 10;  logValueBlock();}// 局部变量myVar是在块常量表达式之后声明和初始化的,因此无法编译。

尝试修改在块常量表达式中通过词汇范围访问的原始数据类型 

{  int myVar = 10;  void (^logValueBlock)(void) = ^{    // 错误,词汇范围变量无法重现赋值    myVar = 5;    NSLog(@"Variable value = %d", myVar);  };  logValueBlock();}

块可以捕捉多重(嵌套)范围内的局部变量 

for(int ii = 0; ii < 2; ii++){  for(int jj = 0; jj < 3; jj++)  {    void (^logValueBlock)(void) = ^{      NSLog(@"Varible values = %d, %d", ii, jj);    };    logValueBlock();  }}

可修改的__block变量

  • 使用存储类型修改法__block可以将块常量表达式使用的局部变量切换为读写模式。
  • 除了C语言的变长数组和含有变长数组的C语言结构之外,可以对Objective-C支持的所有数据类型使用__block修改符。
  • __block修改符不能与局部存储修改符auto、register和static组合使用。

正确使用__block存储类型修改符 

{  __block int myVar = 10void (^intBlock)(int) = ^(int amout){    myVar += amout;    NSLog(@"New value = %d", myVar);  };  intBlock(5);}// 当引用变量的块被复制到堆存储区域时,使用__block修改符的变量也会被复制到堆存储区域。

块的内存管理

在运行程序时,块常量表达式会获得栈内存,因而会拥有与局部变量相同的生命周期。因此,它们必须被复制到永久存储区域(堆)中,才能在定义它们的范围之外使用。

在块常量的词汇范围之外使用它 

void (^greetingBlcok)(void);{ // 范围的起点,将块变量(局部变量)压入栈中  greetingBlcok = ^{    NSLog(@"Hello, World!");  };} // 范围的终点,从栈中弹出块变量greetingBlcok(); // 调用这个块可能会使程序崩溃!

使用块的复制和释放方法 

void (^greetingBlcok)(void);{   greetingBlcok = [^{    NSLog(@"Hello, World!");  } copy]; // 将块常量表达式复制到堆中} greetingBlcok(); // 块调用语句起了作用(使用堆内存)[greetingBlcok release]; // 释放块,防止内存泄露

对使用id类型参数的块调用Block_copy()和Blcok_release()函数 

void (^greetingBlcok)(id salutation);{   greetingBlcok = Block_copy(^(id salutation){                              NSLog(@"%@, world!", salutation);                             });} greetingBlock(@"Hello"); Block_release;
注意
  • 使用Blcok_copy()命令可以将块常量复制到堆中(这就像实现了一个将块常量引用作为输入参数并返回相同类型块常量的函数)。
  • 使用Block_release()命令可以释放堆中的块常量(这就像实现了一个将块常量引用作为输入参数的函数。只有当堆中的块常量引用与块常量对应时,才能从堆中释放块常量引用;否则,该函数调用不会有效果)。
  • Blcok_copy()命令必须与Block_release()命令搭配使用,以防内存泄露。
  • Foundation框架提供了处理块的copy和release方法。
  • 在ARC内存管理方式时,只要块没有返回id类型值或将id类型值用作参数,编译器就会自动执行块的复制和释放操作。否则,就必须手动执行复制和释放操作。
  • 在使用MRR时,如果在块常量中使用__block变量,那么这些__block变量就不会被保留。 在使用ARR时,如果在块常量中使用__block变量,那么这些__block变量就会被保留。这意味着如果在使用ARC时,不想保留__block变量(如避免循环引用),还应对变量使用__weak存储类型修改符。

本章要点

介绍了块的语法、块的内存管理、使用块编写程序的方式和如何使用现有API中的块,要点有:

  • 块与函数和方法类似,但块除了是可执行代码,还含有与栈内存和堆内存绑定的变量。块就是一个实现的闭包,一个允许访问其常规范围之外变量的函数。
  • 可以将块声明为块类型的变量,在可以使用普通变量的地方就可以使用块变量,如作为函数/方法的参数等。
  • 块常量表达式含有调用块时执行的代码。可以通过内嵌方式定义块常量表达式(像定义匿名函数一样),然后将块用作函数/方法调用的参数。
  • 与C语言函数相比,通过词汇范围、__block变量、实例变量访问和常数导入,块参数具有更好的可见性。
  • 在运行程序时,块常量表达式会获得栈内存,因而会拥有与局部变量一样的生命周期。因此,必须将之复制到永久存储区域(即堆内存),才能在定义它们的范围之外使用它们。
  • 在使用MRR时,使用Block_copy()和Blcok_release()命令可以复制和释放堆内存中的块。
  • 在使用ARC时,如果块没有返回id类型的值或者使用id类型的参数,编译器就能自动执行复制和释放块的操作。
  • 通常,块用于实现小型的、用于封装任务单元的独立代码段。它们通常以并行方式执行集合中的多个项目,或者被用作完成操作后的回调函数。因为可以通过内嵌方式定义块,所以不用为上下文关联的代码(如异步完成处理程序)创建完全独立的类和方法。
  • 块还可以使有关联的代码被放在一起,而不会将这些代码分割存储到不同文件中。

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


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

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

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

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

块对象和类型声明
同函数指针一样,为了简化书写,可以使用typedef简化声明。如:
typedef int (^myBlockType) (int);
使用该类型后,声明函数func就可以使用下面的方式:
void func(myBlockType block);

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

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

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

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

总结:
  1. 块句法主题中,除块句法内部的局部变量和形参外,还包括当前位置处可以访问的变量。这些变量中有外部变量,还有包含块句法的代码块内可以访问的局部变量。
  2. 从块对象内部可以直接访问外部变量和静态变量(static变量),也可以直接改变变量的值。
  3. 在包含块句法的代码块内可访问的局部变量中,书写句法块时自动变量(栈内变量)的值会被保存起来,然后再被访问。
  •     所以,即使自定变量最初的值发生了变化,块对象在使用时也不会知道。
  •     自动变量的值可以被读取但是不能被改变。
  •     自动变量为数组时,会发生编译错误。

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

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

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


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

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

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

#include<stdio.h>

void pr(int (^block)(void
)) {     printf("%d\n", block());
}

int (^g)( void ) = ^{ return 100 ; };
void func1(int 
n) {    int (^b1)(void) = ^{return  n; };
    pr(b1);
    g = b1; 
// assign the local block

}

void func2( int  n) {    int a = 10 ;    int (^b2)(void) = ^{ 
return  n * a; };
    pr(b2);
}

int main( void )
{
    pr(g);
    func1( 5);
    func2(5);     pr(g);  //
会发生于运行时错误
   
return 0 ; }




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

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

总结:
  1. 块句法写在函数外面时,只在静态数据区分配一片内存区域给块对象。这片区域在程序执行期会一直存在。
  2. 块句法写在函数内时,和自动变量一样,块对象的内存区域会在执行包含块对象的函数时被保存在栈上。该区域的生命周期就是在函数运行期间。

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

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

证明块对象只有一个实体的实例:
void func1(void ) {        int  i;        int (^blocks1[10])(void );        for (i = 0; i < 10 ; i++)         blocks1[i] = ^{ return i; };       for (i = 0; i < 10 ; i++)        
        pr
(blocks1[i]); }


运行结果如图所示,这是因为保存块对象实体的内存区域只有一块,因此在for循环中数组中保存的指针都是一样的。

如果想为数组的各个元素代入不同的块对象,就必须要进行下一节中所说的复制。但是,使用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) = ^(int index,int length){...;};
使用该块对象作为参数的方法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 {
    id  obj = ...;
   
int n = 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 {
    int n = 10 ;
   
void (^block)( void ) = ^{ [ivar calc:n]; };
    ...
    cp = [block copy];
}

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

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

块对象和对象的关系总结如下:
  1. 方法定义内的块句法中存在实例变量时,可以直接访问实例变量,也可以改变其值。
  2. 方法定义内的块句法中存在实例变量时,如果在栈上生成的块对象的副本,retain就会被发送给self而非实例变量,引用计数器的值也会加1.实例变量的类型不一定非得是对象。
  3. 块句法内存在非实例变量的对象时,如果在栈上生成某个块对象的副本,包含的对象就会接受到retain,引用计数器的值也会增加。
  4. 已经复制后,堆区域中某个块对象即使收到copy方法,结果也只是块对象自身的引用计数器加1.包含的对象的引用计数器的值不变。
  5. 复制的块对象在被释放时,也会向包含的对象发送release。

ARC中使用块对象时的注意事项?
使用ARC开发软件时需要注意不要写死循环代码。使用块对象时,相关对象可能会被自动保存,这时也许就会产生死循环。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值