编写高质量代码的52个有效方法(六)—块与大中枢派发

outstanding.png

当前多线程编程的核心就是“块”(block)与“大中枢派发”(Grand Central Dispatch,GCD)。

37.理解“块”这一概念

1.块的基础知识

块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。

^{
    //Block implementation
}

//块类型的语法结构及事例

return_type (^block_name)(parameters)

//使用
block_name(parameters);

eg:

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

//使用
int result = addBlock(3,5);

块的强大之处在于:在声明它的范围里,所有变量都可以为其所捕获。这就是说,块所在的范围里的全部变量,在块里依然可用。默认情况下,为块所捕获的变量,是不可以在块里修改的,声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。对于实例变量,块总是能够修改的,所以对于要修改的实例变量则无需加__block.

块的保留环:块里面使用了实例变量或self,self也是个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致保留环。

2.全局块、栈块及堆块

定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。

void (^block)();

if(/* some condition */){
    block = ^{
        NSLog(@"Block A");
    }
}else{
    block = ^{
        NSLog(@"Block B");
    }
}

block();

定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对于的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

为解决此问题,可给块对象发送copy消息来拷贝之。这样的话,就可以把块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。

//全局块
void (^blocks)(void) = ^{
//    self.propert = @"string";//会报错,不会捕捉任何状态
};

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

}

除了“栈块”和“堆块”之外,还有一类块叫做“全局块”(global block)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无需有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局块的拷贝操作是空操作,因为全局块决不可能为系统所回收。

要点:

  1. 块是C、C++、Objective-C中的词法闭包。
  2. 块可接受参数,也可返回值
  3. 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。

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

与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不是放在右侧。鉴于此,我们应该为常用的块类型起个别名。为了隐藏复杂的块类型,需要用到C语言中名为“类型定义”(type definition)的特性。typedif关键字用于给类型起个易读的别名。

typedef int(^BlockName)(BOOL flag, int value);

BlockName block = ^(BOOL flag, int value){
    // block implementation
}

要点:

  1. 以typedef重新定义块类型,可令块变量用起来更加简单
  2. 定义新类型时应遵循现有的命名习惯,勿使用其名称与别的类型相冲突
  3. 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应typedef中的块签名即可,无需改动其他typedef。

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

设计API时,对于回调的选择有多种,选用合适的回调方式能够让我们的代码更加清晰整洁。

要点:

  1. 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明
  2. 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起
  3. 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

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

要点:

  1. 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题
  2. 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。

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

在Objective-C中,如果有多个线程要执行同一份代码,那么有时可能会出现问题。这种情况下,通常要使用锁来实现某种同步机制,在GCD出现之前,有两种办法,第一种是采用内置的“同步块”(synchronization block);第二种是直接使用NSLock对象;

//同步块(synchronization block)

- (void)synchronizedMehtod{
    @synchronized(self){
        // safe 安全的执行代码
    }
}

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。这么写通常没错,因为它可以保证每个对象实例都能不受干扰地运行其synchronizationMehtod方法。然而,滥用@synchronized(self)则会降低代码效率,因为共用同一个锁的那些同步块,都必须按照顺序执行。若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。

//NSLock对象

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod{

    [_lock lock];
    
    //safe code
    
    [_lock unlock];
}

也可以使用NSRecursiveLock这种“递归锁”(recursive lock),线程能够多次持有该锁,而不会出现死锁(deadlock)现象。这两种方法都很好,不过也有其缺陷。比方说,在极端情况下,同步块会导致死锁,另外,效率也不见得很高,而如果直接使用锁对象的话,一旦遇到死锁,就会非常麻烦。

有种简单而高效的办法可以代替同步块或锁对象,那就是使用“串行同步队列”。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。

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

- (NSString *)someString{
    __block NSString *localString;
    dispatch_sync(_syncQueue,^{
        localString = _someString;
    });
    return localString;
}

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

此模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。为了shi块代码能够设置局部变量,获取方法中用到了__block语法,若是抛开这一点,那么这种写法要比前面那些更为整洁。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的,于是能够做许多优化。因此,开发者无需担心那些事,只要专心把访问方法写好就行。

要点:

  1. 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块或NSLock对象更简单
  2. 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的形成
  3. 使用同步队列及栅栏块,可以令同步行为更加高效

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

SEL selector = @selector(test);
[self performSelector:selector];

报警告:PerformSelector may cause a leak because its selector is unknown

原因在于:编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

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

传入的参数都是id类型,所以传入的参数必须是对象才行,基本数据类型不行;再者,返回值也是id。还有一个问题就是,多个参数的传递,我们可能需要使用字典等集合来进行封装再进行传递。

如果改为其他替代方案,那就不受这些限制了。最主要的替代方案就是使用块。

//using performSelector
    [self performSelector:@selector(doSomeThing) withObject:nil afterDelay:5.0];
    
    //using 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];
    });
//using performSelector
    [self performSelectorOnMainThread:@selector(doSomeThing) withObject:nil waitUntilDone:NO];
    
    //using GCD
    //if waitUntilDone is YES,then dispatch_sync
    dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomeThing];
    });

要点:

  1. performSelector系列方法在内存管理方法容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
  2. performSelector系列方法所能处理的选择子太多局限了,选择子的返回值类型及发送给方法的参数个数都受到限制
  3. 如果想把任务放另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。

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

出了GCD之外,还有一种技术叫做NSOperationQueue,它虽然与GCD不同,但是却与之相关,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作也能并发执行。区别:GCD是纯C的API,而操作队列则是Objective-C的对象。在GCD中,任务用块来表示。用NSOperationQueue类的“addOperationWithBlock:”方法搭配NSBlockOperation类来使用操作队列,其语法与GCD方式类似。使用NSOperation及NSOperationQueue的好处如下:

  1. 可以取消某个操作。
  2. 指定操作间的依赖关系。
  3. 通过键值观测机制监控NSOperation对象的属性。
  4. 指定操作的优先级。
  5. 重用NSOperation对象

NSNotificationCeter使用的就是操作队列而非派发队列


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


[[NSNotificationCenter defaultCenter] addObserverForName:(nullable NSNotificationName) object:(nullable id) queue:(nullable NSOperationQueue *) usingBlock:^(NSNotification * _Nonnull note) {
        
    }];

要点:

  1. 在解决多线程与任务管理问题时,派发队列并非唯一方案。
  2. 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码

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

GCD常见方法

//创建队列组
dispatch_group_t 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执行完毕
//arg0:等待的队列组 arg1:等待时间 DISPATCH_TIME_FOREVER表示一直等着dispatch_group执行完
//返回类型long,如果执行group所需的时间小于timeout,则返回0,否则返回非0值
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    
//通知队列组执行完后在指定的队列进行回调 与上面的方法相比,在非主队列中不会阻塞
void dispatch_group_notify(dispatch_group_t group,
                          dispatch_queue_t queue,
                          dispatch_block_t block);
    
    
//GCD遍历集合 该方法会持续阻塞,从0开始,直至iterations - 1
 void dispatch_apply(size_t iterations, dispatch_queue_t queue,DISPATCH_NOESCAPE void (^block)(size_t));

要点:

  1. 一系列任务可归入一个dispatch group中。开发者可以在这组执行完毕时获得通知。
  2. 通过dispatch group,可以在并发式派发队列中同时执行多项任务。此时GCD会根据系统资源来调度这些并发执行的任务。开发者若自己来实现此功能,则需要编写大量代码。

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

//单例中的dispatch_once使用

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
        
    });

要点:

  1. 经常需要编写“只需要执行一次的线程安全代码”。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
  2. 标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的。

46.不要使用dispatch_get_current_queue

要点:

  1. dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试使用
  2. 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念
  3. dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决



PDF格式的资料来自iOS开发交流群、感觉作者的贡献,对于知识的系统归纳总结很有帮助。
编写高质量代码的52个有效方法
编写高质量代码的52个有效方法(一)—熟悉OC
编写高质量代码的52个有效方法(二)—对象、消息、运行期
编写高质量代码的52个有效方法(三)—接口与API设计
编写高质量代码的52个有效方法(四)—协议与分类
编写高质量代码的52个有效方法(五)—内存管理
编写高质量代码的52个有效方法(六)—块与大中枢派发
编写高质量代码的52个有效方法(七)---系统框架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容