iOSRunloop 运行循环写下来的一些经验总结

2,265 阅读10分钟

前言

学如逆水行舟,不进则退!!共勉!!!

今天主要是来简单的分析一下iOSRunloop运行循环,主要是从以下四个方面分析:

  1. runloop是什么?
  2. runloop面试题?
  3. 运行循环内部结构是什么样子的?
  4. 运行循环底层实现原理?
  5. 主线程与子线程运行循环的区别?
  6. 运行循环的API
  7. iOS面试学习资料:领取地址

runloop是什么?

1、runloop:runloop是事件接收和分发机制的一个实现,是线程相关的基础框架的一部分

  • 一个runloop就是一个事件处理循环,用来不停的调度工作及处理输出事件,当需要和该线程进行交互的时候才会使用runloop!

  • 一个runloop有五种model。runloop也叫运行循环,作用是维持线程不退出,通过在内部维护事件循环来管理各种事件,包括定时器、用户交互、系统内部事件。当有消息需要处理时,运行循环被唤醒处理,处理完再进入休眠状态,等待下一次唤醒

  • 内部的事件循环是一个 do-while 循环, do-while 内部再插入接收消息的端口阻塞线程,使线程进入一种闲等待的状态,这是并不会消耗CPU资源。与咱们手动写的纯do-while 循环还是有区别的,手动写的是忙等待,很消耗CPU资源。

  • 能唤醒运行循环的消息有

    • 通过端口接收系统内部事件(Source1)
    • 用户交互事件响应 ,本质上也是通过端口唤醒(Source0)
    • timer
  • Runloop状态六种状态 1、启动 2、退出 3、即将处理timer事件 即将处4、理sources事件 5、被唤醒 6、即将进入休眠

  • runloop有:Source事件原、timer、observer 监听器、model

Source0 用户主动发出的事件 source1 非用户主动发出的事件

注意:一次Runloop循环需要最多渲染18张图片,一次runloop循环只渲染一张图片,循环18次来渲染18张图片,runloop在运行的时候创建自动释放池,在休眠的时候释放

Runloop model

UI模式优先级最高,UI模式被唤醒的时候,runloop只处理UI模式下的UI展示(tableView的滑动等)

UI模式 UITrackingRunLoopModel 只能被触摸才能唤醒 唤醒之后其他事件暂停

NSDefaultRunLoopModel 默认模式 苹果建议放时钟 网络事件 通常主线程是在这个Mode下运行。

NSRunLoopCommonModel 不是一个真正的Runloop模式 占位模式

GSEventReceiveRunLoopModel 接收系统事件的内部model,通常用不到

UIinitializationRunLoopModel 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

2、线程和runloop是什么关系

每条线程都有唯一的一个与之对应的runloop对象;主线程的runloop已经自动创建,子线程的runloop需要自动创建;runloop在第一次获取时创建,在线程结束时销毁

3、runloop的model的作用

指定事件在运行循环中的优先级,线程的运行需要不同的模式,去响应各种不同的事件,去处理不通情景模式。(比如可以优化tableView的时候设置UITrackingRunLoopModel下不进行一些操作,比如设置图片等)

二、面试题

1、滑动ScrollView时,NSTimer 为什么停止工作

滑动ScrollView时,主线程的RunLoop会切换到UITrackingRunLoopModel这个model,执行的也是UITrackingRunLoopModel下的任务,而timer是添加在NSDefaultRunLoopMode下的,所以timer任务不会执行,只有当UITrackingRunLoopModel的任务执行完毕,runloop切换到NSDefaultRunLoopModel后,才会继续 执行timer,保证TableView伤的定时器执行就要他把添加到UITrackingRunLoopModel中去执行

注意timer在不需要的时候,一定要记得调用invalidate的方法定时器失效,否则得不到释放

三、运行循环内部结构是什么样子的?

1. RunLoop结构体的定义:

struct __CFRunLoop {
    __CFPort _wakeUpPort;				// used for CFRunLoopWakeUp
    _CFThreadRef _pthread;				// 一个运行循环对应一个线程
    CFMutableSetRef _commonModes;		// commonModes 是一个伪模式,内部默认包含default & traking
    CFMutableSetRef _commonModeItems;   // commonModes里面包含的注册事件(timer/source/observer)
    CFRunLoopModeRef _currentMode;		// 当前正在使用的模式,同一时间只能使用一个mode
    CFMutableSetRef _modes;				// 运行循环下的所有模式
    // ... 省略其他成员
};

2. RunLoopMode结构体的定义

struct __CFRunLoopMode {
    
    // ... 省略其他成员
    
    CFMutableSetRef _sources0;			// source0,
    CFMutableSetRef _sources1;			// source1,
    CFMutableArrayRef _observers;		// 监听运行循环状态的监听者的数组
    CFMutableArrayRef _timers;			// 加在运行循环上的定时器的数组
    __CFPortSet _portSet;				// 接收消息的端口集
    
#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
    __CFPort _timerPort;
    Boolean _mkTimerArmed;
#endif
};

1. RunLoopMode 的5种类型:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
  • UITrackingRunLoopMode:跟踪用户交互事件
    • 界面滑动时切换到这个mode, 可以保证滑动的时候不受其他 Mode 影响, 这就是iOS系统的 ScrollView 滑动非常顺滑的原因!
    • 因此,所有影响主线程 UI 滑动的操作,都放到default mode 上去执行。比如添加网络加载回来后的图片到 UITbaleview 的ImageView 上; 更新网络请求回来的数据到 tableview 的数据源上.
    • 防止tableview 加载图片导致滑动卡顿,也可以类似思路,将加载图片的方法放到default mode 上去执行。
    • 网络请求数据加载回来后,更新UI的动作也放在default mode, 这样不会影响用户正在滑动的体验。
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不 再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式
    • 是包含 Source/Timer/Observer 多个 Mode 中的一种解决方案,默认包含 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode
    • 如果把事件源添加到 common mode,就相当于在 Default Mode 和 Tracking Mode 里都添加了这个事件源。在这两种模式下的运行循环,都会执行该事件源。
    • 也可以通过CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);添加新的mode进入common modes 中

2. RunLoopMode的事件源

1 CFRunLoopSource:输入源/事件源 分为 source0 和 source1 两种

  1. source1: Mach port驱动的事件,如CFMachport,CFMessagePort。可监听系统端口、内核、其他线程发送的消息,能主动唤醒 RunLoop,接收分发系统事件。 具备唤醒线程的能力。
  2. source0: 是用户触发的事件,如UIEvent,CFSocket。不能直接唤醒线程,需要借助source1 来唤醒线程,将当前线程从内核态切换到用户态

2 CFRunLoopObserver: 监听者

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),			// 即将进入运行循环
    kCFRunLoopBeforeTimers = (1UL << 1),	// 即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),	// 即将处理sources
    kCFRunLoopBeforeWaiting = (1UL << 5),	// 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),	// 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),			// 已经退出运行循环
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

如何监听运行循环的事件?

- (void)viewDidLoad {
    [super viewDidLoad];
	[self.class addRunLoopObserver:(__bridge CFRunLoopRef)([NSRunLoop currentRunLoop])];
}

// RunLoop Observer
#pragma mark add Observer
+ (void)addRunLoopObserver:(CFRunLoopRef)runLoop {
    static CFRunLoopObserverRef observer;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CFOptionFlags activities = (kCFRunLoopAllActivities);          // before exiting a runloop run
        // allocator activities repeats order after CA transaction commits
        observer = CFRunLoopObserverCreateWithHandler(NULL, activities, YES, 720, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                    
                case kCFRunLoopEntry:
                    NSLog(@"进入 RunLoop");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将进入休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"刚从休眠中唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"退出 RunLoop");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将处理Timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将处理Source");
                    break;
                default:
                    break;
            }
        });
        CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}
  1. CFRunLoopTimer 定时器

NSTimer 是对 RunLoopTimer 的封装, 可实现定时执行和延迟执行的功能。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; 
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; 

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes; 

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel; 

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

运行循环与线程、mode、注册事件的关系
从以上结构体的定义上可以看出:\

  • 运行循环与线程是一对一的关系
  • 运行循环内部有多个运行模式
  • 运行模式内部有多种注册事件,其中source0,source1 只有1个,timer/observer 可能是多个。

image.png

四. 运行循环底层实现原理

  • 运行循环底层是依赖于[Mach Port] 来实现的。
  • 在整个运行循环的流程中,会启动一个 do-while 的循环
  • 循环内部通过__CFRunLoopServiceMachPort函数创建了拥有接收权限的端口来使线程处于等待唤醒的状态,这个等待过程于线程来讲是阻塞状态,对于运行循环来讲是休眠状态。所以运行循环的休眠本质上就是线程被阻塞。
  • 接收到端口的消息后,解除线程的阻塞状态,也就是运行循环被唤醒。之后判断唤醒端口类型
    • 是 timer 端口,执行 timer 回调函数 __CFRunLoopDoTimers
    • 是主线程端口,执行主线程回调函数 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    • 不是以上2种,就认为是source1端口,执行source1回调函数 __CFRunLoopDoSource1

3.1 运行循环执行流程图:

image.png

或者下面这样:

image.png

3.2 运行循环执行流程的伪代码:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; 
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; 

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes; 

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel; 

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

删减后的源码截图: 红色为执行具体动作,蓝色为回调监听事件

image.png

五. 主线程与子线程运行循环的区别

image.png

六.运行循环的API

在iOS中,有 2 个 API 来处理运行循环:\

  • NSRunLoop(位于Foundation框架)
  • CFRunLoop(位于CoreFoundation框架)
  • NSRunLoop 是 CFRunLoop 的面向对象 API 的封装,所以本质是一样的。

5.1 NSRunLoop run 的三种方式区别
开启某线程的Runloop,mode 中必须具有timer、source、observer任一事件源才能开启
// 运行循环外还有一层循环包裹着,不能使用CFRunLoopStop 来停止 [NSRunLoop.currentRunLoop run]; [NSRunLoop.currentRunLoop runUntilDate:[NSDate distantFuture]]; // 只有一层循环,能使用CFRunLoopStop 来停止, 一般用来线程保活,因为可以手动控制停止 [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

七. 运行循环的应用:
1.线程保活\

  • 主线程的线程保活,获取主线程的modes, 开启一个死循环,里面遍历重新开启运行循环CFRunLoopRunInMode. 可以用来**[采集崩溃日志、防止因崩溃而造成的闪退]**现象发生
  • 子线程的线程保活,在子线程上插入一个接收消息的端口,开启一个死循环,里面遍历重新开启运行循环CFRunLoopRunInMode。可以用来:
    • 监控卡顿: 子线程不断地给主线程发送消息,记录应答时间,超过指定时间未应答,即可认为是卡顿。
  • 节约开销:  当需要子线程频繁执行任务,多次创建线程产生大量的内存开销,开启子线程保活,可以节省一些开辟线程的开销。 - AFN 防止下载完成之前,线程提退出导致 NSOperation 对象接收不到回调AFN 创建单例的保活子线程,发起连接和接收回调,不占用主线程资源。

AFN常驻线程的部分源码:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
     @autoreleasepool {
          [[NSThread currentThread] setName:@"AFNetworking"];

          NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
          [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
          [runLoop run];
     }
}

+ (NSThread *)networkRequestThread {
     static NSThread *_networkRequestThread = nil;
     static dispatch_once_t oncePredicate;
     dispatch_once(&oncePredicate, ^{
          _networkRequestThread =
          [[NSThread alloc] initWithTarget:self
               selector:@selector(networkRequestThreadEntryPoint:)
               object:nil];
          [_networkRequestThread start];
     });

     return _networkRequestThread;
}

注意: CFRunLoopRunInMode 这个方法的参数 mode 不能是 kCFRunLoopCommonModes, 因为源码里面的逻辑就是这样,如果为kCFRunLoopCommonModes,直接返回kCFRunLoopRunFinished
CFRunLoopRunSpecific 源码:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
        ...
        return kCFRunLoopRunFinished;
    }
    ....
}

2.优化TableView滚动体验 利用 CFRunLoopMode 的特性,可以将更新 UI 的动作放到 NSDefaultRunLoopMode 的 mode 里,这样在滚动时 使用 UITrackingRunLoopMode ,NSDefaultRunLoopMode 这个 mode 的动作不会被执行。就不会因更新 UI 而影响到平滑滚动的效果。

  1. 图片加载完,更新 UI 到 ImageView时,使用NSDefaultRunLoopMode, 不影响滑动

UIImage *downloadedImage = ...; [self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];\

  1. 数据加载完,更新 UI 到 tableview时,使用NSDefaultRunLoopMode, 不影响滑动

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

赠人玫瑰,手留余香。如果有所收获,不如点赞支持一下吖。

iOS学习资料:领取地址

推荐阅读:面试宝典