RunLoop学习

开篇几道面试题:

讲讲RunLoop,在项目中有用到吗?
runloop内部实现逻辑
runloop和线程的关系
timer与runloop的关系
程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应,为什么?怎样可以解决?
runloop是怎样响应用户操作的,具体流程是什么样?
说说runloop的几种状态
runloop的model作用是什么?

RunLoop,顾名思义,就是运行循环,就是在程序运行过程中循环做一些事情。
RunLoop的应用范围:
定时器(Timer)、PerformSelector、GCD
事件响应、手势识别、界面刷新、网络请求、自动释放池

RunLoop学习

RunLoop做了一个类似do-while循环的事情,有事的时候去处理消息,没事的时候就睡眠并等待消息。
伪代码:

RunLoop学习

RunLoop的基本作用:

保持程序的持续运行
处理App中的各种事件(比如:触摸事件、定时器事件)
节省CPU资源,提高程序性能:该做事做事,没事去休息

RunLoop对象

iOS有两套API来访问和使用runloop
在Foundation框架下的NSRunLoop
在Core Foundation框架下的CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是对CFRunLoopRef的一层OC包装
CFRunLoopRef是开源的

NSRunLoop *runloop1 = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();

两种方法都可以拿到RunLoop对象。

RunLoop与线程关系

每条线程都有唯一的一个与之相对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop会在线程结束时销毁
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。
稍微翻下源码:
RunLoop学习

RunLoop学习

确实表明:
每条线程都有唯一的一个与之相对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建

RunLoop相关的类

Core Foundation中关于RunLoop的5个类

  1. CFRunLoopRef
  2. CFRunLoopModeRef
  3. CFRunLoopSourceRef
  4. CFRunLoopTimerRef
  5. CFRunLoopObserverRef

CFRunLoopRef的定义:

RunLoop学习

RunLoop学习

可以看出,里面有一个CFRunLoopModeRef类型的_currentModel,以及CFMutableSetRef类型的_modes。
需要说明的是,CFMutableSetRef是一个集合,集合里面是也是CFRunLoopModeRef类型,那么

CFRunLoopModeRef的结构又是怎样的呢?

RunLoop学习

可以看出,CFRunLoopModeRef里面主要有:
mode的名字,以及sources0、sources1、observers、timers。
sources0、sources1里面装着CFRunLoopSourceRef类型的对象
observers里面装着CFRunLoopObserverRef类型的对象
timers里面装着CFRunLoopTimerRef类型的对象

可以看出:
CFRunLoopRef里面有CFRunLoopModeRef类型的_currentModel和_modes,而CFRunLoopModeRef里面又有CFRunLoopSourceRef、CFRunLoopObserverRef和CFRunLoopTimerRef三种类型。

CFRunLoopRef包含CFRunLoopModeRef
CFRunLoopModeRef包含CFRunLoopSourceRef、CFRunLoopObserverRef和CFRunLoopTimerRef
RunLoop学习

RunLoop里面有很多mode(模式),但是在运行的时候,只会选择一种mode(模式)。

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个source0、source1、observer、timer
RunLoop启动时只能选择其中一个Mode作为currentMode。
如果需要切换Mode,只能退出当前loop,重新选择一个Mode进入。
不同组的source0、source1、observer、timer能分隔开来,互不影响
如果Mode里没有任何source0、source1、observer、timer,RunLoop会立马退出

常见的两种Mode模式:
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程在这个Mode下运行。
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。

NSRunLoop *runloop1 = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
NSLog(@"%p, %p", runloop1, runloop2);

结果:0x600000f7cc60, 0x60000177c000

怎么打印的结果地址不一样呢?难道一个线程有两个runloop?
其实并不是,看下一个结果:

NSLog(@"111%@, 333%@", runloop1, runloop2);
111<CFRunLoop 0x60000177c000.....
333<CFRunLoop 0x60000177c000 [0x7fff80617cb0].....

可以看到,runloop1和runloop2的地址是一样的,都是runloop2的地址。
说明,NSRunLoop是对CFRunLoopRef的一层封装,实际runloop还是CFRunLoopRef的地址。

RunLoop学习

CFRunLoopModeRef里面的内容分别代表什么?

Source0
触摸事件处理
performSelector:onThread:

Source1
基于Port的线程间通信
系统事件捕捉

Timers
NSTimer
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)

RunLoop学习

lldb指令下,bt可以打印出函数调用栈
可以发现,触摸事件,是由Sourse0处理的。

CFRunLoopObserverRef

RunLoop学习

添加Observer监听RunLoop的所有状态

RunLoop学习

RunLoop的运行逻辑

RunLoop学习

RunLoop学习

RunLoop学习

RunLoop休眠的实现原理

休眠是调用的内核api

RunLoop学习

RunLoop在实际开中的应用

  1. 控制线程生命周期(线程保活)
  2. 解决NSTimer在滑动时停止工作的问题
  3. 监控应用卡顿
  4. 性能优化

有关NSTimer和runloop的关系,可以参考NSTimer学习笔记

如何将子线程一直存在?(面试基本会遇到)

当然,你不能用strong指针拥有thread就算完事,那样只是拥有这个thread,但是thread可能已经失效,不能使用了。

@interface YZThread : NSThread
@end

#import "YZThread.h"
@implementation YZThread
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

----------------------------------
ViewController.m文件
#import "ViewController.h"
#import "YZThread.h"

@interface ViewController ()
@property (strong, nonatomic) YZThread *thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.stopped = NO;
    __weak typeof(self) weakSelf = self;
    YZThread *thread = [[YZThread alloc] initWithBlock:^{
        NSLog(@"begin - %s, %@", __func__, [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (weakSelf && !weakSelf.isStopped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        /**
        [[NSRunLoop currentRunLoop] run];
        相当于在执行下面的代码
        while (1) {
            [[NSRunLoop currentRunLoop] runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>]
        }
        有个while循环,一直在调用
         */
        NSLog(@"end - %s, %@", __func__, [NSThread currentThread]);
    }];
    self.thread = thread;
    [thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.thread == nil) {
        return;
    }
    //waitUntilDone:YES 等test方法执行完毕,再执行打印123。NO  不等test方法执行完毕,就执行打印123
    [self performSelector:@selector(test) onThread:self.thread withObject:self waitUntilDone:YES];
    NSLog(@"123");
}

- (IBAction)stop:(id)sender {
    if (self.thread == nil) {
        return;
    }
     [self performSelector:@selector(threadStop) onThread:self.thread withObject:self waitUntilDone:YES];
}

- (void)threadStop
{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());//停止runloop
    NSLog(@"%s, %@", __func__, [NSThread currentThread]);
    self.thread = nil;
}

//子线程需要做的操作
- (void)test
{
    NSLog(@"%s, %@", __func__, [NSThread currentThread]);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self stop:nil];
}
@end

简单来说就是:
在子线程中加入runloop来保证子线程存活

上面的用法能满足需求,但,可以做的更好,做下封装:

#import <Foundation/Foundation.h>
@interface YZKeepAliveThread : NSObject
/**
 开启线程
 */
- (void)run;

/**执行block*/
- (void)excuteTaskWithBlock:(void(^)(void))block;

/**
结束线程
*/
- (void)stop;
@end



#import "YZKeepAliveThread.h"
@interface YZKeepAliveThread ()
@property (strong, nonatomic) NSThread *thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation YZKeepAliveThread

- (instancetype)init
{
    if (self = [super init]) {
        self.stopped = NO;
        __weak typeof(self) weakSelf = self;
        self.thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"begin - %s, %@", __func__, [NSThread currentThread]);
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
            /**
            [[NSRunLoop currentRunLoop] run];
            相当于在执行下面的代码
            while (1) {
                [[NSRunLoop currentRunLoop] runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>]
            }
            有个while循环,一直在调用
             */
            NSLog(@"end - %s, %@", __func__, [NSThread currentThread]);
        }];
    }
    return self;
}

/**
 开启线程
 */
- (void)run
{
    [self.thread start];
}

/**执行block*/
- (void)excuteTaskWithBlock:(void(^)(void))block
{
    if (self.thread == nil || block == nil) {
        return;
    }
    //waitUntilDone:YES 等test方法执行完毕,再执行打印123。NO  不等test方法执行完毕,就执行打印123
    [self performSelector:@selector(__excuteTaskWithBlock:) onThread:self.thread withObject:block waitUntilDone:NO];
}

- (void)__excuteTaskWithBlock:(void(^)(void))block
{
    block();
}

/**
结束线程
*/
- (void)stop
{
    if (self.thread == nil) {
            return;
     }
     [self performSelector:@selector(threadStop) onThread:self.thread withObject:self waitUntilDone:YES];
}

- (void)threadStop
{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());//停止runloop
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self stop];
}
@end


ViewController.m文件
#import "ViewController.h"
#import "YZKeepAliveThread.h"
@interface ViewController ()
@property (strong, nonatomic) YZKeepAliveThread *thread;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[YZKeepAliveThread alloc] init];
    [self.thread run];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    __weak typeof(self) weakSelf = self;
    [self.thread excuteTaskWithBlock:^{
        [weakSelf test];
    }];
}

- (IBAction)stop:(id)sender {
    [self.thread stop];
}

//子线程需要做的操作
- (void)test
{
    NSLog(@"子线程需要做的操作--%s, %@", __func__, [NSThread currentThread]);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

更多学习有关RunLoop深入理解RunLoop

上一篇:Linux下Nagios的安装与配置


下一篇:《计算机系统:系统架构与操作系统的高度集成》——2.8 编译函数调用