Block与GCD

“块”的概念

  • 块的基础知识

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

    ^{
        //Block
    }
    

    块的本质是一个值,而且有其相关类型。与int、float或OC对象一样,也可以把块赋值给变量,然后像使用其他变量那样使用它。块的语法与函数指针近似:

    //块的语法结构
    return_type (^block_name)(parameters)
    
    //块的简单使用方法
    //定义块
    int (^addBlock)(int a, int b) = ^(int a, int b){
        return a + b;
    };
    //使用块
    int add = addBlock(2, 5);
    

    块可以捕获在它声明的范围内的所有变量。默认情况下,被块所捕获的变量,是不可以在块里修改的,不过,声明变量时可以加上_block修饰符,例如_block NSIntger count = 0,这样就可以在块内修改count变量了。

  • 小心循环引用

    如果将块定义在OC类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加_block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。也就是说,只要你在块中调用到了属性值,那么这个块就会捕获这个类本身也就是self。

    self也是一个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致“保留环”。如果块中没有显式地使用 self 来访问实例变量,那么块就会隐式捕获 self,这很容易在我们不经意间造成循环引用,编译器会给出警告。假设有实例变量name,块中最好使用 self->_nameself.name 来访问。

    self.block = ^{
        _name = @"Block";
        // ⚠️ Block implicitly retains ‘self’; explicitly mention ‘self’ to indicate this is intended behavior
    }
    
  • 块的内存布局

    • isa 指针指向 Class 对象

    • invoke 变量是个函数指针,指向块的实现代码。函数原型至少需要接受一个void*型的参数,此参数代表块

    • descriptor 变量是指向结构体的指针,其中声明了块对象的总体大小,还声明了保留和释放捕获的对象的 copy 和 dispose 这两个函数所对应的函数指针

    • 块还会把它所捕获的所有变量都拷贝一份,放在 descriptor 变量的后面。捕获了多少个变量,就要占据多少内存空间。

  • Block按内存位置分类

    根据block在内存中的位置,block被分成三种类型:

    1. NSGlobalBlock 全局块

      这种块运行时无需获取外界任何状态,块所使用的内存区域在编译器就可以完全确定,所以该块声明在全局内存中。如果全局块执行copy会是一个空操作,相当于什么都没做,因为全局块决不可能被系统所回收,其实际上相当于单例。

      eg:

      void (^block)() = ^{
          NSLog(@"I am a NSGlobalBlock");
      }
      
    2. NSStackBlock 栈块
      栈块保存于栈区,超出变量作用域,栈上的block以及__block变量都会被销毁。

      eg:

      NSString *name = @"NSStackBlock";
      void (^testBlock)() = ^{
          NSLog(@"I am a %@", name);
      };
      NSLog(@"%@", testBlock);
      

      打印结果:

      <__NSMallocBlock__: 0x10580bc10>
      

      这个结果是在ARC下编译的,ARC下编译器编译时会帮你优化,自动帮你加上了copy操作。如果没有copy操作的打印结果应该是:

      <__NSStackBlock__: 0x10580bc10>
      
      void (^block)();
      if ( /* some condition */ ) {
          block = ^{
              NSLog(@"Block A");
          };
      } else {
          block = ^{
              NSLog(@"Block B");
          };
      }
      block();
      

      上面的代码有危险,定义在 if 及 else 中的两个块都分配在栈内存中,当出了 if 及 else 的范围,栈块可能就会被销毁。如果编译器覆写了该块的内存,那么调用该块就会导致程序崩溃。若是在 ARC 下,上面 block 会被自动 copy 到堆,所以不会有问题。但在 MRC 下我们要避免这样写。

    3. NSMallocBlock 堆块
      堆block内存保存于堆区,在变量作用域结束时不受影响。通过之前在ARC下的输出已经看到了__ NSMallocBlock __。所以我们在定义block类型的属性时常常加上copy修饰,这个修饰其实是多余的,系统在ARC的时候已经帮我们做了copy,但是还是建议写上copy。这样的话,就可以把块从栈复制到堆了。并且拷贝后的块,可以在定义它的那个范围之外使用。

为常用的块类型创建typedef

  • 以 typedef 关键字重新定义块类型

    每个块都具备其 “固有类型”(由其参数及返回值组成),因而可将其赋值给适当类型的变量。

    typedef int(^EOCSomeBlock)(BOOL flag, int value);
    
    EOCSomeBlock block = ^(BOOL flag, int value) {
        // Implementation
    }
    
  • 以 typedef 关键字重新定义块类型的好处

    • 易读,定义、声明块方便
      定义块变量语法与定义其他类型变量的语法不同,而定义方法参数所用的块类型语法又和定义块变量语法不同。这种语法很难记,所以以 typedef 关键字重新定义块类型,可令块变量用起来更加简单,起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。
    • 重构块的类型签名时很方便
      当修改 typedef 类型定义语句时,使用到这个类型定义的地方都无法编译,可逐个修复。而若是不用类型定义直接写块类型,修改的地方就更多,而且很容易忘掉其中一两处的修改而引发难于排查的 bug。
  • 要注意的

    • 最好在使用块类型的类中定义这些 typedef,而且新类型名称还应该以该类名开头,以阐明块的用途。
    • 可以根据不同用途为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应的 typedef 中的块签名即可,无须改动其他 typedef。

用 handler 块降低代码分散程度

  • 当异步方法在执行完任务之后,需要以某种手段通知相关代码时,我们可以使用委托模式,也可以将 completion handler 定义为块类型。在对象的初始化方法中添加 handle 块参数,在创建对象时就将 handler 块传入。这样可以降低代码分散程度,令代码更加清晰整洁,令 API 更紧致,同时也令开发者调用起来更加方便。

    [fetcher startWithCompletionHandler:^(NSData *data) {
        self->_fetchedFooData = data;
    }];
    
  • 委托模式的缺点:在有多个实例需要监控时,如果采用委托模式,那么就得在 delegate 回调方法里根据传入的对象来判断执行何种操作,回调方法的代码就会变得很长。

    - (void)networkFetcher:(EOCNetworkFetcher *)networkFetcher didFinishWithData:(NSData *)data {
        if (networkFetcher == _fooFetcher) {
            ...
        } else if (networkFetcher == _barFetcher) {
            ...
        }
        // etc.
    }
    

    改用 handler 块来实现,可直接将块与相关对象放在一起,就不用再判断是哪个对象。

    [_fooFetcher startWithCompletionHandler:^(NSData *data) {
        ...
    }];
    [_barFetcher startWithCompletionHandler:^(NSData *data) {
        ...
    }];
    
  • 当异步的回调有成功和失败两种情况时,可以有两种 API 设计方式:

    • 用两个 handler 块来分别处理成功和失败的回调,这样可以让代码更易读,还可以根据需要对不想处理的回调块参数传 nil。

      [fetcher startWithCompletionHandler:^(NSData *data) {
          // Handle success
      } failureHander:^(NSError *error) {
          // Handle failure
      }];
      
    • 用一个 handler 块来同时处理成功和失败的回调,并给块添加一个 error 参数来处理失败情况。

      [fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
          if (error) {
              // Handle failure
          } else {
              // Handle success
          }
      }];
      
      • 缺点:全部逻辑放在一起会令块变得很长且复杂
      • 优点:更灵活,成功和失败情况有时候可能要统一处理
  • 有时需要在相关时间点执行回调参数,就比如下载时想在每次有下载进度时都得到通知,这种情况也可以用 handler 块处理。

  • 设计 API 时,如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。可以参照通知的 API:

    - (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;
    

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

  • NSObject 定义了几个 performSelector 系列方法,可以让开发者随意调用任何方法,可以推迟执行方法调用,也可以指定执行方法的线程等等。

    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    // ...
    
  • performSelector:方法有什么用处?

    如果你只是用来调用一个方法的话,那么它确实有点多余

    用法一:selector 是在运行期决定的

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

    用法二:把 selector 保存起来等某个事件发生后再调用

  • performSelector:方法的缺点:

    1. 存在内存泄漏的隐患:

      由于 selector 在运行期才确定,所以编译器不知道所要执行的 selector 是什么。如果在 ARC 下,编译器会给出警告,提示可能会导致内存泄漏。

      waring: PerformSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
      

      由于编译器不知道所要执行的 selector 是什么,也就不知道其方法名、方法签名及返回值等,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC 采用了比较谨慎的做法,就是不添加释放操作,然而这样可能会导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

      如果是调用以 alloc/new/copy/mutableCopy 开头的方法,创建时就会持有对象,ARC 环境下编译器就会插入 release 方法来释放对象,而使用 performSelector 的话编译器就不添加释放操作,这就导致了内存泄漏。而其他名称开头的方法,返回的对象会被添加到自动释放池中,所以无须插入 release 方法,使用 performSelector 也就不会有问题。

    2. 返回值只能是 void 或对象类型
      如果想返回基本数据类型,就需要执行一些复杂的转换操作,且容易出错;如果返回值类型是 C struct,则不可使用 performSelector 方法。

    3. 参数类型和个数也有局限性
      类型:参数类型必须是 id 类型,不能是基本数据类型;
      个数:所执行的 selector 的参数最多只能有两个。而如果使用 performSelector 延后执行或是指定线程执行的方法,那么 selector 的参数最多只能有一个。

  • 使用 GCD 替代 performSelector

    1. 如果要延后执行,可以使用 dispatch_after
    2. 如果要指定线程执行,那么 GCD 也完全可以做到

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

  • 在解决多线程与任务管理问题时,我们要根据实际情况使用 GCD 或者 NSOperation,如果选错了工具,则编写的代码就会难以维护。以下是它们的区别:
    GCD:GCD 是 iOS4.0 推出的,主要针对多核 CPU 做了优化,是 C 语言的技术 GCD 是纯 C 的 API; GCD 是将任务(block)添加到队列(串行/并发/全局/主队列),并且以同步/异步的方式执行任务; GCD 提供了一些 NSOperation 不具备的功能:① 队列组 ② 一次性执行 ③ 延迟执行
    NSOperation :NSOperation 是 iOS2.0 推出的,iOS4 之后重写了 NSOperation,底层由 GCD 实现; NSOperation 是 OC 对象; NSOperation 是将操作(异步的任务)添加到队列(并发队列),就会执行指定操作; NSOperation 里提供的方便的操作:① 最大并发数 ② 队列的暂停/继续/取消操作 ③ 指定操作之间的依赖关系(GCD 中可以使用同步实现)

  • 使用 NSOperation 和 NSOperationQueue 的优势:

    • 取消某个操作
      可以在执行操作之前调用 NSOperation 的 cancel 方法来取消,不过正在执行的操作无法取消。iOS8 以后 GCD 可以用 dispatch_block_cancel 函数取消尚未执行的任务,正在执行的任务同样无法取消。
    • 指定操作间的依赖关系
      使特定的操作必须在另外一个操作顺利执行完以后才能执行。
    • 通过 KVO 监控 NSOperation 对象的属性
      在某个操作任务变更其状态时得到通知,比如 isCancelled、isFinished。而 GCD 不行。
    • 指定操作的优先级
      指定一个操作与队列中其他操作之间的优先级关系,优先级高的操作先执行,优先级低的则后执行。GCD 没有直接实现此功能的办法。
    • 重用 NSOperation 对象
      可以使用系统提供的 NSOperation 子类(比如 NSBlockOperation),也可以自定义子类。
  • GCD 任务用块来表示,块是轻量级数据结构,而 NSOperation 则是更为重量级的 Objective-C 对象。虽说如此,但 GCD 并不是最佳方案。有时候采用对象所带来的开销微乎其微,反而它所到来的好处大大反超其缺点。另外,“应该尽可能选用高层 API,只在确有必要时才求助于底层” 这个说法并不绝对。某些功能确实可以用高层的 API 来做,但这并不等于说它就一定比底层实现方案好。要想确定哪种方案更佳,最好还是测试一下性能。

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

  • GCD 队列组,又称 “调度组”,实现所有任务执行完成后有一个统一的回调。
    GCD 有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务,使用队列组,既可以并发执行一系列给定的任务,又能在这些给定的任务全部执行完毕时得到通知。
    有时候我们需要在多个异步任务都并发执行完毕以后再继续执行其他任务,这时候就可以使用队列组。

  • 创建一个队列组。

    dispatch_group_t dispatch_group_create(void);
    
  • 异步执行一个 block,并与指定的队列组关联。

    void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    
  • 也可以通过以下两个函数指定任务所属的队列组。

    需要注意的是,调用了 dispatch_group_enter 之后必须要有与之对应的 dispatch_group_leave,如果没有的话那么这个队列组的任务就永远执行不完。

    void dispatch_group_enter(dispatch_group_t group);
    void dispatch_group_leave(dispatch_group_t group);
    
  • 同步等待先前 dispatch_group_async 添加的 block 都执行完毕或指定的超时时间结束为止才返回。可以传入 DISPATCH_TIME_FOREVER,表示函数会一直等待任务都执行完,而不会超时。

    注意:dispatch_group_wait 会阻塞线程

    // @return long 如果 block 在指定的超时时间内完成,则返回 0;超时则返回非 0。
    long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    
  • 等待先前 dispatch_group_async 添加的 block 都执行完毕以后,将 dispatch_group_notify 中的 block 提交到指定队列。

    dispatch_group_notify 不会阻塞线程

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

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

  • dispatch_once 可以用来实现 “只需执行一次的线程安全代码”。

  • 使用 dispatch_once 来实现单例,它比 @synchronized 更高效。它没有使用重量级的同步机制,而是采用 “原子访问” 来查询标记,以判断其所对应的代码原来是否已经执行过。

    // 普通单例,线程不安全
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        if (sharedInstance == nil) {
            sharedInstance = [[self alloc]init];
        }
        return sharedInstance;
    }
    // 加锁,线程安全
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        @synchronized(self) {
            if (!sharedInstance) {
                sharedInstance = [[self alloc] init];
            }
        }
        return sharedInstance;
    }
    // dispatch_once,线程安全,效率更高
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        });
        return sharedInstance;
    }
    
  • 对于只需执行一次的 block 来说,每次调用函数时传入的标记都必须完全相同,通常标记变量声明在 static 或 global 作用域里。

不要使用 dispatch_get_current_queue

  • dispatch_get_current_queue 函数返回当前正在执行代码的队列,但从 iOS6.0 开始就已经正式弃用此函数了。我们只应该在调试时使用该函数。

  • dispatch_get_current_queue 函数有个典型的错误用法,就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题,但这样仍然可能会产生死锁。

  • 由于队列间有层级关系,所以通过

    dispatch_get_current_queue
    

    函数来 “检察当前队列是否为执行同步派发所用的队列” 这种办法并不总是奏效。

    什么是 “队列间的层级关系”?


    排在某条队列中的块,会在其上层队列(也叫父队列)中执行,层级里地位最高的那个队列总是全局并发队列。
    上图中,排在队列 B,C 中的块会在 A 里依序执行。于是排在 A、B、C 中的块总是要彼此错开执行。而排在 D 中的块有可能与 A(包括 B、C)的块并行,因为 A 和 D 的目标队列是个并发队列。

    比方说,开发者可能会认为排在队列 C 中的块一定会在 C 中执行,通过 dispatch_get_current_queue函数判断当前队列是 C,就认为在 A 中同步执行该块一定安全,然而该块可能会在队列 A 执行,就导致了死锁。

  • dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用 “队列特定数据” 来解决。

    dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
    dispatch_set_target_queue(queueB, queueA); // 将 B 的目标队列设为 A
    
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CFSTR("queueA");
    // 给队列 A 设置队列特定数据
    dispatch_queue_set_specific(queueA, 
                        &kQueueSpecific, 
                        (void *)queueSpecificValue, 
                        (dispatch_function_t)CFRelease); 
    
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ NSLog(@"NO deadlock"); };
    
        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
        // 根据队列特定数据判断,如果当前是队列 A,则直接执行 Block,否则将同步块提交到队列 A
        if (retrievedValue) { 
            block();
        } else {
            dispatch_sync(queueA, block);
        }
    });
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值