iOS RunLoop

​ 前言:一般代码运行完就结束了,为何APP就一直能保持运行状态呢?这个秘诀就是RunLoop,本文先介绍了RunLoop的概念,引出它的作用,并在CoreFoundation框架源码查看它的底层结构;然后介绍RunLoop和线程的关系,并介绍它运行的各种模式;最后介绍RunLoop在实际项目中的应用,比如解决NSTimer在滑动时停止工作的问题,线程保活和性能优化等问题。

一、RunLoop概念

1、RunLoop概念:

​ 顾名思义,运行循环,在程序运行过程中循环做一些事情。应用范畴有:定时器(Timer)、PerformSelector、GCD、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool等。

2、RunLoop作用:

​ 如果没有RunLoop,运行代码

int main(int argc, char * argv[]) {
    @autoreleasepool {
//        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"Hello world!");
    }
    return 0;
}

程序运行完就结束了,App也就退出了,所以RunLoop作用有以下几条:

​ 1)保持程序的持续运行;

​ 2)处理App中的各种事件(比如触摸事件、定时器事件等);

​ 3)节省CPU资源,该做事时做事,该休息时休息等;

3、RunLoop构成:

​ 下载CoreFoundation框架源码, 在CFRunLoop.c文件中找到RunLoop的底层结构:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list 线程锁 */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData;  // reset for runs of the run loop
    pthread_t _pthread;  //RunLoop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;  //记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;  //存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;  //当前运行的mode
    CFMutableSetRef _modes;  //存储的是CFRunLoopModeRef
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

可见,RunLoop底层是一个结构体,主要包含了一个线程,当前运行的Mode,若干个commonMode,若干个commonModeItem等。

二、RunLoop和线程

​ 程序运行的时候需要一个常驻线程,可以让线程接收到事件的时候干活,没事的时候休眠。我们执行下面的伪代码,一直等待消息,线程就不会退出了。

do {
   // 睡眠中等待消息
   // 接收消息
   // 处理消息
} while (消息 != 退出)

那么RunLoop和线程的关系是怎样呢?

​ 1)每条线程都有唯一的一个与之对应的RunLoop对象;

​ 2)RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;

​ 3)线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;

​ 4)RunLoop会在线程结束时销毁;

​ 5)主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop;

在RunLoop官方文档中,可以看到RunLoop和线程的关系

iOS RunLoop_第1张图片
RunLoop运行

​ 图中展现了RunLoop 在线程中的作用:从 input sources 和 timer sources 接受事件,然后在线程中处理事件。

三、RunLoop对象

1、获取RunLoop对象:

​ iOS中有两套来访问和使用RunLoop。

​ 1)Foundation框架:NSRunLoop,NSRunLoop是基于CFRunLoopRef的OC的封装;

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

​ 2)CoreFoundation框架:CFRunLoopRef;

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
2、CFRunLoopModeRef-运行模式:

​ Mode模式可以看做事件的管家,一个Mode管理着各种事件,在CFRunLoop.c文件中结构:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name; //mode名称
    Boolean _stopped; //mode是否被终止
    char _padding[3];
    CFMutableSetRef _sources0; //sources0 触摸事件处理,performSelector:onThread:
    CFMutableSetRef _sources1; //sources1 基于port的线程通信,系统事件捕捉
    CFMutableArrayRef _observers; //观察者 用于监听RunLoop状态,UI刷新,AutoreleasePool
    CFMutableArrayRef _timers; //定时器
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet; //端口
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

一个CFRunLoopMode对象有一个name,若干个sources0、sources1、observers、timers和ports,可见事件都是由Mode在管理,而RunLoop管理Mode。RunLoop运行时总是指定一种Mode,就是currentMode,当切换Mode时必须退出当前Mode,重新进入RunLoop。
iOS RunLoop_第2张图片
RunLoop和Mode关系

在iOS中五种模式如下图:
iOS RunLoop_第3张图片
RunLoopMode

注意:实际开发中有三个使用

​ 1)NSDefaultRunLoopMode:这个是默认模式,使用最多;

​ 2)UITrackingRunLoopMode:界面追踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;

​ 3)NSRunLoopCommonModes:这个不是一种模式,默认包括NSDefaultRunLoopMode和UITrackingRunLoopMode;

比如常用定时器代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建tableView
    [self tableView];
    // 创建一个定时器,并把它放在当前的RunLoop上,模式是NSDefaultRunLoopMode
//    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    
    // 创建一个定时器,但是必须手动把它添加到RunLoop中,添加的时候可以指定Mode
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    // 默认模式,运行执行timerAction,当滑动tableView时,timerAction就不会执行
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
    // 通用模式组合,运行执行timerAction,当滑动tableView时,timerAction依然继续执行
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    
    // UI追踪模式,此时默认不执行timerAction,当滑动tableView时,才会执行timerAction
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:UITrackingRunLoopMode];
}

四、RunLoop项目应用

1、解决NSTimer在滑动时停止工作的问题

​ 1)NSTimer:如上讲述,创建timer后,将它放进当前的RunLoop,Mode格式为NSRunLoopCommonModes,NSRunLoopCommonModes包含NSDefaultRunLoopMode(没有触摸事件)和

UITrackingRunLoopMode(有触摸事件),所以不管有没有滑动事件都会执行timer。NSTimer有个属性tolerance容忍度,NSTimer依赖于RunLoop,所以执行timer的时间不会非常准确,使用的时候需要注意这点。

​ 2)GCD Timer:GCD定时器,使用GCD定时器不会受RunLoop的影响;

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建tableView
    [self tableView]; 
    
    // 创建一个定时器(dispatch_source_t本质还是个OC对象)
    dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    self.timer = sourceTimer; //持有
    
    // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    //每隔一秒执行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(sourceTimer, start, interval, 0);
    // 设置回调
    dispatch_source_set_event_handler(sourceTimer, ^{
        NSLog(@"num = %ld", self.num ++);
    });
    // 启动定时器
    dispatch_resume(sourceTimer);
}

运行代码,滑动tableView,可以看到打印台仍然有执行block的回调,也就是滑动事件不会影响GCD定时器。

​ 3)CADisplayLink:是一个执行频率(fps)和屏幕刷新相同的定时器(可以修改preferredFramesPerSecond属性来修改具体执行的频率)。时间精度比NSTimer高,但是也要添加到RunLoop里。通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建tableView
    [self tableView];
    
    CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerAction)];
    self.timer = timer;
    if (@available(iOS 10.0, *)) {
        timer.preferredFramesPerSecond = 30; //30帧
    } else {
        timer.frameInterval = 2; //屏幕刷新60帧,每2帧刷一次,就是每秒30帧频率
    }
    // 添加到NSRunLoopCommonModes
    [timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

运行代码,滑动tableView,当然也会执行timerAction,与NSTimer类似。

2、控制线程生命周期(线程保活)

​ 有时候我们需要常驻线程来处理频繁的事务,比如早期的AFNetworking创建一个常驻线程处理网络事务,比如监测网络状态等。

默认情况一个线程创建出来,运行完要做的事情,线程就会消亡。而程序启动的时候,创建的主线程已经加入到mainRunLoop中,所以主线程不会消亡。

线程保活代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"mainThread = %@", [NSThread mainThread]);
    
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
    self.thread = thread;
    thread.name = @"KeepThread";
    [self.thread start];
}

// 子线程运行
- (void)runThread {
    NSLog(@"runThread = %@", [NSThread currentThread]);
    // 给runloop添加一个NSPort,就是添加一个事件源,也可以添加一个timer,或者observer,让runloop不会挂掉
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"-----runThread end-----"); //不会执行
}

// 测试线程保活
- (void)testThread {
    NSLog(@"testThread = %@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(testThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

点击屏幕触发performSelector,打印结果:

mainThread = {number = 1, name = main} //主线程
runThread = {number = 5, name = KeepThread} //运行子线程
testThread = {number = 5, name = KeepThread} //子线程保活
3、性能优化

​ 1)tableView图片显示优化:由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];

​ 2)AsyncDisplayKit:AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架(最新的改为Texture),其原理大致如下:

​ UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

​ 其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

​ 为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

​ ASDK 仿照QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

​ 3)第二条是Facebook写好的框架,下面自己手动写下计算cell的预缓存高度的伪代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CFRunLoopRef currentLoop = CFRunLoopGetCurrent(); //获取当前的RunLoop
    CFStringRef mode = kCFRunLoopDefaultMode; //mode
    // 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
        // 在RunLoop处于“空闲”状态时进行计算
        NSLog(@"Thread = %@", [NSThread currentThread]);
        sleep(1);
        CFRunLoopRemoveObserver(currentLoop, observer, mode); //移除observer
    });
    CFRunLoopAddObserver(currentLoop, observer, mode); //添加observer
}

觉得写的不错,有些启发或帮助,点个赞哦!

你可能感兴趣的:(iOS RunLoop)