美文网首页RunLoopiOS 进阶
RunLoop优化 - UITableViewCell加载大图

RunLoop优化 - UITableViewCell加载大图

作者: kwdx | 来源:发表于2018-03-12 15:35 被阅读118次

    在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传送门

    相关文章

      网友评论

        本文标题:RunLoop优化 - UITableViewCell加载大图

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