前言

Effective Objective C 读书笔记 - 块与大中枢派发

多线程的两大核心:block,GCD

笔记

理解“块”这以概念

  1. block其实也是一个数据类型,块也是对象类型

    1
    2
    3
    void (^someBlock)() = ^{
    // do something here
    }
  2. 块的强大之处在于:在声明块的范围里面,所有的变量都可以为其所捕获,在快内可以使用这些变量

  3. 在Block之前,只能使用函数指针或者是selector

  4. 每个OC对象都占有一个内存区域,块也是对象,下图就是块的内存区域:

    block memory

    1. invoke 是函数指针,指向块的实现代码
    2. descriptor 是一个结构体指针,每个block都有这么一个变量,该结构体包含了块的详细信息:大小等等
    3. 块还会把 捕获到的变量 拷贝一份,注意这里拷贝的是指针
  5. 定义块的时候,块所占的内存是分配在栈中的,来看看下面的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void (^block)(); // 声明一个block,是一个指针
    if (/* some condition */) {
    block = ^{
    // do something
    }
    } else {
    block = ^{
    // do something
    }
    }
    block();

    注释:在if和else中,分别创建了block,这个block是一个栈对象,也就是他们的内存是在栈上面的;然后将该栈内存的首地址赋予block。但是这个对象出了{} ,其内存可能被覆写,那么block变量指向的就不是一个真正的块了

    注意,这里的block是一个指针;和一般的int理解上有所不同

    1
    2
    3
    4
    5
    6
    7
    8
    int a;
    if (/* some condition */) {
    int tmp = 2;
    a = tmp;
    } else {
    int tmp = 3;
    a = tmp;
    }

    这里tmp的内存在出了{}之后也是会被覆写的,但是在{}里面,直接将tmp的内存中的值写到了a的内存中,所以a还是可以使用的。但是,写到block内存中的是这个栈对象的地址,地址还是可以使用的,但是地址里面的内容就不一定是原来的块对象了

    至于,栈和堆的区别可以看看 Objective-C 拾遗:从Heap and Stack到Block 这篇文章

  6. 所以为了避免在栈上的块对象被销毁,就得使用copy方法了。block的copy方法将block从栈拷贝到堆上面,那么这个时候的block就和正常的OC对象一样了,具备引用计数,可以使用ARC来管理了

  7. 5和6分别讲述了 栈块堆块 的区别,还有一种块,叫做 全局块 。这种block不会捕捉外部变量,它的内存在全局内存中,而且对他执行copy操作也只是空操作,类似一个函数

为常用的块类型创建typedef

  1. 定义块变量是非常不符合常理,不好记住,所以要使用 typedef

    1
    2
    3
    4
    typedef int (^EOCSomeBlock)(BOOL flag, int value);
    EOCSomeBlock block = ^(BOOL flag, int value) {
    // block implememtation
    };

    上面的EOCSomeBlock表示 接受2个参数,并返回int的block类型

用handler块降低代码分散程度

  1. 也就是使用block来代替delegate
  2. 还可以同时使用多个block,来处理success或者fail的情况

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

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

  1. dispatch_sync(queue, block); 等待block执行完,再执行后面的语句;dispatch_async(queue, block); 不等待block执行完,立刻执行后续语句。具体参考dispatch_async 和 dispatch_sync 的区别

  2. 来个例子说明一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    _syncQueue = dispatch_queue_create("com.vanney9.queue", NULL);

    - (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{ // 1
    localSomeThing = _someString;
    });
    return localSomeString; // 2
    }

    - (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{ // 3
    _someString = someString;
    });
    return; // 4
    }
    1. get方法使用同步派发(dispatch_sync):因为必须等待block执行完,才能返回localString,如果使用异步派发(dispatch_async)的话,block还没有执行,也就是localString还没有设置,就返回了,那么就返回空值了。。
    2. set方法使用异步派发,因为不需要等待block执行完
  3. 上面的例子使用的queue都是串行队列,也就是必须等待注册到该序列中的前面的block执行完了,才能执行自己的bloc;但是对于读操作来说是可以并行的,只要写操作和其他的操作串行化就可以了。所以可以使用 并行序列+栅栏 来实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 1
    - (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{ // 2
    localSomeString = _someString;
    });
    return localSomeString;
    }

    - (void)setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{ // 3
    _someString = somString;
    });
    }

    注释1:创建了一个并发队列

    注释2:使用并发队列,可以同时执行get方法

    注释3:使用栅栏技术,让写操作执行的时候,队列中的其他操作无法执行

多用GCD,少用performSelector系列方法

  1. 先来说说performSelector方法

    1
    2
    3
    4
    5
    6
    7
    SEL selector;
    if (/* some condition*/) {
    selector = @selector(foo);
    } else {
    selector = @selector(bar);
    }
    [object performSelector:selector];

    动态绑定之上再动态绑定

  2. 为什么不推荐使用呢?因为该方法返回的是id类型,也就是一个指向任意OC对象的指针(在32位架构上占32位,4字节;在64位架构上占64位,8字节)。但是如果selector方法返回int,float等类型时就不好转换了

  3. 使用GCD可以实现:延迟执行,选择特定线程执行

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

  1. 还有一整多线程机制:NSOperationQueue + NSOperation 也就是操作队列, 它也可以实现和GCD相同的功能。其中NSOperationQueue类似GCD;而NSOperation类似block
  2. 操作队列对比GCD的优点:
    1. 可以取消尚未开始执行的NSOperation
    2. 可以指定操作间的依赖关系:可以指定当前的NSOperation在某个NSOperation执行结束后,才开始执行
    3. NSOperation的许多属性适合KVO,通过监听可以更粒子化的监控NSOperation
    4. 可以指定NSOperation的优先级:GCD的优先级是针对队列之间的;但是操作队列的优先级是针对NSOperation的,也就是同一队列里面的block的优先级。当然,操作队列也有线程间的优先级

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

  1. 使用 dispatch group 机制可以将任务分组。调用者可以等待这组任务执行完毕;也可以给直接提供回调函数,立刻进行下面的步骤
  2. 这个特性最重要的一点用途就是:把将要并发执行的多个任务合并为一组,那么就可以知道这些任务什么时候全部执行完毕
  3. dispatch_group_waitdispatch_group_notify 两个函数可以用来执行结束后该做什么,具体参见 iOS与OS X多线程和内存管理 - GCD
  4. 使用并发队列进行多线程任务时,系统会根据当时的情况自动分配线程,无须码农自己来调度

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

  1. 这个常用于单例模式

  2. example:

    1
    2
    3
    4
    5
    6
    7
    8
    + (id)sharedInstance {
    static ECOClass *sharedInstanced = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
    }

    在函数中的static变量:只在函数中可见,但是其生命期为整个程序。具体见 Static variable inside of a function in C

不要使用 dispatch_get_current_queue

参考