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

QoSiOS8之后苹果推出的一套机制,用来修饰多线程派发任务的性质和重要性,GCD会根据这些性质决定执行任务的时候获取的资源量。QoS提供了五种不同的任务性质枚举变量,分别是:

  • NSQualityOfServiceUserInteractive 表示用户交互任务,任务优先级高

  • NSQualityOfServiceUserInitiated 用户发起的需要立即得到回应的任务,优先级高

  • NSQualityOfServiceUtility 不需要立刻返回结果的任务,执行时间稍长。比如下载图片,数据请求

  • NSQualityOfServiceBackground 后台任务,对用户不可见,比如数据备份。任务的时间比较长

  • NSQualityOfServiceDefault 默认优先级任务,处于UserInitiatedUtility之间

通过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循环中不被多次调用,重写isEqualhash方法

图片转自Shelin

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源码中禁用了initnew方法。笔者平时虽然也会重写这两个方法抛出异常,但是从来没在头文件中声明过:

@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优先级

Delpan的页面跳转性能优化中提到了一个技巧:嵌套式的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)

    synchronousFULL时,数据库引擎会在紧急时刻暂停以确定数据写入磁盘,这样能保证在系统崩溃或者计算机死机的环境下数据库在重启后不会被损坏,代价是插入数据的速度会降低。如果synchronousOFF则不会暂停。除非计算机死机或者意外关闭的情况,否则即便是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的时候让线程进入等待。通常情况下,对于信号的初始化分为01两种,用来表示两种不同的操作。

  • 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; }