RunLoop 介绍

Monday, February 10, 2020

RunLoop

RunLoop 概念

runloop 是线程生成的一个对象,一个线程只能有一个 runloop。一般来讲,一个线程执行完任务了,就会退出了线程,而 runloop 就是可以让线程能随时处理任务但并不退出的机制。

RunLoop 管理了需要处理的事件和消息,提供了一个函数来执行Event Loop 的逻辑,线程执行了这个函数之后,就会处于接受消息->等待->处理的循环了。

其中 NSRunloop 是基于 CFRunLoopRef 的封装,提供了面向对象的API,但不是线程安全的。

RunLoop 是不能直接去创建的,而是跟线程一一对应的,而且是懒加载的,当你创建了线程之后,里面是没有 RunLoop 的,直到你获取这个线程时,才会去创建,销毁则是在线程结束时。

runloop

RunLoop 内部逻辑 runloop2

mach_msg

这是系统内核在某个 port 收发消息使用的函数,发送用的参数是 MACH_SEND_MSG, 接受为 MACH_RCV_MSG mach_msg 可以当作多进程之间的一种通信机制,不同的进程使用同一个消息队列来交流数据,并且可以在参数中设置 timeout 值,在 timeout 之前没读到消息,会进入休眠状态。

RunLoop 流程

可以看上图,总体而言主要做三件事

  • performTask()
  • callout_to_observer()
  • sleep()

perfromTask

每次 runloop 的运行都会执行若干个 task,执行 task 的方式有如下几个

DoBlocks()

这种方式可以被开发者使用,通过CFRunLoopPerformBlock将一个 block 插入目标队列,插入的时候是绑定 mode 的

DoSources0()

source0可以被开发者使用,source1则只能系统使用,source0也是绑定 mode 的

DoSources1()

上面已说这个是系统使用的,拿来执行内部任务,比如渲染 UI

DoTimers()

开发者调用 NSTimer 相关的方法时,runloop 就会执行这个了,同理也是绑定mode 的

DoMainQueue()

开发者调用 GCD 将任务放到 main queue 中,runloop 会执行这个了,这里就没有 mode 参数了,所以与 mode 不相关,msg 是通过 mach_msg 函数从某个 port 读出的 msg

callout_to_observer

告知外部某个任务已被执行,或者是 runloop 现在处于什么状态

sleep

这个就是任务就执行,没任务就 sleep 了,没什么特殊的逻辑

RunLoop Mode

第一张图已经说明,一个 RunLoop 会有若干个 mode,mode 里面的内容图中也大致说明了,就不再说明,mainQueue 任务的执行与 mode 无关,所以 mode 结构没定义 mainQueue 相关信息

mode 种类

  • common mode
  • private mode

我们常见 common mode:

  • kCFRunLoopDefaultMode
  • UITrackingRunLoopMode
  • kCFRunLoopCommonModes
  • UITrackingRunLoopMode

我们经常会传入kCFRunLoopCommonModes作为参数,这样比如说你滑动屏幕,runloop 切换到UITrackingRunLoopMode时,你设置的定时器也不会受影响,如果放到kCFRunLoopDefaultMode,则定时器就停止了。

但这里是有个小问题的,如果当前的 runloop 是以 private mode 来运行的,你的定时器还是会停止的。

runloop 的切换方式有两种,一种在中途切换,另一种是当前 mode 结束之切换

网络请求中的 RunLoop

在 iOS 里,网络请求有这么几层

  • CFSocket
  • CFNetwork
  • NSURLConnection
  • NSURLSession

主要介绍下 NSURLConnection 的工作过程

使用 NSURLConnection 时,会传入一个 Delegate,当调用了 connection start 后,Delegate 就会不断收到事件回调了。

start 这个函数内部会获取 CurrentRunLoop,然后在 DefaultMode 里添加4个 Source0。

当开始网络传输时,NSURLConnection 会创建两个线程,一个是 CFSocket 的线程,一个是 NSURLConnectionLoader 的线程。

CFSocket 线程是处理底层socket 连接,NSURLConnectionLoader 内部使用 RunLoop 来接收底层socket 的事件,并通过之前添加的 Source0通知到上层 Delegate

NSURLConnectionLoader 中的 Runloop 会通过mach port 的 Source 来接收底层 CFSocket 的通知(也就是 Source1)

AFNetworking 中的 RunLoop

在 AFNetworking2.x 版本中,是基于 NSURLConnection 来构建的,所以我们可以看看这里是怎么处理的。

首先为什么2.x 版本需要常驻线程,这里上面的 NSURLConnection 已经说到了,这个是被设计成异步发送的,调用了 start 后,通过新建一些线程用底层的 CFSocket 去发送和接收请求,然后通知原线程的 RunLoop 去回调事件,所以2.x 版本需要常驻线程等回调。

那为什么不直接在主线程中等回调呢,一来是你还得切换 mode 不让滑动事件影响回调函数,二来是你拿到数据后还有一系列事情要做,比如序列化,所以放到子线程是更合适的。

总结来说,就是首先需要在子线程去start connection,请求发送后,所在的子线程需要保活以正常接收到 NSURLConnectionDelegate 回调方法。如果每来一个请求就开一条线程,并且保活线程,这样开销太大了。所以只需要保活一条固定的线程,在这个线程里发起请求、接收回调。

而在3.x 版本之后,就用了 NSURLSession 了,就不需要向上面那样了。NSURLSession 发起的请求,可以指定回调的 delegateQueue

比如

self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

这里展示一下2.x 版本的常驻线程的代码

+  (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;

}

start 函数

-  (void)start  {

    [self.lock lock];

    if  ([self  isCancelled])  {

        [self  performSelector:@selector(cancelConnection)  onThread:[[self  class]  networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

    }  else  if  ([self  isReady])  {

        self.state  =  AFOperationExecutingState;

        [self  performSelector:@selector(operationDidStart)  onThread:[[self  class]  networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

    }

    [self.lock unlock];

}
iOS

AutoLayout

常见并发场景