iOS-Runloop原理与应用

Runloop:运行循环-死循环

主要目的:提高性能,有事情就干,没事情休眠。
参考https://blog.csdn.net/callauxiliary/article/details/107419854

主要应用

1,保证线程一直运行,处理事件,比如触摸事件,时钟事件,都是由runloop完成。
2,优化卡顿:将一次runloop执行完的任务,放到多次runloop中执行。
3,UI滑动时计时不准确的问题,设置定时器的Mode为:NSRunLoopCommonModes。
4,需要在线程上使用performSelector*****方法(运行时方法)。例如

让UITableView、UICollectionView等延迟加载图片

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

这算一个优化点,这里用的defaultMode,就是滚动的时候不加载图片,停止滚动加载图片

Runloop运行原理

当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。
Runloop的生命周期:在第一次获取时创建,在线程结束时销毁。

Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
系统默认注册了5个mode

    a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

        b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

        c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

        d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

        e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。

主要分为三大步骤:

1、首先根据modeName找到对应的Mode

2、如果model里没有Source/Timer/Observer,直接返回

3、如果model有Source/Timer/Observer,就会即将进入runloop


image.png

详细步骤

简单来讲就是一个do-while循环,有输入源就唤醒,没有就处于休眠状态,官方解释链接。具体过程如下:

  1. 通知观察者开始进入 Runloop
  2. 通知观察者开始处理 Timer 事件
  3. 通知观察者将要处理非基于 port 的事件
  4. 启动准备好的事件
  5. 如果基于 port 的事件已经准备好,立即启动。并进入步骤 9
  6. 通知观察者进入休眠状态
  7. 线程进入休眠直到以下事件出现
    • 基于 port 的事件源出现
    • 定时器启动
    • 设置的时间已经超时
    • RunLoop 被唤醒
  8. 通知观察者线程将被唤醒
  9. 处理已经进入的事件
    • 如果用户自定义的计时器启动,处理事件并重启 Runloop。转到第二步
    • 输入源启动,传递消息
    • 如果 Runloop 被显示唤醒且没有超时,重启 Runloop。转到第二步
  10. 通知观察者 Runloop 已经退出

Runloop组成

两种对象:NSRunLoop 和 CFRunLoopRef。

CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。
1,Timder,时钟
2,source,事件源:一切事件的来源,按照函数的调用栈分为source0:非系统内核事件,source1:系统内核事件。
3,observer,观察者,观察的runloop的循环周期。

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

APP启动过程

1,当点击APP时,操作系统开启一条线程执行程序的main函数。这个线程就是这个程序的主线程。这个线程是一个常驻线程,因为这个线程的Runloop被开启了,不会被线程池释放。

线程和runloop 的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

Runloop的作用

1,保证线程不退出。
2,监听所有的事件,比如触摸事件,时钟事件,网络事件等。
子线程要监听事件,就必须开启子线程的runloop。

验证将时钟添加到runloop才可以实现监听

1,scheduledTimerWithTimeInterval初始化方法默认是添加到当前线程的runloop的,所以不用显示的添加。
这种方法添加是NSDefaultRunLoopMode模式,UI事件会中断响应。

-(void)test{

    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

3,子线程结束runloop

-(void)test{
    //要放在子线程,设置runloop时间,时间过了没有执行的任务,子线程就会结束,并释放
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //将时钟手动添加到当前的runloop,否者时钟方法不会执行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        //当为yes时,取消RunLoop循环
        while (!_finish) {
            NSLog(@"来了11");
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
        }
        NSLog(@"来了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"22222");
    _finish = YES;
}

2,timerWithTimeInterval初始化方法需要手动将时钟添加到当前线程的runloop中,否则时钟不能被监听。

-(void)test{
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    //将时钟手动添加到当前的runloop,否者时钟方法不会执行
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

runloop的三种Mode

1,当对象使用NSDefaultRunLoopMode(默认模式,一般处理网络事件和Timer事件),在runloop执行UI事件的时候,会暂停响应该对象的事件。
2,当对象使用UITrackingRunLoopMode(UI模式,一般处理UI事件),只有在runloop执行UI事件的时候,才会会暂停响应该对象的事件。
3,当对象使用NSRunLoopCommonModes(占位模式,这个不是一个真的模式,只是同时添加了上面两种模式),在runloop执行UI事件的时候,也会响应该对象的事件(最理想的结果)。

开启当前线程的runloop

子线程处理需要监听的事件(需要子线程活着一直监听的情况),可以启动子线程的runloop,让子线程成为常驻线程,避免被释放,从而可以响应监听的事件。唯一让线程不被释放的方法就去启动他的runloop。
解决NSTimer在滑动时停止工作的问题的办法
1,设置定时器的Mode为:NSRunLoopCommonModes
2,将NSTimder放到子线程并且开启runloop,name就是算是NSDefaultRunLoopMode,滑动UI也不会影响Timer。
NSTimder处理的事件一般放在子线程,并开启子线程的runloop,避免影响主线程处理事件。

-(void)test{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //将时钟手动添加到当前的runloop,否者时钟方法不会执行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
        //手动开启当前线程的runloop,这是一个死循环
        [[NSRunLoop currentRunLoop]run];
        NSLog(@"这里不会走了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

GCD实现定时器

Dispatch Source创建定时器timer,优于NSTimer
必须要声明timer属性强应用,不然会被释放

@property (nonatomic, strong) dispatch_source_t timer;
-(void)test2{
    NSLog(@"启动");
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"间隔一秒执行%@",[NSThread currentThread]);
    });
    dispatch_resume(_timer);
    NSLog(@"启动");
}

优化练习

tableview在快速滑动时,高清图片造成卡顿优化。
原因:tableview在快速滑动时,runloop必须在一次循环内,渲染所有的图片。
思路:runloop每次循环只加载一张图片,例如一个屏幕总的可以显示15张图片,那么就分15次加入到runloop中加载。相当于把一次做完的事情,分成了15次。例如监听kCFRunLoopBeforeWaiting事件,回调函数加载图片,加载完成后删除这个任务。每次加载图片后都会再次到达kCFRunLoopBeforeWaiting,还有图片任务就会继续执行。
通过CFRunLoopObserverRef监听runloop 的状态,函数指针回调,Ref是引用,指针的意思。
步骤
1,添加runloop观察者,观察runloop 的状态变化。
2,将加载图片的代码块放到数组中。
3,在观察者回调中,拿出数组中加载图片的代码执行。

你可能感兴趣的:(iOS-Runloop原理与应用)