第六章:Block与GCD
第三十七条:理解block
- 1.根据block在内存中的位置,block被分成三种类型:
NSGlobalBlock 全局块:
这种块运行时无需获取外界任何状态,块所使用的内存区域在编译器就可以完全确定,所以该块声明在全局内存中。如果全局块执行copy会是一个空操作,相当于什么都没做。全局块例如:
void (^block)() = ^{
NSLog(@"I am a NSGlobalBlock");
}
- 2.NSStackBlock 栈块:
栈块保存于栈区,超出变量作用域,栈上的block以及__block变量都会被销毁。例如:
NSString *name = @"PHP";
void (^block)() = ^{
NSLog(@"世界上最好的编程语言是%@", name);
};
NSLog(@"%@", block);
运行下你会发现控制台打印的是:
<__NSStackBlock__: 0x7fff5480fa18>
什么,你说什么,你打印出来的是__ NSMallocBlock __? 那是因为你在ARC下编译的,ARC下编译器编译时会帮你优化自动帮你加上了copy操作,你可以用-fno-objc-arc关闭ARC再看一下
- 3.NSMallocBlock 堆块:
NSMallocBlock内心独白:我已经被暴露了,为什么要最后才介绍我!!
堆block内存保存于堆区,在变量作用域结束时不受影响。通过之前在ARC下的输出已经看到了__ NSMallocBlock __.所以我们在定义block类型的属性时常常加上copy修饰,这个修饰其实是多余的,系统在ARC的时候已经帮我们做了copy,但是还是建议写上copy。
第三十八条:为常用的块类型创建typedef
这条主要是为了代码更易读,也比较重要。
- (void)getDataWithHost:(NSString *)host success:(void (^)(id responseDic))success;
//以上要改成下面这种
typedef void (^SuccessBlock)(id responseDic);
- (void)getDataWithHost:(NSString *)host success:(SuccessBlock)success;
第三十九条:用handler块降低代码分散程度
在iOS开发中,我们经常需要异步执行一些任务,然后等待任务执行结束之后通知相关方法。实现此需求的做法很多,比如说有些人可能会选择用委托协议。那么在这种异步执行一些任务,然后等待执行结束之后调用代理的时候,可能代码就会比较分散。当多个任务都需要异步,等等就显得比较不那么合理了。
所以我们可以考虑使用block的方式设计,这样业务相关的代码会比较紧凑,不会显得那么凌乱。
第四十条:用块引用其所属对象是不要出现保留环
这点比较基础了,但是要稍微说一下,不是一定得在block中使用weakself,比如下面:
[YTKNetwork requestBlock:^(id responsObject) {
NSLog(@"%@",self.name);
}];
block 不是被self所持有的,在block中就可以使用self
第四十一条:多用派发队列,少用同步锁
在iOS开发中,如果有多个线程要执行同一份代码,我们可能需要加锁来实现某种同步机制。有人可能第一印象想到的就是@synchronized(self),例如:
- (void)synchronizedMethod {
@synchronized(self){
// Safe
}
}
这样写法效率很低,而且也不能保证线程中觉得的安全。如果有很多属性,那么每个属性的同步块都要等其他同步块执行完毕才能执行。
应该用GCD来替换:
_syncQueue = dispatch_queue_create("syncQueue", DISPATCH_QUEUE_CONCURRENT);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
第四十二条:多用GCD,少用performSelector系列方法
Objective-C本质上是一门非常动态的语言,开发者在开发中可以指定任何一个方法去调用,也可以延迟调用一些方法,或者指定运行方法的线程。一般我们会想到performSelector,但是在GCD出来之后基本就没那么需要performSelector了,performSelector也有很多缺点:
内存管理问题:在ARC下使用performSelector我们经常会看到编译器发出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
performSelector的返回值只能是void或对象类型。
performSelector无法处理带有多个参数的选择子,最多只能处理两个参数。
为了改变这些,我们可以用下面这种方式
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
替换掉
[self performSelectorOnMainThread:@selector(doSomething)
withObject:nil
waitUntilDone:NO];
然后还可以用
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
替换
[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:5.0];
});
第四十三条:掌握GCD以及操作队列的使用时机
GCD技术确实很棒,但是也有一些局限性,或者说有一些场景并不适合。比如过想取消队列中的某个操作,或者需要后台执行任务。还有一种技术叫NSOperationQueue,其实NSOperationQueue跟GCD有很多相像之处。NSOperationQueue在GCD之前就已经有了,GCD就是在其某些原理上构建的。GCD是C层次的API,而NSOperation是重量级的Objective-C对象。
使用NSOperation和NSOperationQueue的优点:
支持取消某个操作:在运行任务前,可以在NSOperation对象上调用cancel方法,用以表明此任务不需要执行。不过已经启动的任务无法取消。GCD队列是无法取消的,GCD是“安排好之后就不管了(fire and forget)”。
支持指定操作间的依赖关系:一个操作可以依赖其他多个操作,例如从服务器下载并处理文件的动作可以用操作来表示,而在处理其他文件之前必须先下载“清单文件”。而后续的下载工作,都要依赖于先下载的清单文件这一操作。这时如果操作队列允许并发执行的话,后续的下载操作就可以在他依赖的下载清单文件操作执行完毕之后开始同时执行。
支持通过KVO监控NSOperation对象的属性:可以通过isCancelled属性来判断任务是否已取消,通过isFinished属性来判断任务是否已经完成等等。
支持指定操作的优先级:操作的优先级表示此操作与队列中其他操作之间的优先关系,优先级搞的操作先执行,优先级低的后执行。GCD的队列也有优先级,不过不是针对整个队列的。
重用NSOperation对象。在开发中你可以使用NSOperation的子类或者自己创建NSOperation对象来保存一些信息,可以在类中定义方法,使得代码能够多次使用。不必重复自己。
第四十四条:通过Dispatch Group机制,根据系统资源状况来执行任务
这条主要是介绍dispatch group,任务分组的功能。他可以把任务分组,然后等待这组任务执行完毕时会有通知,开发者可以拿到结果然后继续下一步操作。
另外通过dispatch group在并发队列上同时执行多项任务的时候,GCD会根据系统资源状态来帮忙调度这些并发执行的任务。
第四十五条:使用dispatch_once来执行只需要运行一次的线程安全代码
这条讲的是常用的dispatch_once
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
dispatch_once比较高效,没有重量级的同步机制。
第四十六条:不要使用dispatch_get_current_queue
dispatch_get_current_queue 函数的行为常常与开发者所预期的不同,此函数已经废弃,只应做调试之用。
由于GCD是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念。
dispatch_get_current_queue 函数用于解决由不可以重入的代码所引发的死锁,然后能用此函数解决的问题,通常也可以用"队列特定数据"来解决。