Effective-Objective-C读书笔记(6)

GCD

第37条:理解 Block

block 的语法结构如下:

1
2
3
4
5
6
7
8
return_type (^block_name)(parameters)

int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
};

// 调用方式
int a = addBlock(2 + 5); // 7

如果 block 定义在 Objective-C 类的实例方法中,除了可以访问类的所有实例变量外,还可以使用 self 变量。block 总能修改实例变量,所以在声明时无需加 _block。不过如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 所只带的实例关联在一起。比如:

1
2
3
4
5
6
7
8
9
- (void)anInstanceMethod {
__block int additional = 5;
void (^someBlock)() = ^{
// block 修改外部变量,需要使用 `__block` 修饰
additional = 100;
_anInstanceMethod = @"Something";
NSLog(@"_anInstanceMethod = %@", _anInstanceMethod);
};
}

如果某个类实例正在执行 anInstanceMethod 方法。那么 self 变量就指向此实例。由于 block 中没有明确使用 self 变量,所以很容易忘记 self 变量被 block 捕获。直接访问实例变量和通过 self->_anInstanceMethod = @"Something"; 来访问是等效的。一定不能忘记,self 也是对象,block 在捕获它时也会将其保留,如果 self 所指代的对象同时保留了 block,这样就会导致循环引用。

block 的内部结构:

在存放 block 内存区域中,首个变量 isa 指针指向 Class 对象的指针。其余内存里含有 block 对象正常运转所需要的信息。详细解释如下:

  • isa:指向 Class 对象的指针。
  • invoke 变量,指向 block 的实现代码。函数原型至少接受一个 void * 类型参数,此参数代表 block,void * 其实是一种代替函数指针的语法结构,可以将原来用标准 C 实现的代码封装成简明易用的接口。
  • descriptor变量,指向结构体指针。每个 block 中都包含结构体,声明了 block 对象的总体大小,copydispose辅助函数对应的函数指针。辅助函数在拷贝及丢弃 block 对象时运行,其中还会执行一些操作。比如前者要保留捕获的对象,后者将其释放。 block 对象会所捕获的变量拷贝一份,放在 descriptor 变量的后面,注意,拷贝的不是对象本身,而是对象的指针变量。在执行 block 时,要通过 invoke 函数把内存中捕获的变量读取出来。

在定义 block 的时候,都是分配在栈内存的,然而等离开相应的作用于之后,编译器有可能把分配的 block 的内存覆写掉,虽然可以编译,但是程序运行起来有时正确,有时错误。可以通过将 block 拷贝到堆上,这样 block 就变成带有引用计数的对象,后续复制就不会真正的复制,只是增加其医用计数。当引用计数为0,堆上的 block 对象,被系统回收。栈上的 block 原本就会被系统自动回收。示例:

1
2
3
4
5
6
7
8
9
10
void ^(block);
if (/* condition */) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}

还有一种是全局 block,这种 block 不会捕捉任何状态(比如外围变量等),整个 block 所用的内存区域,在编译期间已经确定了。因此全局 block 可以声明在全局内存里,不需要在每次调用的时候于栈中创建,全局 block 的拷贝操作是个空操作,因此这种 block 绝不可能被系统回收。这种 block 相当于单例。

1
2
3
void (^block)() = ^ {
NSLog(@"This is a block");
};

由于该 block 所需要的信息在编译期就能确定,就可以把它做成全局 block。使用全局 block 可以优化程序,减少复制、丢弃该 block 时执行的不必要的操作。

总结:

  • block 可以分配在栈上或堆上,也可以是全局的,分配在栈上的 block 可拷贝至堆中,这样就具备引用计数了。全局的 block 对象可以用来优化程序调用。

第38条:为常用 block 创建 typedef

使用 typedef 来重新定义 block 结构,可以使代码简洁易懂,此时要避免其名称与别的类型相冲突。

1
2
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

第39条:使用 handler block 降低代码分散程度

使用 handler block 来作为回调函数,通常比使用委托代理更加简洁清晰。在设计 handler 的时候,建议成功和失败的 handle 分开写,理由是避免全部逻辑写到一起,造成 block 过长。

第40条:block 引用所属对象时不要出现循环引用

使用 block 一定要注意循环引用的问题,一定要在适当的时机解除循环引用,而不能把责任推给 API 调用者。有一个办法可以避免破坏代码的封装性,类似于 TWRequest 框架所做的那样,一旦调用 completion handler 之后,就没有必要继续在保留它。比如:

1
2
3
4
5
6
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadedData);
}
self._completionHandler = nil;
}

第41条:多用 GCD,少用同步锁

不要滥用 @synchronized(self),会降低代码效率。可以使用 NSLock、或者 NSRecursiveLock(递归锁,线程能够多次持有该锁,而不会出现死锁现象)。在使用 atomic 修饰属性的时候,setter、getter 方法是这样的:

1
2
3
4
5
6
7
8
9
10
11
- (NSString *)someString {
@synchronized(self) {
return _someString;
}
}

- (void)setSomeString:(NSString *)someString {
@synchronized(self) {
_someString = someString;
}
}

这种方式能保证线程安全,但是无法保证该对象是绝对的线程安全,比如在同一线程上多次调用 getter,在两次访问期间,其他线程也可能会写入新的值。造成获取结果未必相同。

可以使用串行同步队列将读取操作与写入操作都安排在同一队列里,即可保证数据同步:

然而还可以通过 dispatch_barrier_async 来提升同步行为的效率。在队列里面,barrier block 必须单独执行,不能与其他的 block 并发执行。因为串行队列当中的 block 总是按照顺序逐个来执行的。并发队列发现接下来的任务是barrier block,就等当前所有的并发任务处理完毕后,才单独执行 barrier block。等到barrier block执行完毕,才按照正常方式继续向下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在并发队列当中,读取操作是普通的 block 实现,写入则是用 `barrier block` 来实现,读取操作可以并行,但写入操作必须单步执行。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(localSomeString, ^{
localSomeString = _someString;
});

return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someSting = someString;
});
}

总结:将同步与异步操作结合起来,可以实现普通加锁一样的机制,这样做却不会阻塞执行派发的线程。多使用barrier block,可以令同步行为更加高效。

第42条:多用 GCD,少用 performSelector 系列方法

总结:

  • performSelector 在内存管理方面会容易有疏失,因为无法确定要执行的selector具体是什么,ARC 编译器无法插入适当的内存管理方法。
  • performSelector 系列方法处理 selector 过于局限,方法和参数个数都受到限制。
  • 最好使用 GCD 来代替 performSelector

第43条:掌握 GCD 的使用时机

在处理多线程与任务管理的问题中,可以使用 GCD、NSOperation 技术。GCD 是基于 C 的 API,NSOperation ,NSOperation 则是 Objective-C 对象。NSOperation 提供了高层的 Objective-C API ,具备实现 GCD 的大多数功能。两者需要具体问题具体分析,每种都有各自的适用场景。比如:NSOperation可以实现取消某个操作、设置操作之间的依赖关系、通过键值观察机制检测 NSOperation 对象属性、指定操作优先级等。GCD 虽然也有优先级,不过这是针对于整个队列来说,而不是针对某个块来说。

第44条:通过 Dispatch Group 机制,根据系统资源状况来执行任务

一个任务可归为一个 dispatch group 只用,可以在并发队列里同时执行多项任务,此时 GCD 会根据资源情况来调度执行这些并发执行的任务。

第45条:使用 dispatch_once 来执行只需要运行一次的线程安全代码

使用 dispatch_once 编写线程安全的代码。标记应该声明在 static、global 作用域中,这样只需执行一次的 block 传给 dispatch_once 函数时,传进去的标记也是相同的。

1
2
3
4
5
6
static dispatch_once_t onceToken;
static EOCClass *sharedInstance = nil;
dispatch_once(&onceToken, ^{
// code to be executed once
sharedInstance = [[self alloc] init];
});

第46条:不要使用 dispatch_get_current_queue

dispatch_get_current_queue 函数已经废弃,只应做调试使用。由于派发队列是按照层级来组织的,所以无法单用某个队列对象来描述“当前队列”这个概念。为了解决死锁问题,可以用“队列特定数据”来替代dispatch_get_current_queue