第37条:理解“块”这一概念
- 块的基础知识
- 模型
return_type (^block_name)(parameters)
- 默认情况下,为块所捕获的变量,是不可以在块里修改的
int additional = 5;
int (^addOption)(int a, int b) = ^(int a, int b){
additional = 7;//Variable is not assignable (missing __block type specifier)
return a + b + additional;
};
int result = addOption(2, 5);
除非加上__block
关键字,但是如果是实例变量就可以直接修改,且不用加__block
- 实例变量读取和写入操作,在块中的访问方式
self.anInstanceVariable
和_anInstanceVariable
都是一样的,所以不要认为使用_anInstanceVariable
这种访问方式时,在块中就不保留self
对象了。防止“保留环“
- 块的内部结构
- 块对象的内存布局
invoke
变量是一个函数指针,指向块的实现代码。
descriptor
变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针
invoke
函数为何需要把块对象作为参数传进来呢?原因在于,块执行时,需要从内存中把这些捕获到的变量读出来。 - 全局块、栈块及堆块
编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存腹写掉。为了解决此问题,可给块发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了,拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数了。如果不在使用这个块,那就应将其释放,在ARC环境下会自动释放,而手动管理引用计数则需要自己来调用release方法。当引用计数降为0后,“分配在堆上的块”会像其他对象一样,为系统所回收。而“分配在栈上的块”则无须明确释放,因为栈内存本来就会自动回收。
第38条:为常用的块类型创建typedef
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使用其名称与别的类型相冲突
typedef void (^VCComplete)(BOOL flag,int value);
//使用
VCComplete complete = ^(BOOL flag,int value){
};
第39条:用handle块降低代码分散程度
- 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
- 在有多个实例需要监控时,如果采用委托模式,那么需要经常要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起。(推荐使用handler块,且有错误的块也放在一起)
倾向于:
而不是
- 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可以通过此参数来决定应该把块安排到哪个队列上执行。
第40条:用块引用其所属对象时不要出现保留环
- 使用块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
- 一定要找个适当的时机解除保留环,而不能把责任推给API调用者。
下面情况注意:
//保留环
int (^addOption)(int a, int b) = ^(int a, int b){
_additional = 7;//_additional实例变量,_additional等效于self. additional,所以该块捕获到了self对象
return a + b + _additional;
};
块复制过后,调用执行块后,要把块置为nil,打破保留环
第41条:多用派发队列,少用同步锁
- 使用get,set方法做同步操作的时候,可以使用GCD,少用同步锁
@synchronized(self)
等方法。原因是GCD更高效,不会阻塞执行异步派发的线程,同步锁会产生死锁现象。 - 使用同步队列及栅栏块,可以令同步行为更叫高效。
栅栏执行的逻辑是,栅栏前面的队列可以并发执行,当遇到栅栏的时候,等到前面的队列全部执行完后,再执行栅栏里面的操作,栅栏执行完后,在执行栅栏后的操作。
栅栏块的效果
第42条:多用GCD,少用performSelector系列方法
-
情况一
使用performSerlector在下面这种情况下,会提示警告
BOOL fool = YES;
SEL selector;
if(fool){
selector = @selector(fetchData);
}else{
selector = @selector(fetchData1);
}
[self performSelector:selector];
//警告:PerformSelector may cause a leak because its selector is unknown
警告的原因是:编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至是否有返回值都不清楚,而且,编译器不知道方法名,所以就没有办法运用ARC的内存管理规则来判断返回值是不是应该释放,ARC选择不释放,所以可能导致内存泄漏。
情况二
这里的
newObject
,copy
这两个创建的对象需要手动释放,someProperty
不需要收到释放,参考上一章的第30条。
-
总结
第43条:掌握GCD及操作队列的使用时机
- 在解决多线程与任务管理问题时,派发队列并非唯一方案。
- 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。
-
NSOperation
及NSOperationQueue
的好处如下:
- 取消某个操作。
NSOperation
可以在对象上调用cancel
方法,来取消尚未执行的任务,已经启动的任务无法取消,GCD无法取消队列,GCD框架是“安排好任务后,就不需要管了”。 - 指定操作间的依赖关系。下载“清单文件”后才能下载其他文件,那么就需要先下载“清单文件”。
- 通过键值观测机制(KVO)来监听
NSOperation
对象的属性。NSOperation
很多属性都可以通过KVO
来进行监听,如isCancelled
,isFinished
等属性。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的任务,那么键值观测机制会有用。 - 指定操作的优先级。操作的优先级表示此操作与队列中的其他操作之间的优先级的关系。GCD的队列确实也有优先级,不过那是针对整个队列来说的,不是针对每个块来说的。
- 重用
NSOperation
对象。NSOperation
对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。这些NSOperation
类可以在代码中多次使用。
第44条:通过Dispatch Group 机制,根据系统资源状况来执行任务
-
dispatch group
能够把任务分组,调用者可以等待这组任务执行完毕,可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。最重要的是执行操作后,可以得到通知,以便可以根据结果处理相应操作。重要用法:就是把将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。
// 1. 创建组
dispatch_group_t group = dispatch_group_create();
// 2. 异步调用,dispatch_group_async没啥特殊用途,就是把队列归属到一个组里面,同dispatch_async
dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
// 或者是指定任务所属的组
void dispatch_group_enter(dispatch_group_t group);//加入到组中
void dispatch_group_leave(dispatch_group_t group);//从组中移除队列
//前者是分组里正要执行的任务递增,后者使之递减,必须成对出现。如果调用enter之后,没有相应的leave操作,那么这组任务永远执行不完。
// 3. 等待组执行完,阻塞线程,dispatch_group_wait前面的先执行,等时间超过timeout,再执行dispatch_group_wait后面的代码,起到阻塞作用
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
// 4. 等到组中的线程都执行完后,再执行这个通知
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
// 5. 遍历某个collection,每个任务可以并发执行,且是阻塞线程的,dispatch_apply后面的任务要等到该函数遍历执行完后,才放开线程。
void dispatch_apply(size_t iterations, dispatch_queue_t queue,
DISPATCH_NOESCAPE void (^block)(size_t));
第45条:使用dispatch_once来执行只需运行一次的线程安全代码
- 使用
dispatch_once
可以简化代码更高效,且彻底保证线程安全。它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,相反,此函数采用“原子访问”来查询标记,判断其所对应的代码是否已经执行过。
+ (id)sharedInstance{
static WCCPerson *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
+ (id)sharedInstance{
static WCCPerson *sharedInstance = nil;
@synchronized(self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
}
//两者执行的时间差不多
CFAbsoluteTime startTime =CFAbsoluteTimeGetCurrent();
WCCPerson *persion = [WCCPerson sharedInstance];
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
//once 0.019968 0.015020 0.009000
//sync 0.010967 0.011027 0.010014
NSLog(@"Linked in %f ms", linkTime *1000.0);
第46条:不要使用dispatch_get_current_queue
没看,不适用的函数了