前言
平时我们优化性能时,主线程处理一些耗时的任务基本上都是能挪到子线程就挪,不能挪的大任务,能拆出来挪到子线程的就拆,可是总有一些大而耗时的任务无法再拆分,为了避免用户操作出现卡顿感,我们就可以充分利用runloop的特点,来将任务放到runloop不繁忙时在执行(即用户停止操作的瞬间)
因此通过runloop可以合理的将大任务执行优先级降低,放到runloop即将休眠时执行,以保证用户UI操作的流畅度
注意: 如果用户操作过程没有卡顿感,则不需要盲目使用此优化方案,这样对于快速浏览型应用体验上会大打折扣,例如新闻类,而针对于一些页面,处理逻辑很大,不优化会有明显卡顿感,则可以使用此方案来解决,例如:大图、动图、视频需要查看的界面,则可以使用此方案来解决
实现代码使用了两种处理任务的方案,偏重方向不同,逻辑也相对简单,下面会逐步讲解
runloop简介
之前介绍到了runloop有多个mode,也就是有多个不同的运行模式,一次只能在一个模式下运行,且通过mode切换来达到切换状态的效果,其mode,如下所示
NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
其中最后一个NSRunLoopCommonModes实际上是不属于基本运行mode,他是所有mode的集合,即设置了NSRunLoopCommonModes参数的代码,可以在各个mode模式下正常执行
如果想尽可能减少用户操作时的事件,可以将任务放到NSDefaultRunLoopMode模式下运行,当用户停止操作时会切换到此模式运行
此外在温习一下runloop运行的流程图,可以清楚的看到observer的调用调用
然后查看一下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休眠时执行,且可以在后台执行(例如:部分数据磁盘写入和更新操作,用户行为统计上传操作等),那么方案二无疑是一个非常好的工具类了