前言
很久以前,手机的性能取决于处理器的处理速度。在经过很长的一段时间发展之后,单个处理器的处理效率几乎达到了极限。这时候多核处理器就诞生了,使得手机有了同时执行多个任务的能力。在单核时代,使用多线程技术更多时候是为了避免耗时操作堵塞了主线程。而在多核时代,多线程技术才真正完成了提升执行效率的工作。
iOS
提供了包括pthread_t
在内的四种多线程方案,其中GCD
作为苹果爸爸重视的多线程技术从诞生为止一直受到iOS
开发者的热爱,其最大的缺点可能在于基于C
语言编写的代码不那么的易于编写。
早前,笔者曾经对GCD
语法进行了简单的面向对象封装,之后可以通过下面的代码调用多线程方案:
[LXDQueue executeInMainQueue: ^{
NSLog(@"Execute task in main queue");
}];
[LXDQueue executeInGlobalQueue: ^{
NSLog(@"The code will be executed after 1 second");
} delay: 1];
随着开发经历的增长,也逐渐感觉到了这样简单的语法封装对GCD
本身的利用还不够好。在学习了YYKit
的源码之后,决定对日常使用的多线程方案进行一次更好的封装。
技术总结
虽然本文对于GCD
的封装使用到的大部分技术点已经记载在YYKit学习笔记,但是这里还是要简单的提一下所用的技术点:
-
锁技术
其中队列轮询查找用到了OSAtomicIncrement32
原子操作
派发任务的状态修改使用dispatch_semaphore_t
信号同步 -
函数指针
虽然block
无疑是一种更常用的回调手段,但是函数指针的优点在于无需考虑循环引用 -
函数重载
这里涉及到了封装过程中遇到的一个坑,最终采用__attribute__
修饰函数来实现重载 -
懒加载
对于并发队列结构的创建总是延后创建的,只有真正用到的时候才会分配内存 -
QoS
QoS
的分级对于高优先级的线程资源占据有着非常突出的表现
虽然每个技术点都有很多的东西可以发掘,但是笔者不会也暂时没有能力去深入这些技术点。
代码实现
本文的GCD
封装基于YYDispatchQueuePool
源码修改而成,有LXDDispatchAsync
以及LXDDispatchOperation
两个文件。前者提供了多个多线程函数接口;后者基于前者进行面向对象封装,提供了取消任务的接口。一个无解的问题是一旦任务开始执行了,那么就无法取消操作。目前的解决方案是block
回调中提供了一个判断任务取消的标记,使用者调用时需要判断是否应该中断任务
-
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];
}
}
}
另外,文件对外只暴露C
函数接口调起任务派发,替换YYDispatchQueuePool
的类调用。
踩坑经历
GCD
封装要考虑到线程竞争频发的可能性,使用一个静态的信号量变量来控制线程同步操作是很有必要的。当然重复的wait
和signal
函数看着也不够优雅,于是决定生成内联函数来完成线程同步的工作。
最开始函数接收一个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);
}
线程获取
这几天使用过程中发现在适配性上还有些不足,主要是在将多线程方案移植到LXDAppMonitor后,想要获取派发任务所在线程有些麻烦,于是添加了只读的disaptch_queue_t
属性,在使用的时候可以随时访问:
@interface LXDDispatchOperation : NSObject
@property (nonatomic, readonly) BOOL isCanceled;
@property (nonatomic, readonly) dispatch_queue_t queue;
+ (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
中学习了大量的技巧,也巩固了自己的知识点。毫无疑问,YYKit
是国内最优秀的源码,非常值得想要深入iOS
开发的小伙伴们去仔细研读。
本文demo:LXDDispatchOperation