Effective objective-C---第六章学习--块

Effective objective-C---第六章学习--块

前言

补进度ing

理解"块"这一概念

首先要了解一下块和GCD的概念,这两者都是一起引入的

块"是一种可在 C 、C++及 Objective-C 代码中使用的 "词法闭包"(lexical closure),它极为有用,这主要是因为借由此机制,开发者可将代码像对象一样传递,令其在不同环境(context)下运行。还有个关键的地方是,在定义"块"的范围内,它可以访问到其中的全部变量。

GCD 是一种与块有关的技术,它提供了对线程的抽象,而这种抽象则基于"派发队列"(dispatch queue)(亦称调度队列)。开发者可将块排入队列中,由 GCD 负责处理所有的调度事宜。GCD 会根据系统资源情况,适时地创建、复用、摧毁后台线程(background thread),以便处理每个队列。此外,使用 GCD 还可以方便地完成常见编程任务,比如编写 "只执行一次的线程安全代码"(thread-safe single-code execution),或者根据可用的系统资源来并发执行多个操作。

块的基础知识

块的基础语法如上,int是返回值类型,块的指针名为addBlock,a,b为传入的参数,等于号后面是一个块对象 ;

块其实就是个值,而且自有其相关类型。与 int、float 或 Objective-C 对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。

块的强大之处是:在声明它的范围里。所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了 additional 变量的值,那么编译器就会报错。不过,声明变量的时候可以加上 __block 修饰符,这样就可以在块内修改了。

如下:

传给 "numerateObjectsUsingBlock:" 方法的块并未先赋给局部变量,而是直接内联在函数调用里了。这种用法叫做内联块 ;

块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。

如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 变量一并捕获了,因为实例变量是与 self 所指代的实例关联在一起的。

一定要记住:self 也是个对象,因而块在捕获它时也会将其保留。如果 self 所指代的那个对象同时保留了块,那么这种情况通常就会导致 "保留环"。

块的内部结构

块本身也是对象,在存放块对象的内存区域中,首个变量是指向 Class 对象的指针,该指针叫做 isa。其余内存里含有块对象正常运转所需要的各种信息。

  • invoke 变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个 void * 型的参数,此参数代表块。
  • descriptor 变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy 与 dispose 这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,而后者则将之释放。
  • 块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在 descriptor 变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke 函数为何需要把块对象作为参数传进来呢?原因在于,执行块时,要从内存中把这些捕获到的变量读出来。

全局块,堆块,栈块

栈块

定义对象的时候是初始分配在栈上的,也就是有可能在使用之后内存被覆写,那样就是产生崩溃

堆块

为了解决问题可以给块对象发送copy信息,这样子就会把块从栈复制到堆上,块也就成了带引用计数的对象了,在ARC下编译器会自动的合理的释放对象。

全局块

除了"栈块"和"堆块"之外,还有一类块叫做"全局块"。这种块不会捕捉任何状态(比如外围的变量),运行时也无须有状态来参与。而且全局块的copy属于空操作。可以把他认为是单例。

在使用单例模式封装网络请求的时候就是使用了全局块

要点

  • 块是C、C++、Objective-C 中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。

为常用的块类型创建typedef

为了隐藏复杂的块类型,需要用到C语言的类型定义,typedef关键字。

return _type(^block_name)(parameters)

上面是块的定义,如果不使用typedef,在调用时的形式比较复杂难记,如下;

所以我们一般以下面这种方式定义块变量:

typedef int(^EOCSomeBlock)(BOOL flag, int value);

这样再调用时就很方便了 ;

EOCSomeBlock block = ^(BOOL flag, int value)

使用类型定义还有个好处,就是当你打算重构块的类型签名时会很方便 ;

其原理类似于用函数来分离一些常用代码,在更改时比较方便,也方便排错 ;

要点

  • 以typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应typedef中的块签名即可,无须改动其他typedef。

用handle块降低代码分散度

为用户界面编码时,一种常用的范式就是"异步执行任务"。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。

异步执行任务的通常使用委托模式实现,也就协议传值

比如我们在执行网络请求,想从某个类对象获取api的data,以前常常使用委托模式,也就是设置delegate ;

当我们在使用别的类对象时,可以将该对象设置为上面那个对象的代理,这样当上面那个对象网络请求完得到的data,当前对象也能访问到 ;

这种做法确实可行,而且没什么错误。然而如果改用块来写的话,代码会更清晰。块可以令这种 API 变得更紧致,同时令开发者调用起来更加方便。

如下:

与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

尤其是在有多个api要请求时,委托模式的代码就显得更加昂长 ;

建议使用同一个块来处理成功与失败情况

要点

  • 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler
    块来实现,则可直接将块与相关对象放在一起。
  • 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

用块引用其所属对象时不要引入保留环

使用块时,若不仔细思量,则很容易导致"保留环"(retain cycle)。

举个例子,就是上面用api获取器的例子,也就是通过某个类的对象获取API的data ;

objectivec 复制代码
#import <Foundation/Foundation.h>
 4 
 5   typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
 6 
 7 
 8 
 9   @interface EOCNetworkFetcher : NSObject
10 
11   @property (nonatomic, strong, readOnly) NSURL *url;
12 
13   - (id)initWithURL:(NSURL *)url;
14 
15   - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionhandler)completion;
16 
17   @end
objectivec 复制代码
 // EOCNetworkFetcher.m
 2 
 3   #import "EOCNetworkFetcher.h"
 4 
 5 
 6 
 7   @interface EOCNetworkFetcher ()
 8 
 9   @property (nonatomic, strong, readwrite) NSURL *url;
10 
11   @property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
12 
13   @property (nonatomic, strong) NSData *downloadedData;
14 
15   @end
16 
17   @implementation EOCNetworkFetcher
18 
19   - (id)initWithURL:(NSURL *)url {
20 
21     if ((self = [super init])) {
22       _url = url;
23      }
24   return self;
25 
26   }
27 
28   - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
29 
30     self.completionHandler = completion;
31     // Start the request
32     // Request sets downloadedData property
33     // When request is finished,  p_requestCompleted is called
34   }
35 
36   - (void)p_requestCompleted {
37 
38     if (_completionHandler) {
39       _completionHandler(_downloadedData);
40     }
41   }
42 
43   @end

当使用这个网络获取器时,

objectivec 复制代码
1   @implementation EOCClass {
 2 
 3     EOCNEtworkFetcher *_networkFetcher;
 4     NSData *_fetchedData;
 5   }
 6 
 7 
 8 
 9   - (void)downloadData {
10 
11     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
12     _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
13     [_networkFetcher startWithCompletionHandler:^(NSData *data) {
14       NSLog(@"Request URL %@ finished", _networkFetcher.url);
15       _fetchedData = data;
16     }];
17   }
18 
19   @end

上面这段代码中,handle块中通过实例变量保留了self,而self又通过属性保留了handle块,所以形成了保留环 ;如图:

获取器对象之所以要把 completion handler 块保存在属性里面,其唯一目的就是想稍后使用这个块。可是,获取器一旦运行过 completion handler 之后,就没有必要再保留它了。所以,只需要将 p_requestCompleted 方法按如下方式修改即可:

objectivec 复制代码
 - (vlid)p_requestCompleted {
2 
3     if (_completionHandler) {
4 
5       _completionHandler(_downloadedData);
6     }
7     self.completionHandler = nil;
8   }

这样一来,只要下载请求执行完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。请注意,之所以要在 start 方法中把 completion handler 作为参数传进去,这也是一条重要原因。

要点

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API 的调用者。
相关推荐
xiaobuding_QAQ1 小时前
自用Proteus(8.15)常用元器件图示和功能介绍(持续更新...)
单片机·嵌入式硬件·学习·proteus
wei_shuo1 小时前
偏标记学习+图像分类(论文复现)
学习·分类·数据挖掘
hong_zc1 小时前
算法【Java】—— 二叉树的深搜
java·算法
进击的女IT2 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端
Miqiuha2 小时前
lock_guard和unique_lock学习总结
java·数据库·学习
一 乐3 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
数云界3 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
阑梦清川3 小时前
Java继承、final/protected说明、super/this辨析
java·开发语言
加油,旭杏4 小时前
【中间件学习】fastCG介绍和使用
学习·nginx·fastcgi
limengshi1383924 小时前
通信工程学习:什么是TFTP简单文件传输协议
网络·网络协议·学习·信息与通信