在面试中经常会被问到关于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
即将进入RunLoopkCFRunLoopBeforeTimers
即将处理TimerkCFRunLoopBeforeSources
即将处理Source事件源kCFRunLoopBeforeWaiting
即将进入休眠kCFRunLoopAfterWaiting
刚从休眠中唤醒kCFRunLoopExit
即将退出RunLoopkCFRunLoopAllActivities
监听全部的活动类型
下面是创建观察者的源码
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
@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空闲时一件一件的做,就不会造成体验不佳的情况。
GitHub源码
GCRunloopObserver