在iOS开发中,用的最多的控件就是UITableView,而UITableView的优化是一个老生常谈的问题了。iOS系统一直深受用户的喜爱是因为流畅性,如果界面出现了卡顿现象,那么就有可能让用户放弃这个APP了。
有个需求,需要从本地加载高清大图到UITableViewCell上,而且每个cell上面需要加载3张图片,当cell数据量足够多,图片很大的时候,我们需要保持流畅度和加载速度。
场景
一开始,我们先按照正常的方法对cell中的3个imageview设置图片:
NSInteger row = indexPath.row;
cell.imageView1.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@(row%3).stringValue ofType:@"jpg"]];
cell.imageView2.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@((row+1)%3).stringValue ofType:@"jpg"]];
cell.imageView3.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@((row+2)%3).stringValue ofType:@"jpg"]];
看起来好像没什么问题,从资源包中加载3张图片,应该没什么问题。但是当我们滑动界面的时候,很明显出现了界面卡顿的现象。
1.gif
- 分析:
当滑动界面的时候,主线程的RunLoopMode会切换到
NSEventTrackingRunLoopMode
,RunLoop在处理滑动事件,这时候我们还要RunLoop去处理大图片的加载,IO操作是很耗时的操作,所以就造成了卡顿现象。
- 解决思路:
界面卡顿是因为RunLoop在一次循环中渲染的图片太多了,如果RunLoop每次循环只渲染一张图片呢?
1. 创建CFRunLoopObserverCreate
// 拿到当前的runloop
CFRunLoopRef runloop = CFRunLoopGetMain();
// 定义一个上下文
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// 创建一个观察者
_defaulModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &Callback, &context);
// 添加观察者到RunLoop中
CFRunLoopAddObserver(runloop, _defaulModeObserver, kCFRunLoopCommonModes);
CFRelease(runloop);
首先需要创建上下文环境,因为是CoreFoundation的框架,所以需要用__bridge
桥接一下(__bridge
不会改变引用计数器)。
CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
创建观察者对象,allocator
内存分配对象句柄;activities
想要监听的RunLoop状态;repeats
是否重复监听;order
观察者索引,当RunLoop中存在多个observer的时候,按照索引排序执行;callout
回调函数;context
调用回调函数时传递的上下文环境。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
2. 使用可变数组存储任务
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ImageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"imagecell" forIndexPath:indexPath];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// IO操作为耗时操作,放到异步线程中执行
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@((row+2)%3).stringValue ofType:@"jpg"]];
dispatch_async(dispatch_get_main_queue(), ^{
[self addTask:^{
cell.imageView.image = image;
}];
});
});
return cell;
}
- (void)addTask:(RunloopBlock)task {
// 将代码块添加到可变数组中
[self.tasks addObject:task];
// 判断当前待执行的代码块是否超出最大代码块数
if (self.tasks.count > self.maxQueueLength) {
// 干掉最开始的代码块
[self.tasks removeObjectAtIndex:0];
}
}
在这里,我设置的最大代码块数为51,可根据具体情况更改,没什么强制性要求。IO操作也是耗时操作,也是放到全局队列去异步执行,刷新UI的时候回到主线程中,将渲染操作放到可变数组中,等到合适的时机再渲染。
3. 处理RunLoopObserver的回调函数
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
TableViewController *vc = (__bridge TableViewController *)info;
if (vc.tasks.count == 0) {
return;
}
RunloopBlock task = vc.tasks.firstObject;
task();
[vc.tasks removeObjectAtIndex:0];
}
info就是我们在创建observer的时候传入的上下文环境,每次RunLoop在即将进入休眠的时候就会通知observer,observer就会调用创建时传入的回调函数。如果数组中没有待执行的代码块则直接返回;如果有就取出第一条代码块执行,并从数组中移除。这样可以保证每次RunLoop循环只渲染一张图片。
4. 保持RunLoop的鲜活性
做到这里就完事了吗?如果是的话你会发现有些图片没有渲染出来,为什么会这样呢?
因为我们创建的是observer,observer不会干扰RunLoop的正常执行,当RunLoop没什么事情做的时候就会进入休眠状态,observer的回调函数也不会被调用,代码块数组tasks
里面的代码块也不会被执行,这就导致了有些图片没有被渲染出来。
所以我们需要RunLoop一直转,不让其休眠。添加一个定时器,让RunLoop定时的转起来就行了。
- (void)timerMethod {}
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0001 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
}
定时器可以什么都不用做,只要让RunLoop醒着就行。
😱 EXC_BAD_ACCESS
EXC_BAD_ACCESS.png为什么出现了野指针异常?因为我们把observer添加到了主运行循环之后没有将其移除,当当前对象被释放之后,我们尝试访问一块已经被释放的内存地址。
解决方法:在合适的地方将observer从RunLoop中移除掉。
- (void)viewWillAppear:(BOOL)animated {
// 添加观察者到runloop中
if (_defaulModeObserver != NULL) {
CFRunLoopAddObserver(CFRunLoopGetMain(), _defaulModeObserver, kCFRunLoopCommonModes);
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_defaulModeObserver != NULL) {
// 移除观察者
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _defaulModeObserver, kCFRunLoopCommonModes);
}
}
- (void)dealloc {
if (_defaulModeObserver != NULL) {
// 释放观察者
CFRelease(_defaulModeObserver);
_defaulModeObserver = NULL;
}
}
效果图.gif注意:timer会造成内存泄漏。
最后附上最终效果图和demo地址:GitHub传送门
网友评论