使用runloop加载任务

1,295 阅读9分钟

前言

平时我们优化性能时,主线程处理一些耗时的任务基本上都是能挪到子线程就挪,不能挪的大任务,能拆出来挪到子线程的就拆,可是总有一些大而耗时的任务无法再拆分,为了避免用户操作出现卡顿感,我们就可以充分利用runloop的特点,来将任务放到runloop不繁忙时在执行(即用户停止操作的瞬间)

因此通过runloop可以合理的将大任务执行优先级降低,放到runloop即将休眠时执行,以保证用户UI操作的流畅度

注意: 如果用户操作过程没有卡顿感,则不需要盲目使用此优化方案,这样对于快速浏览型应用体验上会大打折扣,例如新闻类,而针对于一些页面,处理逻辑很大,不优化会有明显卡顿感,则可以使用此方案来解决,例如:大图、动图、视频需要查看的界面,则可以使用此方案来解决

实现代码使用了两种处理任务的方案,偏重方向不同,逻辑也相对简单,下面会逐步讲解

源码地址

runloop简介

之前介绍到了runloop有多个mode,也就是有多个不同的运行模式,一次只能在一个模式下运行,且通过mode切换来达到切换状态的效果,其mode,如下所示

NSDefaultRunLoopMode

NSConnectionReplyMode

NSModalPanelRunLoopMode

NSEventTrackingRunLoopMode

NSRunLoopCommonModes

其中最后一个NSRunLoopCommonModes实际上是不属于基本运行mode,他是所有mode的集合,即设置了NSRunLoopCommonModes参数的代码,可以在各个mode模式下正常执行

如果想尽可能减少用户操作时的事件,可以将任务放到NSDefaultRunLoopMode模式下运行,当用户停止操作时会切换到此模式运行

此外在温习一下runloop运行的流程图,可以清楚的看到observer的调用调用

2868a192697b4816b8fa0ac7d91bec4d_tplv-k3u1fbpfcp-watermark.png

然后查看一下runloop代码枚举给出的可以监听的observer类型

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),            //即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变
};

看了上面的runloop简介,我们实际用到的mode就是NSDefaultRunLoopMode,当用户滑动时此模式会自动切出挂起,当用户停止操作界面(滚动视图)时,会恢复到defaultMode中执行,当恢复到defaultModel如果没有任务执行,runloop将会进入休眠状态,此时在defaultMode中设置监听的Observer将会得到执行

实现逻辑介绍

实现逻辑分为两种:

一、使用哈希表根据key值去重,当runloop即将进入休眠时,一次性完毕执行所有任务(加载大图推荐)

二、使用哈希表根据key值去重,当runloop即将进入休眠时,子线程唤醒任务一个一个到队列执行,当用户切换操作时终止任务执行,剩下的任务下一轮执行(实现过程中,发现效果一般,由于处理的大任务只能在主队列运行时,主线程操作仍然会陷入卡顿,此时必须要对任务进行时间片管理,即执行一个任务,休眠一段时间,实际执行效率就一般了),因此方案二的设计更适合加入任务,等待休眠时串行执行任务的操作,可用于计算或者统计,数据库信息查询等场景,后面介绍

方案一源码介绍(大图加载推荐此方案)

此方案是源码中的 LSRunloopTaskManager 文件

声明了一个mapTable和Oberver以便于内容的保存和观察者的建立,同时NSMapTable可以设置弱引用时释放对象,避免了对内容的维护

{
    NSMapTable *_mapTable;
    CFRunLoopObserverRef _observer;
}
- (instancetype)init
{
    self = [super init];
    if (self) {
        _mapTable = [NSMapTable mapTableWithKeyOptions:
            NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality 
            valueOptions:NSPointerFunctionsStrongMemory];
    }
    return self;
}

里面设置了全局单例和弱引用单例两种使用情况,一个适用于全局,一个适用于某种场景,即使用完毕后,界面退出释放

+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

+ (instancetype)weakSingleInstance {
    static __weak LSRunloopTaskManager *weakInstance = nil;
    __strong id strongInstance = weakInstance;
    @synchronized (self) {
        if (!weakInstance) {
            strongInstance = [[self alloc] init];
            weakInstance = strongInstance;
        }
    }
    return strongInstance;
}

加入事件,注册移除Observer

//当开始监听是创建register,避免实际没有任务时,runloop总被唤醒
//虽然系统优化runloop唤醒逻辑,还是避免过多的性能消耗
- (void)setTaskBlock:(void (^)(void))taskBlock forKey:(id)key {
    NSAssert([NSThread isMainThread], @"任务必须在主线程中设置");
    //加入任务到mapTable中去
    [_mapTable setObject:[taskBlock copy] forKey:key];
    if (!_observer) [self registerObserver];
}
//当runloop即将休眠时执行的方法
void __runloopTaskCallback(CFRunLoopObserverRef observer, 
    CFRunLoopActivity activity, void *info) {
    //遍历mapTable执行保存的所有block
    NSMapTable *mapTable = [LSRunloopTaskManager sharedInstance]->_mapTable;
    for (id key in mapTable) {
        void (^block)(void) = [mapTable objectForKey:key];
        block();
    }
    //执行完毕任务后移除任务
    [mapTable removeAllObjects];
    //执行完毕所有任务之后,关闭observer,避免runloop总是被唤醒,迟迟无法进入休眠状态
    [[LSRunloopTaskManager sharedInstance] removeObserver];
}

//每次即将进入休眠调用回调方法
- (void)registerObserver {
    //注册Observer,设置观察类型和回调方法
    //观察类型为kCFRunLoopBeforeWaiting,即runloop即将进入休眠时环形
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting,
        YES, 0, &__runloopTaskCallback, &context);
     并且将Observer放到主队列的DefaultMode中去观察
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
}
//当任务处理完毕时,为了保证runloop能立即进入休眠状态,需要立即移除runloop,避免后台一直浪费电量
//据操作,实际runloop优化过,即使不及时移除,休眠后继续唤醒,执行一定次数或者一段时间用户无操作也会进行休眠
//但不能确定条件是什么,因此手动移除是最安全的,毕竟省电也很重要
- (void)removeObserver {
    if (_observer == NULL) return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
    CFRelease(_observer);
    _observer = NULL;
}

方案一总结

此方案算是最简单粗暴runloop解决大任务加载的工具类了,且效果显著

适用于runloop停止时,总任务执行时间不是很长的场景,或者任务数量比较少的场景

此方案执行UI任务时,在滑动时任务不会执行,因此有些体验上会有所欠缺,可以在其他场景优化处理,例如:列表中出现视频、gif时,滑动是默认显示一张首图(小图),停止运行时,进行播放、放置高清图等

因此,使用此工具类时,尽量把影响卡顿的重要操作放到里面即可,没必要把所有的都放进去,否则体验效果会略差

此方案也是目前比较推荐的一个小工具了

方案二源码介绍(仅供参考)

方案二介绍了另外一种任务的处理手段,同样是使用runloop加载图片,执行任务的时候确实一个一个执行,以便于用户及时打断,但用户越方便,效率上回差的越多,仅仅是多一种思路,可以自己在尝试做一个类似于cpu执行片段效果,这个只是延伸,实际并不合适,仅仅是玩一玩新策略,仅供参考

此方案是源码中的 LSRunloopTaskAsyncManager 文件

注册和移除Observer,与方案一不同的是,注册的时候,额外加入了kCFRunLoopBeforeSources,是便于用户进行操作时及时停止后续任务的执行

//当切换到kCFRunLoopBeforeWaiting状态时,即将休眠,此时开始执行任务,否则将任务执行状态设置为false
//即停止执行后续任务
void __runloopTaskExCallback(CFRunLoopObserverRef observer, 
    CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        [[LSRunloopTaskAsyncManager sharedInstance] executeTasks];
    }else {
        [LSRunloopTaskAsyncManager sharedInstance]->_canExecute = false;
    }
}
//每次即将进入休眠调用回调方法
- (void)registerObserver {
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, 
        kCFRunLoopBeforeWaiting | kCFRunLoopBeforeSources, 
        YES, 0, &__runloopTaskExCallback, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
}

//移除observer
- (void)removeObserver {
    if (_observer == NULL) return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
    CFRelease(_observer);
    _observer = NULL;
}

设置任务

//开始加入任务到taskMap中去,并且没有添加监听时添加监听,可以设置任务执行完毕后是否删除
//其中taskMap是一个双向列表的队列,任务为先进先执行,后进后执行
- (void)setAsyncTaskBlock:(void (^)(void))taskBlock forKey:(id)key {
    [_taskMap setTaskBlock:taskBlock forKey:key executeLeave:YES];
    if (!_observer) [self registerObserver];
}
- (void)setAsyncTaskBlock:(void (^)(void))taskBlock 
        forKey:(id)key executeLeave:(BOOL)executeLeave {
    [_taskMap setTaskBlock:taskBlock forKey:key executeLeave:executeLeave];
    if (!_observer) [self registerObserver];
}

子队列任务执行方法,子队列中一个个执行任务,这个是此方案比较实用的场景了

//执行任务的方法,任务会在子线程被一个一个执行
//当用户开始操作时_canExecute会被设置为false此时结束任务的继续执行
- (void)executeTasks {
    dispatch_async(_queue, ^{
        self->_canExecute = true;
        while (self->_canExecute) {
            if ([self->_taskMap executeBlock]) {
                self->_canExecute = false;
                [self removeObserver];
            }
        }
    });
}

UI操作任务执行方法, 子队列整理任务,一个一个放到主线程中执行,执行时间有休眠片段,由于主队列为串行队列,因此需要流出时间片给用户操作,否则对性能和操作只会比方案一差(此方案如果有更好解决方案,欢迎大家讨论)

- (void)executeTasks {
    if (_canExecute) return;
    
    dispatch_async(_queue, ^{
        self->_canExecute = true;
        CFTimeInterval lastInterval = CACurrentMediaTime();
        while (self->_canExecute) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([self->_taskMap executeBlock]) {
                    self->_canExecute = false;
                    [self removeObserver];
                }
                dispatch_semaphore_signal(self->_semaphore);
            });
            dispatch_semaphore_wait(self->_semaphore, DISPATCH_TIME_FOREVER);
            //轮询时间片,由于会任务执行过程会卡主线程, 实际体验还是一般,不过还算可以了
            CFTimeInterval interval = CACurrentMediaTime();
            if (interval - lastInterval > 0.05) {
                [NSThread sleepForTimeInterval:0.05];
                lastInterval = interval;
            }
        }
    });
}

方案二总结

此方案是强化版本的runloop加载工具类,最适合用于可以移动到子队列运行的场景,且优先级不高的场景,例如:本地数据预加载、清理缓存、更新本地数据等操作,此任务可以长期存在,只要runloop进入休眠即可执行

另外,此方案经过改编,加入时间片操作,也可以实现和方案一一样的在主队列进行任务的加载,缺陷与优点一样明显,优点就是任务多时可以及时中断任务,以避免用户操作卡顿,缺点就是执行效率相对较低,有时仍然会卡顿

所以方案二加载主队列任务的功能不完善,还有待开发,欢迎讨论

LSTaskMap

LSTaskMap是方案二衍生出来的数据结构,其模仿YYCache的部分数据结构,编写的一个任务队列,由双向列表和哈希表构成,以保证任务能够快速访问、增删除、更新任务,这里只贴上代码,就不多介绍了

//基本任务节点,目前只支持block,可以避免对象selector和参数的额外引用
@interface LSTaskNode : NSObject
{
@package
    id _key;
    id (^_block)(void);
    BOOL _executeLeave; //是否实行完毕出队
    //避免造成释放问题,在LSTaskMap释放期间内,其不会被释放,因此可以放心使用
    __unsafe_unretained LSTaskNode *_preNode; 
    __unsafe_unretained LSTaskNode *_nextNode;
}

@end

@implementation LSTaskNode


@end

//任务维护的map,由双向链表和哈希表共同组成
@interface LSTaskMap : NSObject
{
    NSMapTable<id, LSTaskNode *> *_mapTable; //主要用来快速查找、去重、避免键值维护
    
    LSTaskNode *_headNode; //头结点 -- 尾进头出
    LSTaskNode *_tailNode; //尾结点
}
//执行任务先进先执行原则
//设置任务Block,更新到队尾,剔除更新重复key的任务
- (void)setTaskBlock:(void (^)(void))taskBlock forKey:(id)key executeLeave:(BOOL)executeLeave;
//队首任务离开
- (void)leaveTask;
//执行一个任务,根据任务类型选择是否离队,结果返回队列是否为空
- (BOOL)executeBlock;
//获取元素
- (LSTaskNode *)taskForKey:(id)key;
//移除某个元素
- (void)removeTaskForKey:(id)key;
//移除所有元素
- (void)removeAllTask;

@end

@implementation LSTaskMap

- (instancetype)init
{
    self = [super init];
    if (self) {
        _mapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory 
            | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsStrongMemory];

        _headNode = nil;
        _tailNode = nil;
    }
    return self;
}

//设置任务
- (void)setTaskBlock:(void (^)(void))taskBlock forKey:(id)key executeLeave:(BOOL)executeLeave {
    LSTaskNode *node = [_mapTable objectForKey:key];
    if (node) {
        //移动到尾,先到的后执行
        node->_block = [taskBlock copy];
        node->_executeLeave = executeLeave;
        if (node == _tailNode) return;
        if (node == _headNode) {
            _headNode = node->_nextNode;
            _tailNode->_nextNode = node;
            node->_preNode = _tailNode;
            _tailNode = node;
            node->_nextNode = nil;
        }else {
            node->_preNode->_nextNode = node->_nextNode;
            node->_nextNode->_preNode = node->_preNode;
            node->_preNode = _tailNode;
            _tailNode->_nextNode = node;
            _tailNode = node;
            node->_nextNode = nil;
        }
    }else {
        node = [LSTaskNode new];
        node->_key = key;
        node->_block = [taskBlock copy];
        node->_executeLeave = executeLeave;
        node->_preNode = nil;
        node->_nextNode = nil;
        
        //尾进头出, 头结点存在,尾结点就存在
        if (!_headNode) {
            _headNode = node;
            _tailNode = node;
        }else {
            LSTaskNode *lastNode = _tailNode;
            node->_preNode = lastNode;
            lastNode->_nextNode = node;
            _tailNode = node;
        }
        [_mapTable setObject:node forKey:key];
    }
}

//队首元素离开
- (void)leaveTask {
    if (!_headNode) return;
    
    if (_headNode == _tailNode) {
        //只有一个元素
        [_mapTable removeAllObjects];
        _headNode = nil;
        _tailNode = nil;
    }else {
        [_mapTable removeObjectForKey:_headNode->_key];
        _headNode = _headNode->_nextNode;
        _headNode->_preNode = nil;
    }
}

//执行一个任务,根据任务类型选择是否离队,结果返回队列是否为空
- (BOOL)executeBlock {
    if (!_headNode) return true;
    _headNode->_block();

    if (_headNode->_executeLeave) {
        if (_headNode == _tailNode) {
            //只有一个元素
            [_mapTable removeAllObjects];
            _headNode = nil;
            _tailNode = nil;
            
            return true;
        }else {
            [_mapTable removeObjectForKey:_headNode->_key];
            _headNode = _headNode->_nextNode;
            _headNode->_preNode = nil;
            
            return false;
        }
    }else {
        return _headNode == _tailNode;
    }
}

- (LSTaskNode *)taskForKey:(id)key {
    return [_mapTable objectForKey:key];
}

- (void)removeTaskForKey:(id)key {
    LSTaskNode *node = [_mapTable objectForKey:key];
    if (!node) return;
    
    if (_headNode == _tailNode) {
        [_mapTable removeAllObjects];
        _headNode = nil;
        _tailNode = nil;
        return;
    }
    if (node == _headNode) {
        _headNode = node->_nextNode;
        _headNode->_preNode = nil;
    }else if (node == _tailNode) {
        _tailNode = node->_preNode;
        _tailNode->_nextNode = nil;
    }else {
        node->_nextNode->_preNode = node->_preNode;
        node->_preNode->_nextNode = node->_nextNode;
    }
    [_mapTable removeObjectForKey:key];
}

- (void)removeAllTask {
    [_mapTable removeAllObjects];
    _headNode = nil;
    _tailNode = nil;
}

最后

runloop加载任务,只是优化的一个方向之一,还有待继续开发

如果平时开发中使用的 大图、gif、视频之类的UI操作,配合key的使用(key可以是任何对象类型),任务数量一般不会不多,优先选用方案一实现,操作简单无污染

如果一些任务长期存在,需要在runloop休眠时执行,且可以在后台执行(例如:部分数据磁盘写入和更新操作,用户行为统计上传操作等),那么方案二无疑是一个非常好的工具类了