浅析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,比较少见。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容