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 ()<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的工具类

接下来介绍,怎么样构造一个基于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 <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需求是刚进入页面时用户的操作就要保持流畅,不能出现无法滑动的卡顿,所以我监听的 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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,963评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,348评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,083评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,706评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,442评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,802评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,795评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,983评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,542评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,287评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,486评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,030评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,710评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,116评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,412评论 1 294
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,224评论 3 398
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,462评论 2 378

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,127评论 1 32
  • 目录 Runloop RunLoop 与线程 个人理解总结 应用场景 1. 什么是RunLoop 基本作用 保持程...
    Ryan___阅读 1,517评论 0 13
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,310评论 0 11
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    阳明AGI阅读 1,110评论 0 17
  • 说明iOS中的RunLoop使用场景1.保持线程的存活,而不是线性的执行完任务就退出了<1>不开启RunLoop的...
    野生塔塔酱阅读 6,815评论 15 109