RunLoopiOS 进阶

RunLoop优化 - UITableViewCell加载大图

2018-03-12  本文已影响118人  kwdx

在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在即将进入休眠的时候就会通知observerobserver就会调用创建时传入的回调函数。如果数组中没有待执行的代码块则直接返回;如果有就取出第一条代码块执行,并从数组中移除。这样可以保证每次RunLoop循环只渲染一张图片。

4. 保持RunLoop的鲜活性

做到这里就完事了吗?如果是的话你会发现有些图片没有渲染出来,为什么会这样呢?
因为我们创建的是observerobserver不会干扰RunLoop的正常执行,当RunLoop没什么事情做的时候就会进入休眠状态,observer的回调函数也不会被调用,代码块数组tasks里面的代码块也不会被执行,这就导致了有些图片没有被渲染出来。

2.jpeg

所以我们需要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;
    }
}

注意:timer会造成内存泄漏。

效果图.gif

最后附上最终效果图和demo地址:GitHub传送门

上一篇下一篇

猜你喜欢

热点阅读