RunLoop学习笔记

参考

深入理解RunLoop

深入研究 Runloop 与线程保活

RunLoop分享by孙源

RunLoop的概念

RunLoop是一个机制,让线程能随时处理事件但并不退出。这种机制实现的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

iOS提供了两个这样的对象:NSRunLoopCFRunLoopRef

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

Runloop和线程之间的关系

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

RunLoop的组成

  • Timer
    • 理解的Timer
  • SourceRunLoop数据源的抽象类protocol)
    • Source0:处理App内部时间,App自己负责触发(UIEventCFSocket
    • Source1:由RunLoopmach内核管理,由mach-port驱动
  • Observer
    • 许多机制都由Observer来触发
      • 例如CAAnimation,在afterwaiting收集完所有animation后才执行动画

RunLoop的Mode

  • NSDefaultRunLoopModekCFRunLoopDefaultMode):App的默认 Mode,通常主线程是在这个 Mode 下运行的
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用(iOS不公开提供)
  • NSRunLoopCommonModeskCFRunLoopCommonModes):Mode集合(iOS不公开提供)
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

RunLoop只能运行在一个mode下,如果要换mode,当前的loop也需要停下重启成新的。

例如:ScrollView滚动过程中NSDefaultRunLoopModekCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动,如果我们把一个NSTimer对象以NSDefaultRunLoopModekCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度,解决方案是将timer添加到NSRunLoopCommonModeskCFRunLoopCommonModes)中或者另起线程避免mode切换来解决。

RunLoop内部逻辑

RunLoop 内部的大致逻辑

RunLoop 内部是一个 do-while 循环。当你调用 CFRunLoopRun()时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop的底层实现

RunLoop 的核心是基于 mach port 的

iOS的内核是Mach,在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 MachIPC (进程间通信) 的核心。

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg()函数会完成实际的工作。

RunLoop调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap()这个地方。

iOS利用RunLoop实现的功能

  • AutoreleasePool

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

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

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

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

  • 事件响应

    苹果注册了一个 Source1 (基于 mach port的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

    当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent事件并由 SpringBoard接收。这个过程的详细情况可以参考这里SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用 mach port转发给需要的App进程。随后苹果注册的那个Source1 就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。

    _UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

  • 手势识别

    当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer标记为待处理。

    苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。

    当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

  • 界面更新

    当在操作 UI 时,比如改变了Frame、更新了 UIView/CALayer的层次时,或者手动调用了 UIView/CALayersetNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

    苹果注册了一个Observer 监听BeforeWaiting(即将进入休眠) 和Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

  • 定时器

    NSTimer其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个NSTimer注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个TimerTimer 有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

    CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的AsyncDisplayLink就是为了解决界面卡顿的问题,其内部也用到了 RunLoop

  • PerformSelecter

    当调用NSObjectperformSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread:时,实际上其会创建一个Timer加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

RunLoop常见应用

  • 使用RunLoopModetableview滑动优化
    • 通过不同的mode的切换,实现滑动时暂停加载图片等,停止滑动时加载
  • NSTimer计时任务
  • autorelease pool
    • RunLoop维护
  • 卡顿检测
    • 利用Observer记录主线程RunLoop休眠的时间
    • 利用Observer记录主线程RunLoop唤醒的时间
    • 计算这个(唤醒时间 - 休眠时间)的值,将其与正常的时间比较,判断当前是否会掉帧
  • Crash的程序回光返照
    • 接收到CrashSignal后手动重启RunLoop
  • 异步Test Case
    • sleep前验证

用到的框架

  • AFNetworking用于维护线程

    AFNetworking是基于NSURLConnection构建的,为了在后台也能接受回调,会创建一个线程,线程中添加一个RunLoop。由于没有调用RunLoop的停止方法,所以RunLoop不会退出。

  • AsyncDisplayKit

    ASDK 创建了一个名为 ASDisplayNode的对象,并在内部封装了UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 framebackgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。
    并在主线程的RunLoop中添加一个Observer,监听RunLoop进入休眠和退出的回调事件,收到回调后,遍历执行队列中的任务。

  • YYAsyncLayer

    • 实现原理如下:
      • 正常情况下:假设一次RunLoop需要处理50张图片
      • 使用YYAsyncLayer的情况:一次RunLoop处理1张图片,利用50个RunLoop去处理50张图片
        • 注意:在不计算休眠时间的情况下,50个RunLoop处理时间 = 1次RunLoop处理50张图片的时间

有关RunLoop的问题

  • RunLoop与线程的关系
    • 一对一,一个线程可以有一个RunLoop,也可以没有
    • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
    • RunLoop在第一次获取时创建,在线程结束时销毁
  • RunLoop只是个死循环吗?
    • 不是,RunLoop是个有时间限制的循环
  • 使用while(true)RunLoop哪个好?
    • RunLoop,因为RunLoop可以在不需要使用的时候休眠,节省CPU资源,而while(true)则一直处于CPU活跃状态
  • 为什么我们主线程需要有RunLoop
    • 保持线程存活,接受事件
    • 为了管理AutoreleasePool
  • [NSRunLoop currentRunLoop]实际上做了什么
    • [NSRunLoop currentRunLoop]实则为一个懒加载的方法。它会遍历一张全局静态的数据表,该数据表以线程PID为Key,以与该线程绑定的RunLoopValue。该表创建的时候会首先对当前线程(主线程)的PID放入一个RunLoop
  • RunLoopautorelease pool的关系
    • 对于每一个Runloop, 系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个象CallStack一样的一个栈式结构,对象会自动被放入栈顶的AutoreleasePool中,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release
    • 两次pop两次push,均利用Observer实现
      • 进入后push
      • 睡眠前pop
      • 睡眠后push
      • 离开前pop
  • GCDdispatch_get_main()是如何实现的
    • 当调用dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch会向主线程的RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个block,并在回调\__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__里执行这个block。但这个逻辑仅限于 利用GCDblock分发到主线程,分发到其他线程仍然是由libDispatch处理的。
    • GCD有自己的线程池,当需要使用到线程的时候随机找一个线程来跑,但是主线程是唯一的,使用RunLoop的主线程
  • 如何切换Mode?为什么要这样做?
    • 先离开,重新进入后切换Mode
    • 这样是为了保证Mode里面的TimerSourcesObserver互不影响
    • 延伸:在主线程Mode切换的时候,RunLoop这一次离开与下一次进入之前有一段间隔,这段间隔会对我们的应用有影响吗(比如会丢事件吗)?
      • 不会有影响,因为我们会把在这期间收到的事件都放在一个队列中,等待下一次RunLoop进入的时候,RunLoop根据该队列进行处理
  • 使用Timer要注意什么
    • 注意使用内存管理[timer invalidate];及设nil
    • 使用addCommonMode / addUITrackingMode保证精准度
  • CommonModes本质是什么
    • CommonModes是一个标识,CFRunLoopAddCommonMode等于给某个Mode打标识。
    • 这里有个概念叫CommonModes:一个Mode可以将自己标记为Common属性(通过将其 ModeName添加到 RunLoopcommonModes 中)。每当RunLoop 的内容发生变化时,RunLoop都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有Common标记的所有Mode里。
  • NSThread在没有RunLoop的情况下,执行完入口函数,会被立刻关闭吗?
    • 不会立刻关闭,会在执行完后,过段时间被清理
    • 延伸:既然如此,为什么把主线程的RunLoop关闭后,应用会崩溃?
      • 应用保证了主线程一定要有RunLoop,没有RunLoop则崩,与上面问题没有关系
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容