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

利用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,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。

你可能感兴趣的:(利用RunLoop空闲时间执行预缓存任务)