Block与GCD要点

Saturday, February 24, 2018

理解『块』

  • 块与函数类似,只是块是直接定义在函数里面的,块用符号^来表示,后面跟着一对括号。

  • 块其实就是一个值,而且自有类型。

    //下面定义了一个名为 someBlock 的变量 //结构: return_type(^block_name)(parameters) void (^someBlock)() = ^{

    }

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

  • 默认情况下,被块捕获的变量,是不可以在块里修改的,但是可以在声明变量的时候加上__block 修饰符来解决这个问题。

  • 块就是一种代替函数指针的语法结构

  • 块本身也有引用计数

块的内部结构

  • 在内存布局中,最重要的是 invoke 指针,这是个函数指针,指向块的实现代码。
  • descriptor 变量是指向结构体的指针,声明了对象的总体大小,还声明了 copy 和 dispose 这两个辅助函数所对应的函数指针。
  • 块会把它所捕获的变量都拷贝一遍,放在 descriptor 变量后面。
  • 拷贝的不是对象本身,而是指向这些对象的指针变量。

全局块,栈块及堆块

  • 块所占的内存空间是在栈中的。

    void (^block)(); if(some condition) { block = ^{ NSLog(@“Block A”); }; }else { block = ^{ NSLog(@“Block B”); }; } block(); //编译器会为每个块分配好栈内存,然而离开了相应的范围后,编译器就有可能把该内存覆写掉,于是这个 block 只能保证在 if else里面有效。 //为解决此问题,可以加上 copy,把块从栈复制到堆。

    void (^block)(); if(some condition) { block = [^{ NSLog(@“Block A”); } copy]; }else { block = [^{ NSLog(@“Block B”); } copy]; }

    //也可以用全局块来解决该问题 void (^block) = ^{ NSLog(@“block”); } //块所使用的整个内存区域,在编译期就已经完全确定了,所以可把它做成全局块。

为常用的块类创建 typedef

<code class="language-null">typedef int(^EOCSomeBlock) (BOOL flag, int value);
</code>

用 handler 块降低代码分散程度

  • 使用 handle 块,可以增加一个参数,通过此参数决定应该把块安排在哪个队列上执行。

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

  • 解除保留环的方法,在 block 内部捕获外部变量时,声明一个 weakSelf

  • 或者

    - (void)p_requestCompleted { if (_completionHandler) { _completionHandler(data); } self.completionHandler = nil; }

多用派发队列,少用同步锁

  • 在 GCD 之前,有两种办法来使用锁实现同步机制

    //内置同步块

    • (void)synchronizedMethod { @synchronized(self) { //safe } } //直接使用 NSLock 对象 _lock = [[NSLock alloc]init];
    • (void)synchronizedMethod { [_lock lock]; //safe [_lock unlock]; }
  • 通常在属性读取的地方需要使用同步锁

  • 滥用同步锁@synchronized(self)会很危险,因为所有同步块都会彼此抢夺同一个锁。要是很多属性都用了同步锁,那么每个属性都要等待其他所有同步块执行完毕才能执行。

  • 使用同步锁虽然能提供线程安全,但却无法保证访问该对象时线程是安全的,多次访问属性的结果未必会相同,因为有可能有其他线程在两次操作之间插入了新的值。

  • 使用串行同步队列解决上述问题,将读取操作都放到一个队列里。

    _syncQueue = dispatch_queue_create(“com.effectiveobjectivec.syncQueue”, NULL);

    • (NSString *)someString { _block NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; }

    • (void)setSomeString:(NSString *)someString { dispatch_sync(_syncQueue, ^{ _someString = someString; }); }

  • 进一步优化,设置方法不一定要同步,可以改成异步派发,这样可以提升速度,但是读取和写入依然会按照顺序执行。但是在简单的任务下,这么做可能会更慢,因为异步派发需要拷贝块。

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

  • 多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,利用这个特点可以写出更快的代码,在设置方法的并发队列中加入栅栏即可。并发队列如果发现接下来处理的是个栅栏块,那么就一直等当前所有并发块都执行完毕,才会单独执行这个栅栏块。

    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    • (NSString *)someStirng { _block NSString *localSomeStirng; dispatch_sync(_syncQueue, ^{ localSomeStirng = _someString; }); return localSomeStirng; }

    • (void)setSomeString:(NSString *)someString { dispatch_barrier_async(_syncQueue, ^{ _someString = someStirng; }); }

多用 GCD,少用 performSelector 系列方法

  • performSelecotr 局限性太大,比如传入的参数必须是对象,参数最多只能两个。

  • 使用GCD 来替代,延后执行可以用 dispatch_after,在另一个线程上执行则可以通过 dispatch_sync 及 dispatch_async 来实现。

    //延后执行 [self performSelector:@selector(doSomething) withObject:nil afterDelay: 5.0];

    //使用 GCD 实现 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 performSelecotrOnMainThread:@selector(doSomething) withObject: nil waitUnitlDone: NO];

    //使用 GCD 实现 dispatch_async(dispatch_get_main_queue(), ^{ [self doSomething]; });

掌握 GCD 及操作队列的使用时机

  • 除了派发队列 GCD,还有一种操作队列,叫做 NSOperationQueue
  • 开发者可以把操作以 NSOperation 子类的的形式放在队列中,而这些操作也能够并发执行。
  • GCD 是纯 C 的 API,而操作队列是 Objective-C 的对象
  • 用 NSOperationQueue 类的 addOperationWithBlock: 方法搭配 NSBlockOperation 类来使用操作队列。
  • 优点如下: 1.可以取消某个操作,任务运行之前,可以再 NSOperation 对象上调用 cancel 方法。 2.可以指定操作间的依赖关系。开发者能够指定操作之间的依赖关系,是特定的操作必须再另一个操作顺利执行完毕后方可 执行。 3.可以通过键值观测机制监控 NSOperation 对象的属性。 4.可以指定操作的优先级。GCD 的队列也有优先级,但是那是针对整个队列的,而不是针对每个块来说。 5.可以重用 NSOperation 对象。

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

  • dispatch group 可以将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。

  • 使用下列函数创建 dispatch group

    dispatch_group_dispatch_group_create();

  • 把任务编组,有两种方法

    //第一种 void 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);

  • 下列函数用于等待 dispatch group 执行完毕

    //两个参数,一个是等待的 group,另一个是等待的时间 timeout,timeout 表示要等待多久,如果执行的时间小于 timeout,则返回0,否则返回非0,也可以取常量 DISPATCH_TIME_FOREVER,这个表示会一直等待 dispatch group 执行完毕 long disaptch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

  • 除了wait 函数,还可以换一种方法

    //等 dispatch group 执行完毕厚,块会在特定的线程执行。 void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

  • 令数组中的每个对象都执行某项任务,并等待所有任务执行完毕:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t dispatchGroup = dispatch_group_create(); for (id object in collection) { dispatch_group_async(dispatchGroup, queue, ^{ [object performTask]; }); };

    dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);

    //若当前线程不应阻塞,则可以用 notify 替代 wait: dispatch_queue_t notifyQueue = dispatch_get_main_queue(); dispatch_group_notify(dispatchGroup, notifyQueue, ^{

    });

使用 dispatch_once 来执行只需运行一次的线程安全代码

  • 单例模式会经常用到

    + (id)sharedInstance { static EOCClass *sharedInstance = nil static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }

不要使用 dispatch_get_current_queue 函数

  • dispatch_get_current_queue 函数的行为常常与开发者预期的不同,已经废弃,只应做调试之用
  • 由于派发队列是按层级来组织的,所有无法用单个某个队列对象来描述当前队列这一概念。
iOSObjective-C

系统框架

内存管理