Runloop在实际中究竟有什么用?
来源:ChinaChong     阅读:683
蜂鸟
发布于 2018-12-12 23:43
查看主页

在面试中经常会被问到关于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的各种状态,包括七个枚举值:

下面是创立观察者的源码

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

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



CFRunLoopAddObserver() 函数中的参数:

创立了监听者并且给当前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

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
小程序开发需要关注的几个知识点
如何与Keras和Apache Spark并行训练您的神经网络
Dart | 彻底了解Dart中的库与访问可见性
掌握这些面试题,java面试再也不会频繁被坑了
解析get方式发送的请求
首页
搜索
订单
购物车
我的