在面试中经常会被问到关于Runloop的问题,比方:
runloop和线程有什么关系?
runloop的mode作用是什么?
猜想runloop内部是如何实现的?
等等诸如此类~~~
既然面试中问到这么多关于Runloop的问题,那Runloop在实际应用中究竟有什么用呢?
先来看一个在实际中遇到的问题
TableView的每一行Cell都有三张图片,在刚进入到这个页面的时候,根本滑不动。由于系统要绘制非常多的图片,假如此时的图片很大,那么就会出现动图中的情况,卡慢。
出现这个问题的起因很简单,就是同时绘制了过多的大型图片。那么这个问题大家平常怎样处理呢?这个问题也是大家平常说的 如何优化TableView卡慢 的问题。
异步加载数据?
异步绘制?
本篇详情的方法是使用Runloop来优化TableView。原理非常简单,就是监听Runloop的空闲状态,在Runloop即将休眠时(空闲时)再去绘制图片,这样就不会像动图中那么卡慢了。
首先在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 ()<UITableViewDelegate, UITableViewDataSource>@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 <UIKit/UIKit.h>@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的工具。
首先,在工具类的初始化方法中开启一个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 repeats
:YES
:持续监听 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 <Foundation/Foundation.h>@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需求是刚进入页面时客户的操作就要保持流畅,不能出现无法滑动的卡慢,所以我监听的 RunloopMode
是 kCFRunLoopDefaultMode
,这样在客户滑动的时候是不加载图片的,所以客户的滑动操作会很流畅。假如这里选择 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空闲时一件一件的做,就不会造成体验不佳的情况。
GCRunloopObserver