dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; //网络请求 dispatch_async(dispatch_get_main_queue(), ^{ // 2 [self fadeInNewImage:overlayImage]; // 3 ,UI更新 }); });
1. 首先将工作从主线程移到全局线程。因为这是一个 dispatch_async() ,Block 会被异步地提交,意味着调用线程地执行将会继续。
double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; });
1. 你声明了一个变量指定要延迟的时长。
2. 然后等待 delayInSeconds 给定的时长,再异步地添加一个 Block 到主线程。
@property(nonatomic,strong) NSMutableArray photosArray;//...+ (instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
+ (instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; });
+ (instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
+ (instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
dispatch_once() 以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。
需要记住的是,这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。这些需要用其它方式来保证线程安全,例如同步访问数据。
处理读者与写者问题
- (void)addPhoto:(Photo *)photo { if (photo) { [_photosArray addObject:photo]; dispatch_async(dispatch_get_main_queue(), ^{ [self postContentAddedNotification]; }); } }
这是一个写方法,它修改一个私有可变数组对象。
现在看看 photos ,如下:
- (NSArray *)photos { return [NSArray arrayWithArray:_photosArray]; }
@interface PhotoManager () @property (nonatomic,strong,readonly) NSMutableArray *photosArray; @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this @end
找到 addPhoto: 并用下面的实现替换它:
- (void)addPhoto:(Photo *)photo { if (photo) { // 1 dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2 [_photosArray addObject:photo]; // 3 dispatch_async(dispatch_get_main_queue(), ^{ // 4 [self postContentAddedNotification]; }); }); } }
新写的函数是这样工作的:
- (NSArray *)photos { __block NSArray *array; // 1 dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 array = [NSArray arrayWithArray:_photosArray]; // 3 }); return array; }
+ (instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; // ADD THIS: sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT); }); return sharedPhotoManager; }
- (void)viewDidLoad { [super viewDidLoad]; dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }
- (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }
Dispatch Groups(调度组)
Dispatch Group 会在整个组的任务都完成时通知你。这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。而且在整个组的任务都完成时,Dispatch Group 可以用同步的或者异步的方式通知你。因为要监控的任务在不同队列,那就用一个 dispatch_group_t 的实例来记下这些不同的任务。当组中所有的事件都完成时,GCD 的 API 提供了两种通知方式。
第一种是 dispatch_group_wait ,它会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时发生。
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); // 2 for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 3 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 4 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5 dispatch_async(dispatch_get_main_queue(), ^{ // 6 if (completionBlock) { // 7 completionBlock(error); } }); }); }
按照注释的顺序,你会看到:
1. 因为你在使用的是同步的 dispatch_group_wait ,它会阻塞当前线程,所以你要用 dispatch_async 将整个方法放入后台队列以避免阻塞主线程。
2. 创建一个新的 Dispatch Group,它的作用就像一个用于未完成任务的计数器。
3. dispatch_group_enter 手动通知 Dispatch Group 任务已经开始。你必须保证 dispatch_group_enter 和 dispatch_group_leave 成对出现,否则你可能会遇到诡异的崩溃问题。
4. 手动通知 Group 它的工作已经完成。再次说明,你必须要确保进入 Group 的次数和离开 Group 的次数相等。
5. dispatch_group_wait 会一直等待,直到任务全部完成或者超时。如果在所有任务完成前超时了,该函数会返回一个非零值。你可以对此返回值做条件判断以确定是否超出等待周期;然而,你在这里用 DISPATCH_TIME_FOREVER 让它永远等待。它的意思,勿庸置疑就是,永-远-等-待!这样很好,因为图片的创建工作总是会完成的。
6. 此时此刻,你已经确保了,要么所有的图片任务都已完成,要么发生了超时。然后,你在主线程上运行 completionBlock 回调。这会将工作放到主线程上,并在稍后执行。
7. 最后,检查 completionBlock 是否为 nil,如果不是,那就运行它。
解决方案还不错,但是总体来说,如果可能,最好还是要避免阻塞线程。你的下一个任务是重写一些方法,以便当所有下载任务完成时能异步通知你。
在我们转向另外一种使用 Dispatch Group 的方式之前,先看一个简要的概述,关于何时以及怎样使用有着不同的队列类型的 Dispatch Group :
1. 自定义串行队列:它很适合当一组任务完成时发出通知。
2. 主队列(串行):它也很适合这样的情况。但如果你要同步地等待所有工作地完成,那你就不应该使用它,因为你不能阻塞主线程。然而,异步模型是一个很有吸引力的能用于在几个较长任务(例如网络调用)完成后更新 UI 的方式。
3. 并发队列:它也很适合 Dispatch Group 和完成时通知。
Dispatch Group,第二种方式
上面的一切都很好,但在另一个队列上异步调度然后使用 dispatch_group_wait 来阻塞实在显得有些笨拙。是的,还有另一种方式……
在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的实现替换它:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { // 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 2 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 3 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4 if (completionBlock) { completionBlock(error); } }); }
下面解释新的异步方法如何工作:
1. 在新的实现里,因为你没有阻塞主线程,所以你并不需要将方法包裹在 async 调用中。
2. 同样的 enter 方法,没做任何修改。
3. 同样的 leave 方法,也没做任何修改。
4. dispatch_group_notify 以异步的方式工作。当 Dispatch Group 中没有任何任务时,它就会执行其代码,那么 completionBlock 便会运行。你还指定了运行 completionBlock 的队列,此处,主队列就是你所需要的。
对于这个特定的工作,上面的处理明显更清晰,而且也不会阻塞任何线程。
看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已经注意到这里的 for 循环,它迭代三次,下载三个不同的图片。你的任务是尝试让 for 循环并发运行,以提高其速度。dispatch_apply 刚好可用于这个任务。
dispatch_apply 表现得就像一个 for 循环,但它能并发地执行不同的迭代。这个函数是同步的,所以和普通的 for 循环一样,它只会在所有工作都完成后才会返回。
当在 Block 内计算任何给定数量的工作的最佳迭代数量时,必须要小心,因为过多的迭代和每个迭代只有少量的工作会导致大量开销以致它能抵消任何因并发带来的收益。而被称为跨越式(striding)的技术可以在此帮到你,即通过在每个迭代里多做几个不同的工作。
那何时才适合用 dispatch_apply 呢?
1. 自定义串行队列:串行队列会完全抵消 dispatch_apply 的功能;你还不如直接使用普通的 for 循环。
2. 主队列(串行):与上面一样,在串行队列上不适合使用 dispatch_apply 。还是用普通的 for 循环吧。
3. 并发队列:对于并发循环来说是很好选择,特别是当你需要追踪任务的进度时。
回到 downloadPhotosWithCompletionBlock: 并用下列实现替换它:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); }]; [[PhotoManager sharedManager] addPhoto:photo]; }); dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ if (completionBlock) { completionBlock(error); } }); }
你的循环现在是并行运行的了;在上面的代码中,在调用 dispatch_apply 时,你用第一次参数指明了迭代的次数,用第二个参数指定了任务运行的队列,而第三个参数是一个 Block。
要知道虽然你有代码保证添加相片时线程安全,但图片的顺序却可能不同,这取决于线程完成的顺序。
在真机上运行新代码会稍微更快的得到结果。但我们所做的这些提速工作真的值得吗?
实际上,在这个例子里并不值得。下面是原因:
1. 你创建并行运行线程而付出的开销,很可能比直接使用 for 循环要多。若你要以合适的步长迭代非常大的集合,那才应该考虑使用 dispatch_apply。
2. 你用于创建应用的时间是有限的——除非实在太糟糕否则不要浪费时间去提前优化代码。如果你要优化什么,那去优化那些明显值得你付出时间的部分。你可以通过在 Instruments 里分析你的应用,找出最长运行时间的方法.
3. 通常情况下,优化代码会让你的代码更加复杂,不利于你自己和其他开发者阅读。请确保添加的复杂性能换来足够多的好处。
记住,不要在优化上太疯狂。你只会让你自己和后来者更难以读懂你的代码。
<完>
本文整理自CocoaChina:http://www.cocoachina.com/industry/20140515/8433.html