最近这两天基本就是优化,今天想起项目中的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,每次都是重建,内存开销大,导致卡顿,后来更改了复用的方法。
- 用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 的运行状态,并在状态机切换时收到通知:
- 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);
分解成多个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已经实现异步加载,配合这条性能杠杠的)。
除了上面最主要的三个方面外,还有很多几乎大伙都很熟知的优化点:
- 正确使用reuseIdentifier来重用Cells
- 尽量使所有的view opaque,包括Cell自身
- 尽量少用或不用透明图层
- 如果Cell内现实的内容来自web,使用异步加载,缓存请求结果
- 减少subviews的数量
- 在heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
- 尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示
只是感觉现在手动绘制cell,比较少见。
参考了很多大神优秀的文章,汇总,不好意思哈!!!
网友评论