Runloop在实际中到底有什么用?

在面试中经常会被问到关于Runloop的问题,比如:

  • runloop和线程有什么关系?

  • runloop的mode作用是什么?

  • 猜想runloop内部是如何实现的?

等等诸如此类~~~

既然面试中问到这么多关于Runloop的问题,那Runloop在实际应用中到底有什么用呢?


先来看一个在实际中遇到的问题

TableView的每一行Cell都有三张图片,在刚进入到这个页面的时候,根本滑不动。因为系统要绘制非常多的图片,如果此时的图片很大,那么就会出现动图中的情况,卡顿

出现这个问题的原因很简单,就是同时绘制了过多的大型图片。那么这个问题大家平时怎么解决呢?这个问题也是大家平时说的 如何优化TableView卡顿 的问题。

  • 异步加载数据?

  • 异步绘制?

本篇介绍的方法是使用Runloop来优化TableView。原理非常简单,就是监听Runloop的空闲状态,在Runloop即将休眠时(空闲时)再去绘制图片,这样就不会像动图中那么卡顿了。


初始化最简单的TableView和Cell

首先在ViewController中构造好最简单的TableView。TableView行高定为 70,行数随数据源的数量而变。使用延迟执行模拟网络请求来获取数据源。cell使用自定义的 TestTableViewCell

//
//  ViewController.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright © 2018 崇. All rights reserved.
//

#import "ViewController.h"
#import "TestTableViewCell.h"

@interface ViewController ()

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self configTableView];
    [self requestData];
}

- (void)requestData {
    NSLog(@"请求数据中...");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        for (int i = 0; i < 100; i++) {
            NSMutableArray *arrM = [NSMutableArray array];
            for (int i = 0; i < 3; i++) {
                NSString *imgName = [NSString stringWithFormat:@"img%d.jpg", i+3];
                [arrM addObject:imgName];
            }
            [self.dataArray addObject:arrM];
        }
        [self.tableView reloadData];
    });
}

- (void)configTableView {
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    [self.tableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:@"TestTableViewCell"];
    [self.view addSubview:self.tableView];
    self.tableView.contentInset = UIEdgeInsetsMake(-20, 0, 0, 0);
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 70;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestTableViewCell" forIndexPath:indexPath];
    [cell setData:self.dataArray[indexPath.row]];
    return cell;
}

- (NSMutableArray *)dataArray {
    if (_dataArray == nil) {
        _dataArray = [NSMutableArray array];
    }
    return _dataArray;
}


@end

dataArray的数据结构是:

[
    [@"imgName1",@"imgName2",@"imgName3"],
    [@"imgName1",@"imgName2",@"imgName3"],
    [@"imgName1",@"imgName2",@"imgName3"]
]

接下来是cell的实现

//
//  TestTableViewCell.h
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright © 2018 崇. All rights reserved.
//

#import 

@interface TestTableViewCell : UITableViewCell

- (void)setData:(NSArray *)dataArray;

@end

//
//  TestTableViewCell.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright © 2018 崇. All rights reserved.
//

#import "TestTableViewCell.h"

@interface TestTableViewCell()

@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) NSMutableArray *imgViewArray;

@end

@implementation TestTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.imgViewArray = [NSMutableArray array];
        
        NSInteger count = 3;
        for (int i = 0; i < count; i++) {
            UIImageView *imgView = [[UIImageView alloc] init];
            [self.imgViewArray addObject:imgView];
            [self.contentView addSubview:imgView];
        }
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    CGFloat screenWidth = self.contentView.bounds.size.width;
    CGFloat width = (screenWidth - (self.imgViewArray.count+1)*10.0f) / self.imgViewArray.count;
    CGFloat height = self.contentView.bounds.size.height;
    for (int i = 0; i < self.imgViewArray.count; i++) {
        UIImageView *imgView = self.imgViewArray[i];
        imgView.frame = CGRectMake( (i+1)*10 + i*width, 0, width, height);
    }
}

- (void)setData:(NSArray *)dataArray {
    _dataArray = dataArray;
    for (int i = 0; i < 3; i++) {
        UIImageView *imgView = weakSelf.imgViewArray[i];
        UIImage *img = [UIImage imageNamed:dataArray[i]];
        imgView.image = img;
    }
}

@end

这样实现的就是动图中卡顿的TableView。

构造Runloop的工具类

接下来介绍,怎么样构造一个基于Runloop的工具。

首先,在工具类的初始化方法中开启一个timer,保证Runloop一直在循环。否则监听到Runloop进入休眠的状态时,我们的代码执行过一次后Runloop就进入休眠了。

- (instancetype)init
{
    self = [super init];
    if (self) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
    }
    return self;
}
- (void)timerFiredMethod {
    // 这个方法不用任何实现,只是保证Runloop一直在循环中。
}


功能核心

监听Runloop需要创建Runloop的观察者 CFRunLoopObserverRef,这个观察者可以根据需要监听Runloop的各种状态,包括七个枚举值:

  • kCFRunLoopEntry 即将进入RunLoop

  • kCFRunLoopBeforeTimers 即将处理Timer

  • kCFRunLoopBeforeSources 即将处理Source事件源

  • kCFRunLoopBeforeWaiting 即将进入休眠

  • kCFRunLoopAfterWaiting 刚从休眠中唤醒

  • kCFRunLoopExit 即将退出RunLoop

  • kCFRunLoopAllActivities 监听全部的活动类型

下面是创建观察者的源码

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    // 我们监听了 kCFRunLoopBeforeWaiting 即将休眠这一个状态,就是Runloop处于空闲的状态,
    // 当Runloop处于kCFRunLoopBeforeWaiting状态就会触发这个回调
    // 在这里可以做我们想做的任务了
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);

CFRunLoopObserverCreateWithHandler() 函数中的各项参数:

  • 第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault() 默认分配

  • 第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopBeforeWaiting 监听即将休眠的状态

  • 第三个参数 Boolean repeatsYES:持续监听 NO:不持续

  • 第四个参数 CFIndex order:优先级,一般填0即可

  • 第五个参数 :回调两个参数 observer:监听者 activity:监听的事件



CFRunLoopAddObserver() 函数中的参数:

  • 第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop

  • 第二个参数 CFRunLoopObserverRef observer 监听者

  • 第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态

创建了监听者并且给当前Runloop设置后,就可以正常的监听Runloop的各种状态了。为了我们优化TableView的目的,我们需要做的是在监听的回调中执行最耗性能的操作,即给cell中的三个 imageView 赋值大图。

把这个功能包装成一个单例工具类,所有耗性能的操作保存在一个数组(taskArray)中,注意:要把这个数组理解成 队列 去使用。然后监听Runloop的空闲状态,在Runloop空闲的时候去一件一件的做这些耗性能的操作。

上源码:

//
//  GCRunloopObserver.h
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright © 2018 崇. All rights reserved.
//

#import 

@interface GCRunloopObserver : NSObject

+ (instancetype)runloopObserver;

- (void)addTask:(void(^)(void))task;

@end

//
//  GCRunloopObserver.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright © 2018 崇. All rights reserved.
//

#import "GCRunloopObserver.h"

@interface GCRunloopObserver(){
    NSTimer *timer;
}

@property (nonatomic, strong) NSMutableArray *taskArray;

@end

@implementation GCRunloopObserver

+ (instancetype)runloopObserver {
    static dispatch_once_t once;
    static GCRunloopObserver *observer;
    dispatch_once(&once, ^{
        observer = [[GCRunloopObserver alloc] init];
    });
    return observer;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
        [self runloopBeforeWaiting];
    }
    return self;
}

- (void)addTask:(void(^)(void))task {
    if (task) {
        [self.taskArray addObject:task];
    }
}

- (void)runloopBeforeWaiting {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (self.taskArray.count == 0) {
            return;
        }
        // 取出耗性能的任务
        void(^task)(void) = self.taskArray.firstObject;
        // 执行任务
        task();
        // 第一个任务出队列
        [self.taskArray removeObjectAtIndex:0];
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
}

- (void)timerFiredMethod {
    
}

- (NSMutableArray *)taskArray {
    if (_taskArray == nil) {
        _taskArray = [NSMutableArray array];
    }
    return _taskArray;
}

@end


工具类的思路

任务数组中保存的是用户的耗性能操作,用Block传递过来。工具类本身是一个单例,所以任务数组是唯一的,所有操作都在保存在这个像 “队列” 一样的数组(taskArray)中,按照先进先出的原则,在Runloop空闲的时候逐个完成。这样这些耗性能的操作不会在Runloop需要完成其它操作的时候来抢占CPU资源,卡顿的情况就会明显得到缓解。

另外,监听Runloop选择的模式(RunloopMode) 也有很大关系。比如我们的APP需求是刚进入页面时用户的操作就要保持流畅,不能出现无法滑动的卡顿,所以我监听的 RunloopModekCFRunLoopDefaultMode,这样在用户滑动的时候是不加载图片的,所以用户的滑动操作会很流畅。如果这里选择 kCFRunLoopCommonModes ,那么在滑动期间仍然会加载图片,还是会有一些卡顿的情况。

使用工具类

说完道理,我们来看看怎么使用吧!创建完这个工具类,只要一步就可以实现优化。把cell给三个 ImageView 赋值的操作提出去,放到Runloop空闲时再做,因为卡顿就是因为它,所以接下来需要对cell的 - (void)setData:(NSArray *)dataArray 进行改造。先找到耗性能的操作是哪些。

这三行是耗性能的元凶:

UIImageView *imgView = self.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;

谁耗性能,就把谁放到Block中:

__weak typeof(self) weakSelf = self;
[[GCRunloopObserver runloopObserver] addTask:^{
    UIImageView *imgView = weakSelf.imgViewArray[i];
    UIImage *img = [UIImage imageNamed:dataArray[i]];
    imgView.image = img;
}];

所以cell的 - (void)setData:(NSArray *)dataArray 方法改造完是这样的:

- (void)setData:(NSArray *)dataArray {
    _dataArray = dataArray;
    for (int i = 0; i < 3; i++) {
        __weak typeof(self) weakSelf = self;
        [[GCRunloopObserver runloopObserver] addTask:^{
            UIImageView *imgView = weakSelf.imgViewArray[i];
            UIImage *img = [UIImage imageNamed:dataArray[i]];
            imgView.image = img;
        }];
    }
}



运行情况



总结

可以看到卡顿情况得到明显缓解,一进入页面的时候滑动不会卡顿,图片加载中时滑动也不会卡顿,只有图片的加载过程是缓慢的。但是如果同时兼顾滑动和加载图片那就一定会卡顿,所以看你的需求具体是什么样的了。

最后要说,这种方式不仅可以用在优化TableView中,还可以应用到你所有出现卡顿的情况当中去。把耗性能的操作放到Runloop队列中去,等Runloop空闲时一件一件的做,就不会造成体验不佳的情况。

GitHub源码

GCRunloopObserver

你可能感兴趣的:(Runloop在实际中到底有什么用?)