Runloop分享

1,420 阅读6分钟

say something

由于近期项目比较忙, 准备的很仓促, 加之个人水平有限, 就抛砖引玉吧。有理解不对的地方, 还请大佬们指正。 🤦‍🤦‍🤦‍

intro

为什么程序会保持运行?

看看 APP 的入口函数:
//  main.m中
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

首先看到, 程序主函数 main() 需要一个 int 类型的返回值; main() 内部调用了 UIApplicationMain() 函数, 并返回它的返回值; UIApplicationMain() 是程序的入口函数, 其内部启动了一个 RunLoop, 这个默认启动的 RunLoop 是跟主线程相关联的, 导致了主线程常驻; 所以 UIApplicationMain() 函数一直没有返回, 从而保证了 main() 函数也没有返回, 保持了程序的持续运行。

Runloop 是什么? 先来了解下 Event Loop

Tips: swift 没有 main?

官方解释:

In Xcode, Mac templates default to including a “main.swift” file, but for iOS apps the default for new iOS project templates is to add @UIApplicationMain to a regular Swift file. This causes the compiler to synthesize a mainentry point for your iOS app, and eliminates the need for a “main.swift” file.

// AppDelegate.swift 中代码:
import UIKit
@UIApplicationMain 
class AppDelegate: UIResponder, UIApplicationDelegate { }

Event Loop

一般情况下, 一个线程一次只能执行一个任务, 执行完后, 线程就会退出。 如果我们想让线程不退出, 并能随时处理事件, 通常的逻辑是这样的:

func loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event Loop。 它的关键在于: 怎样让线程在没有事件处理时休眠, 以节约性能, 在有事件到来时立刻响应事件。

Event Loop 在很多系统和框架里都有实现, macOS / iOS 下, 对应实现了 Runloop。

RunLoop 简介

简介:

RunLoop 字面意思为 运行循环, 表示在程序运行中循环着做一些事情; 其实它内部就是一个 do-while 循环, 在这个循环内部, 合理安排作息时间, 有事时做事, 没事做就休息。

基本作用:

  1. 保持程序的持续运行;
  2. 处理App中的各种事件 (比如触摸事件、定时器事件、Selector事件、GCD回主线程 等);
  3. 减少CPU空转, 节省CPU资源, 提高程序性能。
  4. 管理 autoreleasepools

注意:

  1. RunLoop 只能选择一个 Mode 启动, 即 CurrentMode;
  2. 如果需要切换 Mode, 只能退出 Loop, 再重新指定一个 Mode 进入。
  3. 如果 CurrentMode 中没有任何Source或者Timer, 那么就直接退出RunLoop。

应用举例:

  1. 子线程保活 (开启子线程的runloop, 同时保证 mode 不空)
  2. 解决滑动页面定时器停止问题 (将timer添加到CommonModes)
  3. 在特定模式下执行某些任务 (performSelector:withObject:afterDelay:inModes)
  4. 卡顿监测 (监听runloop, BeforeSources -> BeforeWaiting 以及 AfterWaiting -> BeforeTimers 的时间间隔)
  5. 解决 tableView 加载 cell 时的耗时卡顿问题 (监听RunLoop, 将多个耗时操作分开执行, 在每次 RunLoop 唤醒时去执行一个耗时任务。)
  6. 等等..

AutoreleasePool 的关系:

Runloop 管理着 AutoreleasePool 的创建和释放 第一次创建: runloop启动的时候 最后一次销毁: runloop退出的时候 其他时候的创建和销毁: 当runloop即将休眠的时候, 此时说明事件全部处理完毕, 销毁之前的释放池, 同时重新创建一个新的。

App启动后, 苹果在主线程 RunLoop 里注册了2个 Observer, 其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

  1. 第1个 Observer 监视的事件是:
  • Entry(即将进入Loop), 其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647(-0x7FFFFFFF), 优先级最高, 保证创建释放池发生在其他所有回调之前。
  1. 第2个 Observer 监视了2个事件:
  • BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧池并创建新池;
  • Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647(0x7FFFFFFF), 优先级最低, 保证其释放池子发生在其他所有回调之后。

在主线程执行的代码, 通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着, 所以不会出现内存泄漏, 我们也不必显式创建 Pool 了。

线程 的关系:

  1. 在第一次获取时创建, 在线程结束时销毁;
  2. 线程和RunLoop一一对应, RunLoop内部通过字典存储, 线程作为key, RunLoop作为value。
  3. 主线程的RunLoop在应用启动的时候就会自动创建, 子线程的RunLoop需要手动创建并启动。

GCD回主线程

当调用 dispatch_async(dispatch_get_main_queue(), block) 时, libDispatch 会向主线程的 RunLoop 发送消息, RunLoop会被唤醒, 并从消息中取得这个 block, 并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。 但这个逻辑仅限于 dispatch 到主线程, dispatch 到其他线程仍然是由 libDispatch 处理的。

事件响应

苹果注册了一个 Source1 (基于 mach port) 用来接收系统事件, 其回调函数为 __IOHIDEventSystemClientQueueCallback()。 当一个硬件事件 (触摸/锁屏/摇晃等) 发生后, 首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。 SpringBoard 只接收按键(锁屏/静音等)触摸加速接近传感器等几种 Event, 随后用 mach port 转发给需要的App进程。 随后苹果注册的那个 Source1 就会触发回调, 并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。 _UIApplicationHandleEventQueue()会把 IOHIDEvent 处理并包装成 UIEvent 进行处理, 分发给 UIWindow。像 UIButton 点击touchesBegin/Move/End/Cancel 这些事件都是在这个回调中完成的。

手势识别

如何分辨用户所做的手势是哪一种手势呢? 上面的 _UIApplicationHandleEventQueue() 识别了一个手势时, 首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting 事件, 这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(), 其内部会获取所有刚被标记为待处理的 GestureRecognizer, 并执行 GestureRecognizer 的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时, 这个回调都会进行相应处理。

CFRunloop 简介

Core Foundation 源码:

Core Foundation源码查看

Core Foundation源码下载

CFRunLoop 主要有5个类:

CFRunLoopRef  // 包含ModeRef类型变量的数组成员(_modes)和当前的mode
CFRunLoopModeRef // 包含 SourceRef、TimerRef、ObserverRef 类型成员
CFRunLoopSourceRef  // source0、source1
CFRunLoopTimerRef // 和 NSTimer 是 toll-free bridged, 可以混用。
CFRunLoopObserverRef

toll-free bridged

这5个类的关系:

一个RunLoop有一个 _modes 成员, 其包含若干个 Mode , 每个 Mode 又包含若干个 Source0 / Source1 / Timer / Observer

RunLoop

runloop 运行机制

先上一张从 ibireme 盗的图:

RunLoop 运行机制

runloop 获取:

苹果不允许直接创建 RunLoop, 只提供了获取 RunLoop 的API

Foundation 中:
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
Core Foundation 中:
CFRunLoopRef CFRunLoopGetMain(void);
CFRunLoopRef CFRunLoopGetCurrent(void);
CFRunLoopRef _CFRunLoopGet0(pthread_t t); // private
CFRunLoopRef __CFRunLoopCreate(pthread_t t); // private

runloop 运行:

Foundation 中:
- (void)run; // 无超时运行在 NSDefaultRunLoopMode, 除非移除所有 timer和source, 否则无法停止
- (void)runUntilDate:(NSDate *)limitDate; // 也是运行在 NSDefaultRunLoopMode
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; // 用指定的Mode启动, 带超时限制
Core Foundation 中:
void CFRunLoopRun(void); // kCFRunLoopDefaultMode, 1.0e10, false
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle);
SInt32 CFRunLoopRunSpecific(); // private
static int32_t __CFRunLoopRun(); // private

runloop 休眠:

__CFRunLoopServiceMachPort(); 调用 mach_msg() 进行 用户态 / 内核态 的切换

用户态 -> 内核态

runloop 停止:

  1. 移除掉 runloop 中的所有事件源(timer 和 source)。
  2. 设置超时时间。
  3. 使用 CFRunLoopRun()CFRunLoopRunInMode() 方法运行起来的 runloop 可以用 CFRunLoopStop() 停止。

推荐阅读

深入理解RunLoop