【iOS开发】RunLoop内部实现和使用

关于iOS开发中RunLoop的知识,归纳起来还是蛮多的。这一次花大力气,系统地总结下,也算是给自己复习功课。

本篇文章主要介绍以下知识点:

1.RunLoop的基本概念。

2.获取RunLoop的源码。

3.RunLoop的底层实现。

4.SrollView滑动,NSTimer失效分析。

一、RunLoop的基本概念:

1、什么是RunLoop

RunLoop,即运行循环。官方给的定义是:“RunLoop是与线程相关的基本基础结构的一部分。RunLoop是一个事件处理循环,用于调度工作和协调接收传入事件。RunLoop的目的是在有工作要做时,让线程保持忙碌;在没有工作时,让线程进入睡眠状态”。

具体原文在以下链接。

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1

从上面这段话可看出,RunLoop有三个方面的含义:

1).说明了RunLoop与线程的关系-----RunLoop与线程相关。

2).本质上说,RunLoop是一个事件循环,能够处理输入的事件。

3).RunLoop的作用就是保证线程一直运行着。

2、RunLoop和线程之间的关系

通过前面的概念,我们知道:

1).RunLoop与线程相关,和线程是一一对应的。

2).主线程的RunLoop由系统创建并默认启动。其实研究过App启动源码就知道:在Main.m的UIApplicationMain()函数内部,系统帮忙做了三件事情中,其中一件就是创建RunLoop并启动。

3).对于子线程的RunLoop必须手动创建,且是在第一次获取时创建,在线程结束时被销毁。

这里有个面试题。

App的主线程为什么一直运行?

答案就是在刚才说到的RunLoop与线程关系的第二点。

二、RunLoop的源码获取

在我们开发中,使用RunLoop的类是NSRunLoop,NSRunLoop本身是基于 CFRunLoopRef 的二次封装,而CFRunLoopRef 是属于 CoreFoundation 框架内的,这个框架主要提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

CoreFoundation 框架的下载地址是https://opensource.apple.com/tarballs/CF/   可以下载最新的源码。

在源码中,RunLoop模块的代码主要在CFRunLoop.h/c文件内。

三、RunLoop的底层实现

在了解RunLoop的底层实现前,需要了解两个非常重要的概念:事件源(Sources)和模式(Modes)。

事件源

事件源(Sources)指的是事件触发的源头,可以理解为触发事件的方式,苹果官方文档将其分为两种:输入源(Input Sources)和定时器源(Timer Sources)。其中输入源表示触发异步事件的源,例如一个线程和另一个线程之间的消息;定时器源表示触发同步事件的源,例如预定时间内触发的事件或重复触发的事件,最典型的例子就是NSTimer。

下图为RunLoop的结构图

简单说明下,上图概要地展示了RunLoop的事件运行机制:一个线程在循环运行并等待事件源的到来,当事件源到来时,处理对应的事件。

上图中左边 为线程在RunLoop上运行的过程,其中runUntilDate表示执行一段时间后结束,timerFired表示处理定时器事件,mySelector处理perform事件,handlePort处理Port事件,start表示创建并开启一个RunLoop,end表示RunLoop停止。

上图中右边为事件源,其中输入源有三种类型,第一种是通过端口(Port)触发,第二种是用户(Custom)自定义事件触发,第三种是通过performSelector:onThread触发。定时器源只有一种,就是Timer。

对应到源码就是如下

从__CFRunLoopSource的结构体中可以看到有一个联合体union,里面有version0和version1两个成员变量,这两个变量对应了Source0和Source1,表示事件源有两种类型。

联合体的作用是共享存储空间,也就是说,version0和version1两个变量(即Source0和Source1)共享一段存储空间,也就是说一个__CFRunLoopSource结构体要么为Source0事件源,要么为Source1事件源。

而对于Source0和Source1的区别如下:

1)Source0对应的是手动触发的事件,就是用户自定义(Custom)和performSelector:onThread两类事件源。

2)Source1为基于端口触发的事件,主要为端口(Port)的事件源。

模式

模式(Mode)指的是一个包括输入源(Inputsource)、定时器(Timer)、观察者(Observer)的模型对象。一个RunLoop在运行的时候,需要配置一个模式来告诉RunLoop需要执行哪些事件。简单点来说,模式就是用来存储RunLoop需要响应的事件,这些事件包括许多输入源、定时器和观察者。具体格式如下图

再到源码中看下模式的定义:

从上图可以看出:   

_sources0和_sources1是使用Set集合存储,对应的是事件源;

_observers和_timers 是用数组存储,分别对应观察者和定时器。

模式类型

根据网络资料,模式一般定义以下5种类型。

1.Default

2.Connection

3.Modal

4.Event tracking

5.Common modes

以下各个模式类型的举例和说明

讲完了两个概念,讲讲RunLoop的运行情况。

当一个RunLoop运行时,该RunLoop会执行当前模式中的事件源、定时器和通知观察者。简单来说,一个RunLoop,执行模式中三种类型的事件。

而对于当前模式的定义。苹果提供了三种方式:

1.默认模式,即上图中的NSDefaultRunLoopMode,在用户没有输入其他模式模式下,会执行默认模式中的事件源、定时器和通知观察者。

2.公共模式,即上图中的NSRunLoopCommonMode,在我们实际开发中,使用该模式解决了ScrollView滑动,NSTimer就失效的问题?

3.UI跟踪模式,即NSEventTrackingRunLoopMode,这个模式在实际中用得少,就不说了。

下面分别介绍下模型对象

观察者

观察者为检测RunLoop状态的对象,当RunLoop状态发生变化时会通知观察者。

那RunLoop又有哪些状态呢?查看源码得知,有7个状态。每一个状态发生变化都会通知观察者。具体状态如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),  //表示刚进入runloop
    kCFRunLoopBeforeTimers = (1UL << 1), //表示将要处理timer。
    kCFRunLoopBeforeSources = (1UL << 2),//表示将要处理Source。
    kCFRunLoopBeforeWaiting = (1UL << 5),//表示将要进入休眠状态。
    kCFRunLoopAfterWaiting = (1UL << 6),//表示将要从休眠状态进入唤醒状态。
    kCFRunLoopExit = (1UL << 7),  //表示退出状态。
    kCFRunLoopAllActivities = 0x0FFFFFFFU //表示所有的状态。
};

也就是说当上面7个状态任意一个发生改变,都会通知RunLoop中的观察者对象。有人就会问了,这个过程是怎么实现的呢?这个就是观察者者运行机制了,

观察者者运行机制

观察者在底层最终实现的是CFRunLoopObserverRef 对象。该对象将要观察的活动注册到_activities中,而_activities用不同的标识位来表示不同的活动,因此,活动的注册其实是通过操作某些位置实现的,类似于位操作。当一个RunLoop的状态发生变化时,通过回调函数_callout来,通知所有监听这个状态的观察者,这样就实现了RunLoop状态的监听和上报。

下面再介绍下定时器。

定时器

Timer在源码下如下定义:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval; /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR; /* TSR units */
    CFIndex _order; /* immutable */
    CFRunLoopTimerCallBack _callout; /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

Timer在底层代码中就是一个CFRunLoopTimer对象, 这个对象的运行,本质来说是由XNU 内核的 mk_timer来驱动的,在通过_callout回调实现定时执行任务

简单来说,一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

4. ScrollView滑动,NSTimer就失效的问题分析

先解释下mode的新增和删除方式。对于mode来说,只能通过mode name来操作内部的 mode,当传入一个新的 mode name 但RunLoop 内部没有对应mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

在我们ScrollView开发过程中, 在主线程一般在kCFRunLoopDefaultMode下,当ScrollView滑动时,会进入NSEventTrackingRunLoopMode,如果有一个 timer 加入到了 defaultMode 下,那么在滑动时 timer 就会失效,要想在滑动时还能继续使用 timer,可以将 timer 分别加入到这两个 mode,还有一种方式,就是加入到kCFRunLoopCommonModes组中,组中包含的三种 mode 都会同步加入此 timer。

以上就是RunLoop的相关技术介绍,感谢阅读。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员华仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值