浅析IOS-TableView的优化

最近这两天基本就是优化,今天想起项目中的tableView感觉体验不是很好,一直有卡顿的现象,数据也不多,就找了找网上的优化方案,看了不少,感觉真正有用的不多,稍微做一下小结。

项目的列表是自定义的Cell,用的xib.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellID = @"cellID";
    IDBActivityCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
    if (!cell) {
        cell = [[[NSBundle mainBundle] loadNibNamed:@"IDBActivityCell" owner:self options:nil] lastObject];
    }
    if (self.dataList.count>0) {
        cell.activityModel = self.dataList[indexPath.row];
    }
    
    return cell;
}

感觉有没有问题,但是其实没有复用Cell,每次都是重建,内存开销大,导致卡顿,后来更改了复用的方法。

  1. 用UITableView 的 -registerClass:forCellReuseIdentifier: 或 -registerNib:forCellReuseIdentifier:其中之一的注册方法。
    2.[tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath].

之后比之前明显流畅多了,这只是目前粗浅的处理办法。

最开始在CocoaChina上看到的这篇文章

Runloop优化列表滑动卡顿

http://www.cocoachina.com/ios/20180228/22365.html
使用的Swift,目前项目用的还是OC。

利用runloop优化,解决卡顿,具体思路是:

1.创建一个任务数组。
2.添加Runloop的监听。

//MARK:处理卡顿
extension XXXFinancialFroductListVC {
    
    ///添加新的任务的方法!
    func addTask(_ indexP: IndexPath, unit: @escaping RunloopBlock) {
        self.tasksArr.append(unit)
        self.tasksIndexPathArr.append(indexP)
        //判断一下 保证没有来得及显示的cell不会绘制
        if self.tasksArr.count > self.maxQueueLength {
            _ = self.tasksArr.remove(at: 0)
            _ = self.tasksIndexPathArr.remove(at: 0)
        }
    }
    ///添加runloop的监听
    fileprivate func addRunloopObserver() {
        //获取当前RunLoop
        let runLoop: CFRunLoop = CFRunLoopGetCurrent()
        //定义一个上下文
        var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: unsafeBitCast(self, to: UnsafeMutableRawPointer.self), retain: nil, release: nil, copyDescription: nil)
        //定义一个观察者
        if   let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue, true, 0, self.observerCallbackFunc(), &context){
            //添加当前RunLoop的观察者
            CFRunLoopAddObserver(runLoop, observer, .commonModes);
        }
    }

3.在绘制cell的方法中,调用添加新任务的方法,删除就任务。
4.通过监听到回调.

<注:监听Runloop的commonModes的Mode切换>

空闲RunLoopMode

当用户正在滑动 UIScrollView(UITableView) 时,RunLoop 将切换到 UITrackingRunLoopMode 接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他 Mode (除 NSRunLoopCommonModes 这个组合 Mode)下的事件将全部暂停执行,来保证滑动事件的优先处理,这也是 iOS 滑动顺畅的重要原因。
当 UI 没在滑动时,默认的 Mode 是 NSDefaultRunLoopMode(同 CF 中的 kCFRunLoopDefaultMode),同时也是 CF 中定义的 “空闲状态 Mode”。当用户啥也不点,此时也没有什么网络 IO 时,就是在这个 Mode 下。

用RunLoopObserver找准时机

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

  1. RunLoop开始
  2. RunLoop即将处理Timer
  3. RunLoop即将处理Source
  4. RunLoop即将进入休眠状态
  5. RunLoop即将从休眠状态被事件唤醒
  6. 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);

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

PS: 预缓存功能因为下拉刷新的冲突和不明显的收益已经废弃

二.

UITableView的优化主要从三个方面入手:

  • 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:- 是调用最频繁的方法;
  • 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
  • 滑动时按需加载(UIScrollView方面),这个在大量图片展示,网络加载的时候很管用!(SDWebImage已经实现异步加载,配合这条性能杠杠的)。

除了上面最主要的三个方面外,还有很多几乎大伙都很熟知的优化点:

  1. 正确使用reuseIdentifier来重用Cells
  2. 尽量使所有的view opaque,包括Cell自身
  3. 尽量少用或不用透明图层
  4. 如果Cell内现实的内容来自web,使用异步加载,缓存请求结果
  5. 减少subviews的数量
  6. 在heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
  7. 尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示

只是感觉现在手动绘制cell,比较少见。

参考了很多大神优秀的文章,汇总,不好意思哈!!!

你可能感兴趣的:(浅析IOS-TableView的优化)