RunLoop基础知识

作用
  1. 保持程序的持续运行
  2. 处理APP中的各种事件(比如触摸事件、定时器事件、Selector事件)
  3. 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 (用户态 切换到 内核态)
RunLoop与多线程的关系
  • 线程与RunLoop是一一对应的关系;RunLoop保存在一个全局的NSDictionary字典里面,线程为key,RunLoop为Value
  • 主线程的RunLoop在Main函数中自动开启,保证了程序的持续运行。
    子线程的RunLoop需要主动创建;RunLoop在第一次获取时由系统内部创建,在线程结束时销毁。(苹果不允许直接创建 RunLoop)
  • 只能在一个线程的内部获取其 RunLoop(主线程除外),它是寄生于线程的
    参考

RunLoop 有五种运行模式,其中常用的有1、2两种

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
  4. UIInitializationRunLoopMode: 启动Mode,在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  5. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

kCFRunLoopCommonModes模式

一种模式组合,在主线程默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程只包NSDefaultRunLoopMode。
注意:
①在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
②在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。

参考

每次RunLoop启动时,只能指定其中一个 Mode;如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入;这样做主要是为了分隔开不同组的 事件源,让其互不影响

4625389-76720407d223fdd9.png
RunLoop对象.png

source就是输入源事件,Timer即为定时源事件,Observer相当于消息循环中的一个监听器

Observer的创建

  • NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
// 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
                
            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
                
            default:
                break;
        }
    });
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);

上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。
但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。

RunLoop正常运行的条件是:

  1. 有Mode。
  2. Mode有事件源。
  3. 运行在有事件源的Mode下

经过NSRunLoop封装后,只可以往mode中添加两类事件源:NSPort(对应的是source1)和NSTimer(Timer源放在后面讲)。

启动RunLoop有那些方法及区别

NSRunLoop总共包装了3个方法供我们使用

- (void) run

不建议使用。 除非希望子线程永远存在,因为这个会导致Run Loop永久性的运行在NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法销毁,只能永久运行下去

- (void)runUntilDate:(NSDate *)limitDate

//举例代码
while (!Stop){

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}

比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate

有一个超时时间限制,而且可以设置运行模式
这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。同样可以像上面那样用while包起来使用

关于GCD

RunLoop 底层也会用到 GCD 的东西,但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

PerformSelecter

4625389-3744cd7640e52fd4.png
performSelector相关的知识.png
4625389-f9bfbc54e7546aab.png
runLoop调用run方法的内部实现.png

子线程保活

4625389-4cb872ab92738f9e.png
子线程保活.png
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空线程
    self.thread = nil;
}

源码地址

自动释放池

Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。

那么什么时候释放,怎么释放呢?

主线程的RunLoop默认启动,并会自动创建自动释放池。当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
子线程需要在线程中手动添加自动释放池
NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool。GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool
参考

RunLoop的应用

  1. NSTimer 用于轮播图
  2. TableView滚动时不显示图片
  3. TableView停止滚动时计算行高或者预加载
    sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。
  4. 怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作。
  • tableView在滑动,处于UITrackingRunloopMode模式下。
  • 子线程请求的数据,那么在和主线程处理的时候,我们将更新的逻辑加载defaultMode下。那么defaultMode下的操作是不会执行的。
  • 滑动结束了,runloop由UITrackingRunloopMode又回到defaultMode下了,那么defaultMode下的更新操作就能执行了

RunLoop高级1
RunLoop高级2

参考2
参考3
参考4 很详细
参考5

猜你喜欢

转载自blog.csdn.net/weixin_33691817/article/details/87045389