美文网首页收藏ios
浅析IOS-TableView的优化

浅析IOS-TableView的优化

作者: Superman168 | 来源:发表于2018-02-28 19:10 被阅读0次

    最近这两天基本就是优化,今天想起项目中的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的优化

        本文链接:https://www.haomeiwen.com/subject/ktuixftx.html