GCD封装

移动设备上的应用性能取决于两个重要因素:硬件性能软件质量

  • 硬件

    通常来说,CPU的核心频率决定了设备的处理速度,频率越高,运行越快。在手机还是单核时代时,硬件对于运行性能的提升更为感官。相同时间内执行任务的数量取决于CPU处理频率

  • 软件

    软件取决于编写的代码质量,良好的代码设计能在相同的时间内完成更多的任务。在手机进入多核时代之后,能够实现真正的并行处理,这时候多线程对于应用性能的提升要比提升CPU频率要明显的多

多线程不是提升应用性能的万能药,过多的线程会造成线程切换的巨大开销。以最常用的GCD为例,多数开发者对于串行队列并行队列的认识可能只停留在如何使用上,没有去了解过线程存在哪些开销。下表展示了创建一个线程的内存和耗时开销:

类型 消耗估算 详情
内核结构体 1KB 存储线程数据结构和属性
栈空间 子线程(512KB)Mac主线程(8MB)iOS主线程(1MB) $堆栈大小必须为4KB的倍数
子线程的最小内存为16KB
创建时间 90微秒 1G内存
Intel 2GHz CPU
Mac OS X v10.5

并行队列

GCD的多线程接口足够的简单,将任务执行的细节完全的隐藏了起来。GCD为我们提供了四种不同优先级的全局并行队列:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self fetchUserDatasFromLocal];
});

GCD维护着一个线程池,关于线程池的知识可以学习这一篇传送门。它会根据当前CPU的负载情况和任务执行情况来动态的增减线程,以此来完成提高效率、减少开销的作用。但是,这种平衡性是存在被打破的风险的:磁盘IO操作中,磁盘响应->数据写入中间是不会占据CPU资源的。如果IO操作发生在并行队列,并且存在大量的IO操作时,GCD会不断的尝试创建新的线程来处理这些任务。线程的数量足够大时,应用会卡死。

另一方面,由于GCD隐藏了大部分的实现细节,我们对于并行队列的控制力是极低的。具体包括两个方面:

  • 线程数量不受控制
  • 执行顺序不受控制

串行队列

GCD提供了串行队列的创建接口,在iOS8之后增加了dispatch_qos_class_t支持设置串行队列的优先级。在更早之前,串行队列只能通过设置target_queue的方式来修改执行优先级。

dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
dispatch_queue_t serialQueue = dispatch_queue_create("com.sindrilin.serial", attr);

相较之下,串行队列的可控性要比并行队列的强。基于串行队列,我们可以实现一套可控性强的并发多线程方案。

代码实现

代码基于YYDispatchQueuePool的实现思路,采用优先级->队列池的一对多设计方案,每个队列池存放着等同于CPU激活核心数的串行队列,尽量让CPU能得到充分的使用。

封装后包括LXDDispatchAsync以及LXDDispatchOperation两个文件。前者提供了多个多线程函数接口;后者基于前者进行面向对象封装,提供了取消任务的接口。一个棘手的问题是一旦任务开始执行了,那么就无法取消任务。暂定的解决方案仿照NSOperation的取消机制,在任务中提供一个cancel标记位,用来判断是否需要停止任务:

  • LXDDispatchAsync.h

      typedef NS_ENUM(NSInteger, LXDQualityOfService) {
          LXDQualityOfServiceUserInteractive = NSQualityOfServiceUserInteractive,
          LXDQualityOfServiceUserInitiated = NSQualityOfServiceUserInitiated,
          LXDQualityOfServiceUtility = NSQualityOfServiceUtility,
          LXDQualityOfServiceBackground = NSQualityOfServiceBackground,
          LXDQualityOfServiceDefault = NSQualityOfServiceDefault,
      };
    		
      void LXDDispatchQueueAsyncBlockInQOS(LXDQualityOfService qos, dispatch_block_t block);
      void LXDDispatchQueueAsyncBlockInUserInteractive(dispatch_block_t block);
      void LXDDispatchQueueAsyncBlockInUserInitiated(dispatch_block_t block);
      void LXDDispatchQueueAsyncBlockInBackground(dispatch_block_t block);
      void LXDDispatchQueueAsyncBlockInDefault(dispatch_block_t block);
      void LXDDispatchQueueAsyncBlockInUtility(dispatch_block_t block);
    
  • LXDDispatchOperation.h

      @class LXDDispatchOperation;
      typedef void(^LXDCancelableBlock)(LXDDispatchOperation * operation);
    	
      /*!
      *  @brief  派发任务封装
      */
      @interface LXDDispatchOperation : NSObject
    	
      @property (nonatomic, readonly) BOOL isCanceled;
    	
      + (instancetype)dispatchOperationWithBlock: (dispatch_block_t)block;
      + (instancetype)dispatchOperationWithBlock: (dispatch_block_t)block inQoS: (NSQualityOfService)qos;
    	
      + (instancetype)dispatchOperationWithCancelableBlock:(LXDCancelableBlock)block;
      + (instancetype)dispatchOperationWithCancelableBlock:(LXDCancelableBlock)block inQos: (NSQualityOfService)qos;
    	
      - (void)start;
      - (void)cancel;
    	
      @end
    

为了保证代码能有更高的效率,YYKit里面经常使用结构体+内联函数的组合来替代类+方法,笔者同样效仿了这点(EOC也提到过static inline的技巧)

#define LXD_INLINE static inline

typedef struct __LXDDispatchContext {
    const char * name;
    void ** queues;
    uint32_t queueCount;
    int32_t offset;
} *DispatchContext, LXDDispatchContext;

LXD_INLINE dispatch_queue_t __LXDQualityOfServiceToDispatchQueue(LXDQualityOfService qos, const char * queueName) {
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_8_0) {
        dispatch_qos_class_t qosClass = __LXDQualityOfServiceToQOSClass(qos);
        dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qosClass, 0);
        return dispatch_queue_create(queueName, attr);
    } else {
        dispatch_queue_t queue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(queue, dispatch_get_global_queue(__LXDQualityOfServiceToDispatchPriority(qos), 0));
        return queue;
    }
}

封装差异

YYDispatchQueuePool向外暴露了类对象,类对象持有一个并发队列结构。在每次使用到的时候重新创建并发队列结构,类对象在不用的时候释放这个结构的内存:

- (instancetype)initWithName:(NSString *)name queueCount:(NSUInteger)queueCount qos:(NSQualityOfService)qos {
    if (queueCount == 0 || queueCount > MAX_QUEUE_COUNT) return nil;
    self = [super init];
    _context = YYDispatchContextCreate(name.UTF8String, (uint32_t)queueCount, qos);
    if (!_context) return nil;
    _name = name;
    return self;
}

- (void)dealloc {
    if (_context) {
        YYDispatchContextRelease(_context);
        _context = NULL;
    }
}

设计理念是很典型的懒加载思想,只有用到了才使用内存。但在大多数情况下,如果应用中用到了多线程,那么总是在后续会多次使用。因此结合实现思路,对队列池的创建以懒加载方式实现,加载后就一直缓存。相较于YYKit的实现,占用内存会更多一些:

LXD_INLINE DispatchContext __LXDDispatchContextGetForQos(LXDQualityOfService qos) {
    static DispatchContext contexts[5];
    int count = (int)[NSProcessInfo processInfo].activeProcessorCount;
    count = MIN(1, MAX(count, LXD_QUEUE_MAX_COUNT));
    switch (qos) {
        case LXDQualityOfServiceUserInteractive: {
            static dispatch_once_t once;
            dispatch_once(&once, ^{
                contexts[0] = __LXDDispatchContextCreate("com.sindrilin.user_interactive", count, qos);
            });
            return contexts[0];
        }
        
        case LXDQualityOfServiceUserInitiated: {
            static dispatch_once_t once;
            dispatch_once(&once, ^{
                contexts[1] = __LXDDispatchContextCreate("com.sindrilin.user_initated", count, qos);
            });
            return contexts[1];
        }
        
        case LXDQualityOfServiceUtility: {
            static dispatch_once_t once;
            dispatch_once(&once, ^{
                contexts[2] = __LXDDispatchContextCreate("com.sindrilin.utility", count, qos);
            });
            return contexts[2];
        }
        
        case LXDQualityOfServiceBackground: {
            static dispatch_once_t once;
            dispatch_once(&once, ^{
                contexts[3] = __LXDDispatchContextCreate("com.sindrilin.background", count, qos);
            });
            return contexts[3];
        }
        
        case LXDQualityOfServiceDefault:
        default: {
            static dispatch_once_t once;
            dispatch_once(&once, ^{
                contexts[4] = __LXDDispatchContextCreate("com.sindrilin.default", count, qos);
        });
            return contexts[4];
        }
    }
}

技术总结

虽然本文对于GCD的封装使用到的大部分技术点已经记载在YYKit学习笔记,但是这里还是要简单的提一下所用的技术点:

  • 锁技术

    其中队列轮询查找用到了OSAtomicIncrement32原子操作 派发任务的状态修改使用dispatch_semaphore_t信号同步

  • 函数指针

    虽然block无疑是一种更常用的回调手段,但是函数指针的优点在于无需考虑循环引用

  • 函数重载

    这里涉及到了封装过程中遇到的一个坑,最终采用__attribute__修饰函数来实现重载

  • 懒加载

    对于并发队列结构的创建总是延后创建的,只有真正用到的时候才会分配内存

  • QoS

    QoS的分级对于高优先级的线程资源占据有着非常突出的表现

踩坑经历

GCD封装要考虑到线程竞争频发的可能性,使用一个静态的信号量变量来控制线程同步操作是很有必要的。当然重复的waitsignal函数看着也不够优雅,于是决定生成内联函数来完成线程同步的工作。

最开始函数接收一个dispatch_block_t类型的参数,后来想到可以增加一个等待超时参数,方便以后做其他修改。因此采用Objective-C++的方式实现函数默认参数,文件改为LXDDispatchOperation.mm

LXD_INLINE void __LXDLockExecute(dispatch_block_t block, dispatch_time_t threshold = dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_FOREVER)) {
    if (block == nil) { return ; }
    static dispatch_semaphore_t lxd_queue_semaphore;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        lxd_queue_semaphore = dispatch_semaphore_create(0);
    });
    dispatch_semaphore_wait(lxd_queue_semaphore, threshold);
    block();
    dispatch_semaphore_signal(lxd_queue_semaphore);
}

执行的时候报了个Apple Mach-O Linker Error的错误,坑爹的是这种错误没有更多的具体信息。

一旦注释掉文件中的C函数调用,这个错误又没了。个人才猜想是:mm文件中不允许调用文件外声明的C函数(如果有错,还望在评论中指出如何修改)于是笔者只能改回m后缀,结合Clang Attributes 黑魔法小记中的技巧,最终通过函数重载实现默认参数功能:

#define LXD_FUNCTION_OVERLOAD __attribute__((overloadable))

LXD_INLINE LXD_FUNCTION_OVERLOAD void __LXDLockExecute(dispatch_block_t block) {
    __LXDLockExecute(block, dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_FOREVER));
}

LXD_INLINE LXD_FUNCTION_OVERLOAD void __LXDLockExecute(dispatch_block_t block, dispatch_time_t threshold) {
    if (block == nil) { return ; }
    static dispatch_semaphore_t lxd_queue_semaphore;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        lxd_queue_semaphore = dispatch_semaphore_create(0);
    });
    dispatch_semaphore_wait(lxd_queue_semaphore, threshold);
    block();
    dispatch_semaphore_signal(lxd_queue_semaphore);
}

最后

通过这段时间简单的阅读,从YYKit中学习了大量的技巧,也巩固了自己的知识点。毫无疑问,YYKit是国内最优秀的源码,非常值得想要深入iOS开发的小伙伴们去仔细研读。

PREVIOUS链式实现数据源
NEXTYYKit学习笔记