首页 > 代码库 > iOS核心笔记——RunLoop-基础

iOS核心笔记——RunLoop-基础

1、RunLoop介绍:

?了解:RunLoop从字面意思看就是运行循环跑圈,通常情况下,一个线程一次只能执行一个任务;任务执行完毕后线程就会进入消亡状态随之退出。有时候我们希望线程执行完任务之后还能随时处理事件且不退出,所以,iOS提供了RunLoop。


1-1、什么是RunLoop?

?重要:RunLoop实际上就是一个对象,RunLoop对象管理其需要处理的事件和消息;RunLoop能够让线程在没有处理消息时进入休眠状态以避免占用资源、在监听到事件源发送的消息时立刻唤醒线程。RunLoop提供了一个入口函数来实现运行循环,当线程执行该函数后,便会处于接收消息->等待处理->处理的循环中,直到运行循环结束、RunLoop退出;RunLoop对象也随之销毁。


RunLoop循环逻辑:技术分享

技术分享

?重要:在iOS程序中,mian函数中的UIApplicationMain()函数内部启动了一个与主线程相关联的RunLoop;所以,UIApplicationMain()函数一直没有返回;保持程序持续运行。


RunLoop实现原理:技术分享


1-2、RunLoop对象:

?了解:苹果不允许我们直接通过alloc、init方式创建线程中的RunLoop对象,只有在第一次访问线程中的RunLoop对象时,RunLoop对象将以懒加载的形式创建,所以,iOS中提供了2套API来访问和使用RunLoop。


1、Foundation框架:
  1. NSRunLoop:在Foundation框架中,一个NSRunLoop对象就代表了一个线程中的运行循环。
  2. 获取方式
类型方式备注
当前线程 通过NSRunLoop的currentRunLoop类方法便能获取到当前线程中的RunLoop对象 当前RunLoop对象可以是主线程中的RunLoop对象,也可以是子线程中的RunLoop对象,如果当前为子线程,则懒加载RunLoop对象之后需要手动调用run方法开启运行循环
主线程 通过NSRunLoop的mainRunLoop类方法能够获取到主线程中的RunLoop对象 当当前线程为主线程时,mainRunLoop与currentRunLoop获取到的是同一个对象

2、Core Foundation框架:
  1. CFRunLoopRef:Core Foundation框架中,一个CFRunLoopRef对象就代表线程中的运行循环。
  2. 获取方式
类型方式备注
主线程 CFRunLoopGetMain()函数 获取主线程中的RunLoop对象,程序运行时,自动创建并开启
当前线程 CFRunLoopGetCurrent()函数 获取当前线程中的RunLoop对象,如果当前为子线程,则懒加载RunLoop对象之后需要手动调用run方法开启运行循环

1-3、RunLoop与线程:
1.    /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef  
2. static CFMutableDictionaryRef loopsDic;
3. /// 访问 loopsDic 时的锁
4. static CFSpinLock_t loopsLock;
5.
6. /// 获取一个 pthread 对应的 RunLoop。
7. CFRunLoopRef_CFRunLoopGet(pthread_t thread){
8. OSSpinLock Lock(&loopsLock);
9.
10. if(!loopsDic){
11. // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
12. loopsDic = CFDictionaryCreateMutable();
13. CFRunLoopRef mainLoop = _CFRunLoopCreate();
14. CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
15. }
16.
17. /// 直接从 Dictionary 里获取。
18. CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread);
19.
20. if(!loop){
21. /// 取不到时,创建一个
22. loop = _CFRunLoopCreate();
23. CFDictionarySetValue(loopsDic, thread, loop);
24. /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
25. _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
26. }
27.
28. OSSpinLockUnLock(&loopsLock);
29. returnloop;
30. }
31.
32. CFRunLoopRef CFRunLoopGetMain(){
33. return _CFRunLoopGet(pthread_main_thread_np());
34. }
35.
36. CFRunLoopRef CFRunLoopGetCurrent(){
37. return _CFRunLoopGet(pthread_self());
38. }

?重要: CFRunLoop是基于pthread来管理的,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。只能在一个线程的内部获取其 RunLoop(主线程除外)。


1-4、主线程与子线程:

技术分享

?重要:在子线程中使用RunLoop时,最好在子线程中的任务中包一个自动释放池(即:在子线程中的任务中手动添加@autoreleasepool,将任务包裹起来,避免内存泄露)。


1-5、RunLoop注意事项:

?了解:1、每条线程都有唯一一个与之对应的RunLoop对象;

?了解:2、主线程的RunLoop对象在程序启动时已经自动创建完毕,并自动开启运行循环;子线程的RunLoop对象需要主动访问其RunLoop对象以懒加载的形式创建,并且,子线程的RunLoop对象需要调用start方法手动开启;

?了解:3、RunLoop在第一次获取时以懒加载的形式创建,在线程结束时销毁。

?了解:4、RunLoop对象必须依赖线程,即:有线程才有RunLoop对象,没有线程就没有RunLoop对象;不能有RunLoop对象而没有线程。


1-6、RunLoop学习:
1、苹果官方文档:

RunLoop官方文档介绍

2、CFRunLoopRef源码:

CFRunLoop源码


2、RunLoop对外接口:

?重要:在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef :RunLoop对象
CFRunLoopModeRef :RunLoop模式
CFRunLoopSourceRef :RunLoop事件源
CFRunLoopTimerRef :RunLoop定时器
CFRunLoopObserverRef:RunLoop观察者

其中,CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。它们之间的关系如下图所示:

技术分享


2-1、CFRunLoopRef:

?重要:1、一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

?重要:2、RunLoop中的Source/Timer/Observer被统称为mode item,一个 item 可以被同时加入多个mode。但一个 item被重复加入同一个mode时是不会有效果的。如果一个 mode 中一个item都没有,则RunLoop会直接退出,不进入循环。

?了解:3、


2-2、CFRunLoopSourceRef:

?重要:CFRunLoopSourceRef是事件产生的地方,Source有两个版本:Source0 和 Source1。

· Source0 :非基于Post,只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

· Source1 :基于Port,包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

?了解:以前CFRunLoopSourceRef可分为:①Port-Based Sources;②Custom Input Sources;③Cocoa Perform Selector Sources


基于事件源的函数调用栈

技术分享


2-3、CFRunLoopTimerRef:

?重要:CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer是toll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。


2-4、CFRunLoopObserverRef:

?重要:CFRunLoopObserverRef是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

1.typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
2. kCFRunLoopEntry = (1UL << 0), // 1 --- 即将进入RunLoop
3. kCFRunLoopBeforeTimers = (1UL << 1), // 2 --- 即将处理定时器
4. kCFRunLoopBeforeSources = (1UL << 2), // 4 --- 即将处理Source
5. kCFRunLoopBeforeWaiting = (1UL << 5), // 32 --- 即将进入休眠状态
6. kCFRunLoopAfterWaiting = (1UL << 6), // 64 --- 唤醒RunLoop
7. kCFRunLoopExit = (1UL << 7), // 128 --- 即将退出RunLoop
8. kCFRunLoopAllActivities = 0x0FFFFFFFU // RunLoop所有状态
9.};

?重要:给RunLoop对象设置观察者:

步骤:①创建观察者对象;②将观察者对象设置为指定RunLoop对象的观察者。

技术分享


3、CFRunLoopModeRef:

?重要:CFRunLoopModeRef代表RunLoop的运行模式,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。


3-1、CFRunLoopMode和CFRunLoop的结构大致如下:

CFRunLoopMode结构

1.struct__CFRunLoopMode{  
2. CFStringRef_name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
3. CFMutableSetRef_sources0; // Set<CFRunLoopSourceRef>
4. CFMutableSetRef_sources1; // Set<CFRunLoopSourceRef>
5. CFMutableArrayRef_observers;// Array<CFRunLoopObserverRef>
6. CFMutableArrayRef_timers; // Array<CFRunLoopTimerRef>
7. ...
8.};

CFRunLoop结构

1.struct__CFRunLoop{  
2. CFMutableSetRef_commonModes; // Set<CFStringRef>
3. CFMutableSetRef_commonModeItems; // Set<Source/Observer/Timer>
4. CFRunLoopModeRef_currentMode; // Current Runloop Mode
5. CFMutableSetRef_modes; // Set<CFRunLoopModeRef>
6. ...
7.};

3-2、RunLoop5种模式:

技术分享

技术分享


?重要: “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

?了解:应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

1.  CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);  
2. CFRunLoopRunInMode(CFStringRef modeName, ...);
3.

Mode 暴露的管理 mode item 的接口有下面几个:

1.  CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);  
2.
3. CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
4.
5. CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
6.
7. CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
8.
9. CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
10.
11. CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

?重要:只能通过mode name来操作内部的mode,当传入一个新的mode name时,但是,RunLoop内部没有对应mode时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,可以用这两个Mode Name来操作其对应的Mode。同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。


3-3、RunLoop模式详细介绍:

技术分享


iOS核心笔记——RunLoop-基础