PMiOSiOS Developer

Effective Objective-C 2.0 读书笔记六

2016-07-15  本文已影响170人  Miridescent

块与大中枢派发

所谓的快和大中枢派发,就是我们常说的block和GCD

37. 理解“块”这一概念

快与函数类似,只不过快直接定义在函数中,和定义它的函数共享同一个范围的东西,下面就是一个简单的快:

^{
        // 快中的代码
  }

快可以看成是一个值,并且和其他值一样有自己的类型,可以把快赋值给其他的变量,然后像使用变量一样使用它:

void (^someBlock) = ^{
        // 快中的代码
    }

上面就是把一个快赋值给一个叫someBlock的变量。快类型的语法结构如下:

return_type (^block_name)(parameters)

下面是一个有两个int类型参数,返回值也是int类型,名字叫addBlock的快,作用是求两个值的和:

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

定义好之后就可以像调用函数一样调用快了:

int add = addBlock(2, 5);

快的一个强大特性就是他可以捕获他所被定义的范围内的所有变量,如下:

__block int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
   additional = a + b;
   return additional;
};
int add = addBlock(2, 5);

我们注意到快以外的东西也被我们引用了,不过,如果要对引入的变量值进行修改就必须用__block修饰,不修饰编译器会报错,后面会讲__block的用处。
我们要注意一旦,如果快捕获的变量是对象类型,那么就会自动保留他,等快释放的时候一起释放。另外一点,快自己也可视为对象,并且也有引用计数。如果快定义在实力变量中,那么快也可以使用self变量,在一些我们没发现的地方,可能就间接使用过了self,例如对实例变量的属性执行设置方法的时候。注意,self也是一个对象,所以在block中使用self就要注意循环引用的问题,后面会单独介绍。
快的具体内部结构这里不做过多介绍,只说两点,快可以看做一种代替函数指针的语法结构,原来需要用指针来传递状态的地方,现在都可以用快实现。快会把捕获的变量拷贝一份,这个拷贝不是拷贝变量本身,拷贝的是指向这些对象的指针。
定义快的时候要注意定义快的范围,通过不同的范围可以把快分为全局快、栈快和堆快。我们通常定义快的时候内存分配是在栈中的,看如下的代码:

    void (^block)();
    if (/*判断条件*/) {
        block = ^{
            NSLog(@"block A");
        };
    } else {
        block = ^{
            NSLog(@"block B");
        };
    }
    block();

定义在if和else中的两个快都是分配在栈中的,当离开相应的范围之后,内存会被回收,所以这样定义的快只有在对应的条件范围内有效,脱离范围就可能出现错误(这取决于这部分内存有没有被覆写),这样的定义是极不安全的,如果想让这样的定义变的安全,可以通过copy把快从栈复制到堆上,这样就可以在定义范围之外使用,如下:

    void (^block)();
    if (/*判断条件*/) {
        block = [^{
            NSLog(@"block A");
        } copy];
    } else {
        block = [^{
            NSLog(@"block B");
        } copy];
    }
    block();

还可以定义一种全局快,这种快不会捕捉任何状态,运行时也无需其他状态参与,快使用的内存在编译期已经确定,这种快声明在全局内存中,不需要在每次调用的时候在栈中创建,这样的快相当于一个单利,例如下面的例子:

void (^block)() = ^{
        NSLog(@"这是一个全局快");
    };

38. 为常用的快类型创建typedef

前面已经介绍过每个块都有自己的固有类型,类型形式如下:

return_type (^block_name)(parameters)

其中return_type是返回值类型,block_name是快名,最后一个括号是参数,我们可以给所有这种类型的快起一个统一的名字,用typedef定义,看下面的例子,我们定义一个参数是bool和int类型,返回值是int类型的快类型:

typedef int (^someBlock)(BOOL flag, int valur);

这样就定义了一个名字是someBlock类型的快类型,这时候创建一个快如下:

someBlock block = ^(BOOL flag, int valur){
        // 快的内部代码
    };

这样定义一个block就和定义其他变量一样了。
在API中用这种方法会使代码更加简洁易懂,例如我们需要用一个快做参数,如果不定义快类型,可能方法名会像下面这样

- (void)emampleWithBlock:(void(^)(BOOL flag, int value))block;

如果通过typedef定义一个block类型,则如下:

typedef void (^someBlock)(BOOL flag, int valur);
- (void)emampleWithBlock:(someBlock)block;

这样参数就简单多了,并且易于理解,定义类型另外一个好处就是,假设我们想多添加一个参数,这时候只要在定义的地方改就可以了,不用每个方法名都更改,另外有时候我们也会在相同的类型的情况下定义不同的类型名,这样我们就可以区分不同类型的用处(虽然他们是相同的结构),总之用法很灵活,在不同的场景做不同的处理。

39. 用handler快降低代码分散程度

在开发中经常会涉及到一些异步执行的线程,最长见的就是网络请求,我们通常把这种耗时的工作放在子线程中,这就涉及到子线程中的任务完成后回调的过程,写委托协议是很常见的情况,我们可以通过不同的状态,在代理方法中做不同的操作。但是通过快,可以使代码更清晰,通过快的内联形式,可以在方法中直接处理接下来的工作,不在需要到别的地方写代码,使代码看起来更顺畅,最典型的可以看AFNetworking的API,看一下AFHTTPSessionManager.h中的一个方法:

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

通过这个方法可以很清楚的看明白整个方法的用处,通过快把整个方法不同阶段的状态表现出来,并且可以针对不同的状态做不同的处理,这种写法简单明了,令使用者简单易懂,也有的API把处理成功和失败的情况放在一起,不过各有各的好处,根据自己的情况自行设定。另外设计这种API还要注意一件事情,就是有一些操作必须在特定的线程,例如UI的操作要在主线程上,上面方法中成功和失败的快都是回调的主线程,API的设计者也是想让我们在网络请求之后通过主线程回调处理UI的事情,具体内部处理更复杂,这里不再深究,感兴趣同学自己研究。
不是所有的情况下都要主动控制线程,有时候应该让API的调用者自己选择在哪个线程上执行相关操作,例如NSNotificationCenter在设计的时候就提供方法,让API调用者自己选择线程,如下:

- (id <NSObject>)addObserverForName:(nullable NSString *)name 
                             object:(nullable id)obj 
                              queue:(nullable NSOperationQueue *)queue 
                         usingBlock:(void (^)(NSNotification *note))block 

40. 用快引用其所属对象时不要出现保留环

前面已经多次介绍过使用快的时候会出现循环引用的情况,并在某些情况下给出了一些解决方案,这一小节还是要提到这个问题,在使用快的时候要注意这个问题,尤其是在快中引用实例变量,间接的就在快中引入了self,这些都是要特别注意的地方。
在API中用到completion handler这样的回调快的时候,特别容易出现保留环,这个时候我们在设计API的时候就要考虑到这些问题,而不应该把消除保留环的工作留给调用者去处理,至于具体的保留环情况,在写代码的时候要谨慎排查,在适当的地方解决问题,这里不再过多举例。

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

OC中多个线程在争夺一个资源的时候会出现问题,这种情况下就需要使用某种机制解决问题,在GCD之前,通常用两种方法,第一种是使用“同步块”,另外一种是使用NSLock对象加锁,下面介绍一下这两种方法。
第一种,同步块

- (void)synchronizedMethod{
    @synchronized (self) {
        // 安全区域
    }
}

这就是给指定的对象添加一个同步锁,在代码结尾处同步锁就释放了,上面的例子是针对self对象,这样就可以保证每个对象实例都能不受干扰地运行synchronizedMethod方法,但是synchronized的滥用是很消耗内存的(所有的加锁都非常消耗内存),不要频繁的加锁。
第二种,NSLock

_lock = [[NSLock alloc] init];
- (void)synchronizedMethod{
    [_lock lock];
    // 安全区域
    [_lock unlock];
}

和第一种方法类似,都是加锁,只不过这个控制的范围更加灵活,另外还用一种NSRecursiveLock(递归锁),这种锁线程可以多次持有,并且不会出现死锁现象。
GCD的出现能够更简单、更高效的给代码加锁,下面通过一个属性的例子介绍这种方法,通常我们可以通过atomic来修饰属性,做到线程安全,下面的例子不通过atomic修饰属性的线程安全,而是通过自定义属性的设置方法,做到属性的线程安全。首先看一下在同步块的情况下我们的写法:

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

这样写做到了线程安全,但是如果有很多属性的时候都用synchronized会产生我们不想要的效果,因为每一个同步块都要等到上一个同步块执行完毕,另外这种写法不能保证绝对的线程安全,当同一个线程多次调用获取方法的时候,每次可能返回不同的结果,因为在两次访问期间,其他的线程可能会写入新的值。
下面看一下GCD的串行同步队列,将读取和写入操作安排在同一个队列里,保证数据的同步:

_syncQueue = dispatch_queue_create("com.eoc.syncQueue", NULL);
- (void)setSomeString:(NSString *)someString{
    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

这种写法是我们把设置和读取都安排在一个事先创建好的队列中,这样所有针对属性的操作都在一个队列中,就做到同步了,GCD是一套纯C的语言,他的加锁任务都是在底层来实现的,于是会有许多优化。另外设置操作不一定非要同步,异步操作也可以,这时候只要将设置方法改成下面这样:

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

注意dispatch_async和dispatch_sync两个方法的不同
异步操作可以提升设置方法的速度,并且可以保证读取和写入操作会按顺序执行,但是异步会让程序变慢,因为异步派发是,需要拷贝快,至于这个拷贝时间和设置时间那个更短则取决于快的大小。
如果想要更高效的代码,可以采用并发队列,不过这个并发是获取方法,获取方法和设置方法之间不能并发执行,结合GCD中的并发队列和栅栏快可以如下设计代码:

- (void)setSomeString:(NSString *)someString{
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

获取方法也可以采用同步的栅栏快,效率可能会更高,至于具体怎么组合,自己可以根据代码实际测一下。

42. 多用GCD,少用performSelector系列方法

GCD出现之前performSelector系列方法是相当重要的,这几个方法可以使得在任何时候调用任何方法都变的可能,这系列最大的特点就是可以动态的执行方法,在运行期根据需要执行合适的方法,而不是在编译期就固定了,如下:

SEL selector;
if (/* 条件 */) {
    selector = @selector(foo);
} else if (/* 条件 */){
    selector = @selector(bar);
} else {
    selector = @selector(baz);
}
[object performSelector:selector];

但是在ARC下编译代码会出现内存泄漏的警告,因为编译器不知道要执行哪个方法,也就不知道方法签名和返回值(也可能没有返回值),而且由于不知道方法名,编译器不能按照常规的内存管理来判断返回值是不是应该释放,基于这些,在此种情况下ARC不执行释放操作,这样就会产生内存泄漏,看下面的例子

SEL selector;
if (/* 条件 */) {
    selector = @selector(newObject);
} else if (/* 条件 */){
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

前面小节介绍过copy、new开头的方法名的特殊内存处理机制,上面的例子中,如果调用的是前两个方法中的一个,那么ret对象应该由这段代码释放,如果是调用第三种方法,则无需释放(在ARC和非ARC下都是如此),这就产生了问题,并且这种问题很难发现。
另一个使用performSelector系列方法的局限就是performSelector系列方法返回的是id类型(指向OC对象的指针),如果方法返回的是整形或者浮点型就需要做转换处理,如果返回的C语言结构体则不能用performSelector系列方法。
列举几个performSelector系列方法:

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

从这可以看出更大的局限性,传入的参数必须是对象,并且最多只能带两个参数,参数限制是performSelector系列方法的通病,虽然我们可以通过编写字典的方法,把参数打包,但是这样做很麻烦,performSelector系列方法提供的功能,在GCD中基本都可以查找到替代方法,并且执行起来不受参数等条件限制,下面是一个延后执行的例子:

// performSelector系列方法
[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(), ^{
     [self doSomething];
 });

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

OC中,这种队列操作的不光GCD采用,还有一种技术叫做NSOperationQueue,两者有很多差别,其中一个特别明显的就是,NSOperationQueue针对OC对象,GCD则是纯C的API,下面介绍几点NSOperationQueue优势与GCD的地方
1.在运行任务前在NSOperation上调用cancel方法可以手动取消操作(运行后就无法执行取消方法),而GCD队列无法取消,GCD的原则是安排好任务后就不管了,如果想取消需要格外编写很多代码。
2.指定操作间的关系,NSOperation对象之间可以通过addDependency:方法使对象之间产生依赖关系,上一个操作不完成,下一个操作不会发生,并且可以依赖多个操作,开发者可以自定义之间的依赖关系。
3.通过键值观察机制监控NSOperation对象,NSOperation对象提供了许多属性对进度进行观测,这样相对于GCD可以做到更精细的控制。
4.指定操作优先级,指的是同一个队列中可以指定特定NSOperation对象的操作优先级,高的先执行,低的后执行,GCD中也有优先级,但是那是针对队列的,而不是针对具体的快。
5.重用NSOperation对象,系统提供了一些NSOperation对象的子类供我们调用,也可以自定义对象。
上面的几点基本就是操作队列(NSOperationQueue)和派发队列(GCD)的区别,不同的情况选择合适的代码很重要,有人遵循使用高层API的原则,其实不应该盲从,从自己的代码出发,选择最合适的。

44. 通过Dispatch Group机制,根绝系统资源状况类执行任务

dispatch group就是任务分组,每个组总有多个并发执行的任务,可以针对这一组任务进行操作,所有任务完成之后会有回调函数,这些特性可以很好的被利用,例如,把压缩一系列任务放到一个组来完成,简单介绍一下这个分组机制:
创建一个dispatch group:

dispatch_group_t dispatch_group = dispatch_group_create();

dispatch group是一个数据结构,彼此之间没有区别,想把任务编组,有两种办法:
第一种:

void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

dispatch_group_async方法可将block快添加到指点的组中。
第二种:

void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

dispatch_group_enter方法使分组里正在执行的任务数递增,后面方法递减,类似引用计数机制,增加后就要减少,否则这一组的任务就永远执行不完。
下面的方法等待dispatch group执行完毕:

long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

第一个参数是要等待的dispatch group,第二个参数是等待超时时间(应该阻塞线程多久),这个时间可以是无限DISPATCH_TIME_FOREVER(无限等待下去),若执行dispatch group所用时间小于设置时间,则返回0,否则返回非0值。
除了上面的方法外,等待执行完毕还有一个方法:

void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

第一个参数是要等待的dispatch group,第二个参数是dispatch group执行完毕后将要执行的代码块所在的队列,第三个参数是dispatch group执行完毕后将要执行的代码块。
下面举个例子,令数组中的每个对象都执行某项任务,然后等全部执行完毕后再执行其他任务:

dispatch_queue_t lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in arrayA) {
     dispatch_group_async(dispatchGroup, lowQueue, ^{
         /* 数组对象要进行的操作 */
     });
}
for (id object in arrayB) {
     dispatch_group_async(dispatchGroup, highQueue, ^{
         /* 数组对象要进行的操作 */
    });
}
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup, notifyQueue, ^{
     /* dispatch group执行完毕后执行的代码块 */
});

上面的例子中dispatch group添加了两个队列,这些队列可以有不同的优先级,从上面的例子我们可以发现,我们不用再为线程的事担忧,并且具体的资源怎么分配都是由GCD底层内部自动帮我们管理的,这样我们只要专注于业务逻辑代码就行了。
GCD中还有一个方法可以实现上面的效果

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

该方法会将最后一个参数中的代码块反复执行一定次数,这个次数是自己制定的,每次传给快的参数值都会递增,从0开始到我们制定的个数减1。怎么实现这里不在举例。
未必总要使用dispatch group,有一些东西可以通过不同的道路去实现。

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

这一小节主要介绍一下通过dispatch_once这个方法编写“只需要执行一次的线程安全代码”,以最常用的单利的写法为例,通过这个方法编写单利代码,可以简化代码并保证线程绝对安全,并只执行一次,下面是单利的写法:

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

标记声明成static是因为每次调用标记必须相同,变量声明成static是因为每次调用变量的时候都复用这个变量,而不是从新生成。

46. 不要使用dispatch_get_current_queue

dispatch_get_current_queue方法的作用是返回当前正在执行的队列,但是用的时候要小心,并且苹果也不推荐这个方法的使用,所以在iOS6.0之后,官方废弃了这个方法,书中举了一个例子,但是太极端,这里不做过多介绍了,dispatch_get_current_queue方法可以在调试的时候用,并且很方便,但是真正的发布代码中不建议用这个方法。

上一篇下一篇

猜你喜欢

热点阅读