您的位置:首页 > 运维架构

利用RunLoop空闲时间执行预缓存任务

2016-06-01 21:49 369 查看


利用RunLoop空闲时间执行预缓存任务

最近在做高度自适应的UITableView的时候,使用了一个FDTemplateLayoutCell的开源组件。

它的主要原理是利用RunLoop空闲时间执行预缓存任务

FDTemplateLayoutCell 的高度预缓存是一个优化功能,它要求页面处于空闲状态时才执行计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。一般来说,这个功能要耦合 UITableView 的滑动状态才行,但这种实现十分不优雅且可能破坏外部的 delegate 结构,但好在我们还有RunLoop

这个工具,了解它的运行机制后,可以用很简单的代码实现上面的功能。


空闲RunLoopMode

当用户正在滑动 UIScrollView 时,RunLoop 将切换到 UITrackingRunLoopMode

接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他 Mode (除 NSRunLoopCommonModes 这个组合 Mode)下的事件将全部暂停执行,来保证滑动事件的优先处理,这也是 iOS 滑动顺畅的重要原因。当 UI 没在滑动时,默认的 Mode 是 NSDefaultRunLoopMode

(同 CF 中的 kCFRunLoopDefaultMode),同时也是 CF 中定义的 “空闲状态 Mode”。当用户啥也不点,此时也没有什么网络 IO 时,就是在这个 Mode 下。


用RunLoopObserver找准时机

注册 RunLoopObserver 可以观测当前 RunLoop 的运行状态,并在状态机切换时收到通知:

RunLoop开始

RunLoop即将处理Timer

RunLoop即将处理Source

RunLoop即将进入休眠状态

RunLoop即将从休眠状态被事件唤醒

RunLoop退出

因为“预缓存高度”的任务需要在最无感知的时刻进行,所以应该同时满足:
RunLoop 处于“空闲”状态 Mode

当这一次 RunLoop 迭代处理完成了所有事件,马上要休眠时

使用 CF 的带 block 版本的注册函数可以让代码更简洁:
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { // TODO here});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);


在其中的 TODO 位置,就可以开始任务的收集和分发了,当然,不能忘记适时的移除这个 observer。


分解成多个RunLoop Source任务

假设列表有 20 个 cell,加载后展示了前 5 个,那么开启估算后 table view 只计算了这 5 个的高度,此时剩下 15 个就是“预缓存”的任务,而我们并不希望这 15 个计算任务在同一个 RunLoop 迭代中同步执行,这样会卡顿 UI,所以应该把它们分别分解到 15 个 RunLoop 迭代中执行,这时就需要手动向 RunLoop 中添加 Source 任务(由应用发起和处理的是 Source 0 任务)Foundation 层没对 RunLoopSource
提供直接构建的 API,但是提供了一个间接的、既熟悉又陌生的 API:
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;


这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件,简单来说就是“睡你xx,起来嗨!”于是,我们用一个可变数组装载当前所有需要“预缓存”的 index path,每个 RunLoopObserver 回调时都把第一个任务拿出来分发:
NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
if (mutableIndexPathsToBePrecached.count == 0)
{ CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
CFRelease(observer); // 注意释放,否则会造成内存泄露 return;
}
NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
[mutableIndexPathsToBePrecached removeObject:indexPath];
[self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
onThread:[NSThread mainThread]
withObject:indexPath waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
});


这样,每个任务都被分配到下个“空闲” RunLoop 迭代中执行,其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: