YYKit学习笔记
概述
YYKit
是集大成者的第三方表现,堪称国内最优秀的框架。因此,在YYKit
中有太多的技术点值得挖掘思考,本文用来记录YYKit
源码阅读中的心得以及认为有价值的技术点
QoS
The following Quality of Service (QoS) classifications are used to indicate to the system the nature and importance of work. They are used by the system to manage a variety of resources. Higher QoS classes receive more resources than lower ones during resource contention
QoS
是iOS8
之后苹果推出的一套机制,用来修饰多线程派发任务的性质和重要性,GCD
会根据这些性质决定执行任务的时候获取的资源量。QoS
提供了五种不同的任务性质枚举变量,分别是:
-
NSQualityOfServiceUserInteractive
表示用户交互任务,任务优先级高 -
NSQualityOfServiceUserInitiated
用户发起的需要立即得到回应的任务,优先级高 -
NSQualityOfServiceUtility
不需要立刻返回结果的任务,执行时间稍长。比如下载图片,数据请求 -
NSQualityOfServiceBackground
后台任务,对用户不可见,比如数据备份。任务的时间比较长 -
NSQualityOfServiceDefault
默认优先级任务,处于UserInitiated
和Utility
之间
通过dispatch_queue_attr_make_with_qos_class
获取线程任务属性,然后用来配置创建的线程:
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
return dispatch_queue_create("sindrilin.com.user_interactive", attr);
并发队列
通过函数dispatch_get_global_queue
获取到的是并发队列,因此我们并不知道这个队列中的线程数是多少。换句话说,如果派发任务到并发队列中,如果执行大量耗时操作导致线程被锁住的时候,GCD
可能会创建新线程来继续执行任务,这样等待的线程越多,新建的线程也就越来越多。每个线程拥有自己的栈信息,需要分配一定的内核内存和应用内存的空间,具体消耗参照下表
类型 | 消耗估算 | 详情 |
---|---|---|
内核结构体 | 1KB | 存储线程数据结构和属性 |
栈空间 | 子线程(512KB) Mac主线程(8MB) iOS主线程(1MB) |
堆栈大小必须为4KB的倍数 子线程的最小内存为16KB |
创建时间 | 90微秒 | 1G内存 Intel 2GHz CPU Mac OS X v10.5 |
如果遇到大量的耗时派发任务,直接使用并发队列可能会造成CPU
过大的负荷。参照YYKit
的做法,对QoS
每一个优先级创建一个静态的结构体。每个结构体存储着等同于CPU
激活核心数的串行队列,然后派发任务的时候轮询队列执行
这种做法让线程最大化的得到复用,以及控制了线程数量。另外使用结构体也是
YYKit
的统一做法了,尽可能减少了继承带来的内存损耗。这是仿写代码的结构体创建核心部分:
typedef struct __LXDDispatchContext {
const char * name;
void ** queues;
uint32_t queueCount;
int32_t offset;
} *DispatchContext, LXDDispatchContext;
LXD_INLINE DispatchContext __LXDDispatchContextCreate(const char * name,
uint32_t queueCount,
LXDQualityOfService qos) {
DispatchContext context = calloc(1, sizeof(LXDDispatchContext));
if (context == NULL) { return NULL; }
context->queues = calloc(queueCount, sizeof(void *));
if (context->queues == NULL) {
free(context);
return NULL;
}
for (int idx = 0; idx < queueCount; idx++) {
context->queues[idx] = (__bridge_retained void *)__LXDQualityOfServiceToDispatchQueue(qos, name);
}
context->queueCount = queueCount;
if (name) {
context->name = strdup(name);
}
context->offset = 0;
return context;
}
绘制事务
所有的异步绘制任务被封装成一个YYTransaction
,每次重新绘制文本时创建一个事务对象存储到一个全局任务集合中,为了保证同一个绘制任务在同一个runloop循环中不被多次调用,重写isEqual
和hash
方法
YYAsyncLayer
绘制任务的机制仿照CoreAnimation
的绘制机制,监听主线程RunLoop
,在空闲阶段插入绘制任务,并将任务优先级设置在CoreAnimation
绘制完成之后,然后遍历绘制任务集合进行绘制工作并且清空集合。
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
//获取完上一次需要执行的方法后,将所有方法清空
transactionSet = [NSMutableSet new];
//遍历set。执行里面的selector
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}
取消机制
YYAsyncLayer
中封装了YYSentinel
用来执行原子操作,以计数来判断绘制任务是否被取消。
@implementation YYSentinel
- (int32_t)increase {
return OSAtomicIncrement32(&_value);
}
@end
@implementation YYAsyncLayer
- (void)_displayAsync:(BOOL)async {
.....
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
//用计数判断是否已经取消
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
......
}
@end
此前思考过封装GCD
派发任务的时候,如何实现中断任务。查阅了很多资料得到的结论是正在执行的任务,无论是使用GCD
还是NSOperation
都不能被取消。因此使用过在派发block
中传入一个BOOL *
类型的指针,通过这个判断。但是实践后依旧存在其他的问题。参照YYAsyncLayer
的取消判断无疑是一种很好的实现思路
循环引用
YYFPSLabel
中存在定时器-展示器
循环引用的风险,为了避开这种风险封装了一个YYWeakProxy
类。简单来说就是生成一个临时对象弱引用回调方,以此破解强引用环。重写YYWeakProxy
类的消息转发方法,保证接收方是实际回调的对象
@implementation YYWeakProxy
-(instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+(instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
-(id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
-(BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
-(BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
-(NSUInteger)hash {
return [_target hash];
}
-(Class)superclass {
return [_target superclass];
}
-(Class)class {
return [_target class];
}
-(BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
-(BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
-(BOOL)isProxy {
return YES;
}
-(NSString *)description {
return [_target description];
}
-(NSString *)debugDescription {
return [_target debugDescription];
}
@end
编译器指令
__attribute__
用来声明单个编译器指令,帮助编译器检查错误以及做出更多的优化。YYKit
虽然编译器指令用的不多,比如YYModel
中使用强制内联声明:
#define force_inline __inline__ __attribute__((always_inline))
static force_inline YYEncodingNSType YYClassGetNSType(Class cls) {
if (!cls) return YYEncodingTypeNSUnknown;
if ([cls isSubclassOfClass:[NSMutableString class]]) return YYEncodingTypeNSMutableString;
if ([cls isSubclassOfClass:[NSString class]]) return YYEncodingTypeNSString;
if ([cls isSubclassOfClass:[NSDecimalNumber class]]) return YYEncodingTypeNSDecimalNumber;
if ([cls isSubclassOfClass:[NSNumber class]]) return YYEncodingTypeNSNumber;
if ([cls isSubclassOfClass:[NSValue class]]) return YYEncodingTypeNSValue;
if ([cls isSubclassOfClass:[NSMutableData class]]) return YYEncodingTypeNSMutableData;
if ([cls isSubclassOfClass:[NSData class]]) return YYEncodingTypeNSData;
if ([cls isSubclassOfClass:[NSDate class]]) return YYEncodingTypeNSDate;
if ([cls isSubclassOfClass:[NSURL class]]) return YYEncodingTypeNSURL;
if ([cls isSubclassOfClass:[NSMutableArray class]]) return YYEncodingTypeNSMutableArray;
if ([cls isSubclassOfClass:[NSArray class]]) return YYEncodingTypeNSArray;
if ([cls isSubclassOfClass:[NSMutableDictionary class]]) return YYEncodingTypeNSMutableDictionary;
if ([cls isSubclassOfClass:[NSDictionary class]]) return YYEncodingTypeNSDictionary;
if ([cls isSubclassOfClass:[NSMutableSet class]]) return YYEncodingTypeNSMutableSet;
if ([cls isSubclassOfClass:[NSSet class]]) return YYEncodingTypeNSSet;
return YYEncodingTypeNSUnknown;
}
其他地方使用了系统封装了__attribute__
的宏定义,比如YYCache
源码中禁用了init
和new
方法。笔者平时虽然也会重写这两个方法抛出异常,但是从来没在头文件中声明过:
@interface YYCache: NSObject
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
@end
宏定义本质上是unavailable
属性的封装:
#define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))
笔者常用的还有包括下面三个:
/// 声明函数可以重载
#define LXD_OVERLOAD __attribute__((overloadable))
/// 强制子类向上调用
#define LXD_FORCE_CALL_SUPER __attribute__((objc_requires_super))
/// 对类名进行混淆
#define LXD_MIX_CLASS(_mix_name_) __attribute__((objc_runtime_name(_mix_name_)))
集合容器
NSSet
是个有趣的容器结构,简单来说一个字典的键合集必然符合一个NSSet
的条件:
[[NSDictionary new] allKeys]; <====> [NSSet set];
字典的key
通过对象的hash
以及isEqual:
来判断是否重复。NSSet
也是如此,在YYTransaction
使用集合容器来帮助同一个绘制循环中不会执行相同的任务
@implementation YYTransaction
- (void)commit {
if (!_target || !_selector) return;
YYTransactionSetup();
[transactionSet addObject:self];
}
- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isMemberOfClass:self.class]) return NO;
YYTransaction *other = object;
return other.selector == _selector && other.target == _target;
}
@end
笔者使用的比较多的是另外一个集合NSHashTable
容器,相比起前者这个容器支持弱引用对象,在YYKeyboardManager
中使用了NSHashTable
存储回调者
- (instancetype)_init {
self = [super init];
_observers = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
......
return self;
}
- (void)addObserver:(id<YYKeyboardObserver>)observer {
if (!observer) return;
[_observers addObject:observer];
}
- (void)removeObserver:(id<YYKeyboardObserver>)observer {
if (!observer) return;
[_observers removeObject:observer];
}
RunLoop优先级
在大表哥的页面跳转性能优化中提到了一个技巧:嵌套式的dispatch_async
不断的将派发的block
提交到下一个RunLoop
中执行,因此合理将UI
代码分配成多个派发block
能提高性能。结合RunLoop
的执行代码:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
可以看到dispatch_async
的处理时间正好在Before Waiting
阶段之后,这个阶段是当前RunLoop
的空闲时间。事实上CoreAnimation
的渲染处理也差不多是在这个时间点执行的。
YYAsyncLayer
将异步绘制任务放到一个集合当中,注册RunLoop
监听者,在空闲时间进行绘制。有个小细节是:注册RunLoop
的监听者函数第四个参数传入回调优先级,YYAsyncLayer
传入了一个足够大的数字保证异步绘制任务放在CoreAnimation
渲染后执行:
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
//获取完上一次需要执行的方法后,将所有方法清空
transactionSet = [NSMutableSet new];
//遍历set。执行里面的selector
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}
static void YYTransactionSetup() {
static dispatch_once_t onceToken;
//gcd只运行一次
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;
//注册runloop监听,在等待与退出前进行
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
//将监听加在所有mode上
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}
数据库优化
YYCache
中使用到了大量的数据库优化技术,这些技术包括建立索引、设置数据库访问模式、修改磁盘同步等级等。
- (BOOL)_dbInitialize {
NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);";
return [self _dbExecute:sql];
}
-
数据库索引
数据库提供了一种列表式的存储方式,数据会按照一定的规则组织在表中,每一行代表了一个数据。如果单行数据存在多列时,如下图所示
其中rowid
是升序排序的,这也意味着除了rowid
之外的其他列基本排除了规则排序的可能性。如果现在搜索水果的价格:SELECT price FROM fruitsforsale WHERE fruit=‘Peach’
那么只能做一次全表查询搜索,最坏时间是O(N)
,这在数据库存在大量数据的情况下开销相当的可观。
create index if not exists last_access_time_idx on manifest(last_access_time);
索引是一种提高搜索效率的方案,一般索引会建立一个树结构来对索引列进行排序,使得查找时间为O(logN)
甚至更低。YYCache
对最后访问时间建立了索引,提高淘汰算法的执行效率
参考资料:sqlite索引的原理
-
设置数据库访问模式
从iOS4.3
开始,sqlite
提供了Write-Ahead Logging
模式,在大部分情况下这种模式读写速度更快,且两者互不堵塞。使用这种模式时,改写操作不改动数据库文件,而是修改到WAL
文件中。pragma journal_mode = wal;
WAL
的文件会在执行checkpoint
操作时写回数据库文件,或者当文件大小达到某个阙值时(默认为1KB
)会自动执行checkpoint
操作
- (void)_dbCheckpoint {
if (![self _dbCheck]) return;
// Cause a checkpoint to occur, merge `sqlite-wal` file to `sqlite` file.
sqlite3_wal_checkpoint(_db, NULL);
}
-
磁盘同步等级
sqlite
的磁盘写入速度分为三个等级:PRAGMA synchronous = FULL; (2) PRAGMA synchronous = NORMAL; (1) PRAGMA synchronous = OFF; (0)
当synchronous
为FULL
时,数据库引擎会在紧急时刻暂停以确定数据写入磁盘,这样能保证在系统崩溃或者计算机死机的环境下数据库在重启后不会被损坏,代价是插入数据的速度会降低。
如果`synchronous`为`OFF`则不会暂停。除非计算机死机或者意外关闭的情况,否则即便是`sqlite`程序崩溃了,数据也不会损伤,这种等级的写入速度最高。`YYCache`采用了第二种,速度不那么慢又相对安全的同步等级:
pragma synchronous = normal;
-
缓存
sql
命令结构
sqlite3_stmt
是操作数据库数据的辅助数据类型,每一个sql
语句可以解析成对应的辅助数据结构,大量的sql
语句解析同样会带来性能上的损耗,因此YYCache
采用CFMutableDictionaryRef
结构将解析后的辅助数据类型存储起来,每次执行sql
语句前查询是否存在已缓存的数据:- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); if (!stmt) { int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NULL; } CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); } else { sqlite3_reset(stmt); } return stmt; }
信号锁
从QoS
推出之后自旋锁在多线程竞争下已经不安全了,于是使用性能稍次的dispatch_semaphore_t
信号锁是更好的选择。信号锁在线程访问临界资源时会对标记位进行原子操作的自减,然后如果标记位低于0的时候让线程进入等待。通常情况下,对于信号的初始化分为0
和1
两种,用来表示两种不同的操作。
-
0
初始化为0
的信号量意味着一旦线程进入等待之后,无法在当前线程继续执行signal
操作,这将导致线程永久堵塞。因此解锁必然是跨线程操作的,一般用来实现任务的同步等待操作,比如卡顿监控的实现:while (lxd_is_monitoring) { __block BOOL timeOut = YES; dispatch_async(dispatch_get_main_queue(), ^{ timeOut = NO; dispatch_semaphore_signal(lxd_semaphore); }); [NSThread sleepForTimeInterval: lxd_time_out_interval]; if (timeOut) { [LXDBacktraceLogger lxd_logMain]; } dispatch_wait(lxd_semaphore, DISPATCH_TIME_FOREVER); }
-
1
初始化为1
或者更多的信号量是为了限制同一时刻访问临界资源的线程数,例如YYCache
对缓存访问的加锁:static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) { if (path.length == 0) return nil; _YYDiskCacheInitGlobal(); dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER); id cache = [_globalInstances objectForKey:path]; dispatch_semaphore_signal(_globalInstancesLock); return cache; }