iOS多线程之RunLoop概念及使用技巧
iOS多线程之RunLoop概念及使用技巧。
RunLoop 基本概念
前面几篇文章详细讲解了创建多线程的方法和多线程编程的相关知识,当我们使用NSThread进行多线程编程时,只要任务结束,线程也就退出了,每次执行一个任务都需要创建一个线程非常浪费资源,所以需要一种能够使线程常驻内存不退出d,当有任务来临时能随时执行的方法,这就是
RunLoop的作用。类似于
javascript的
Event Loop模型,大致类似于如下代码:
int retVal = Running; do { // 执行各种任务,处理各种事件 // ...... } while (retVal != Stop && retVal != Timeout);
上述循环只有在特定条件才才会退出,否则就会一直在循环中处理各种任务或事件,诸如触摸屏幕事件、手势事件、定时器事件、用户提交的任务、各种方法的执行等。
RunLoop与线程关联的,是一种事件处理环,用来安排和协调到来的事件,目的就是让其关联的线程在有事件到达时时刻保持运行状态,而当没有事件需要处理时进入睡眠状态从而节约资源,每一个线程都可以有一个
RunLoop对象与之对应,并且是在第一次获取它是系统自动创建的,比如主线程关联的
RunLoop,我们都知道程序的入口函数是
main函数,下面是创建工程后
Xcode自动生成的
main.m文件的
main函数代码:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
该方法执行体被
autoreleasepool包围,所以程序可以使用
ARC来管理内存,后面会讲解
RunLoop与
autoreleasepool的关系,
main函数直接返回了
UIApplicationMain函数,该函数内部就会第一次获取
RunLoop对象,所以系统就会创建这样一个
RunLoop对象,因此在没有满足特定条件的时候该主线程不会退出,应用就可以持续运行而不会退出。
从上图可以看出一个线程会关联一个
RunLoop对象,
RunLoop对象会一直循环,直到超时或收到退出指令。在无限循环的过程中会一直处理到来的事件,右侧将事件分为了两类,一类是
Input sources这部分包括基于端口的
source1事件,开发者提交的各种
source0事件,调用
performSelector:onThread:方法事件,还有一类
Timer sources这个就是常用的定时器事件,这些事件在程序运行期间会不断产生之后会由
RunLoop对象检测并负责处理相关事件。
RunLoop 源码解析
RunLoop 源码解析
RunLoop有两个对象,
NSRunLoop和
CFRunLoopRef,区别在于由
Core Foundation框架提供的
CFRunLoopRef是纯C语言编写的,提供的也是C语言接口,这些接口都是线程安全的,由
Foundation框架提供的
NSRunLoop是面向对象的,它是基于
CFRunLoopRef的封装,提供的都是面向对象的接口,但这些接口不是线程安全的,
Core Foudation框架是开源的,可以在这个地址下载:Core Foundation开源代码,本文接下来的内容主要是针对该开源代码进行讲解。
首先,看一下在代码中如何获取
RunLoop对象,在
Foundation框架中的
NSRunLoop类提供了如下两个类属性:
//获取当前线程关联的RunLoop对象 @property (class, readonly, strong) NSRunLoop *currentRunLoop; //获取主线程关联的RunLoop对象 @property (class, readonly, strong) NSRunLoop *mainRunLoop
对应的
Core Foundation框架中提供了如下两个函数来获取
RunLoop对象:
//获得当前线程关联的RunLoop对象 CFRunLoopGetCurrent(); // 获得主线程关联的RunLoop对象 CFRunLoopGetMain();
前面一直讲每一个线程都会关联一个
RunLoop对象,并且不能通过手动创建该对象,只能在第一次获取时系统自动创建,看一下
Core Foundation框架是如何实现的:
//CFRunLoopGetMain函数用于获取主线程关联的RunLoop对象 CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK(); //静态变量保存主线程关联的RunLoop对象 static CFRunLoopRef __main = NULL; // no retain needed //如果主线程关联的RunLoop对象为NULL就调用_CFRunLoopGet0函数获取一个 if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed return __main } //获取当前线程关联的RunLoop对象 CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); //这一段没找到对应的函数...猜测是和上面的函数用意一样 CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; //如果上面没找到就调用_CFRunLoopGet0函数去获取一个 return _CFRunLoopGet0(pthread_self()); } //全局的可变字典数据结构,key为thread_t即线程,value为RunLoop对象 static CFMutableDictionaryRef __CFRunLoops = NULL; //全局的一个锁 static CFLock_t loopsLock = CFLockInit; //_CFRunLoopGet0接收一个pthread_t对象,即线程对象,返回一个与之关联的RunLoop对象 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { //判断是否为主线程 if (pthread_equal(t, kNilPthreadT)) { //pthread_main_thread_np()函数用来获取主线程 t = pthread_main_thread_np(); } //加锁,防止产生竞争创建多个RunLoop对象 __CFLock(&loopsLock); //如果全局的保存线程和runloop对象的字典为空 if (!__CFRunLoops) { __CFUnlock(&loopsLock); //创建一个字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); /* 根据主线程创建RunLoop对象 所以,当第一次获取RunLoop对象时就会自动创建主线程关联的RunLoop对象 */ CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); //设置全局的字典,key为主线程,value为主线程关联的RunLoop对象 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } //通过线程在字典中获取RunLoop对象 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); //如果没有获取到 if (!loop) { //没有获取到就根据线程创建一个RunLoop对象 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); //再次获取 loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); if (!loop) { //字典中仍然没有线程关联的RunLoop对象就将刚才新创建加入到字典照中 CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { //设置销毁时的回调 _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } //返回线程关联的RunLoop对象 return loop; } /* 真正的用于创建RunLoop对象的静态函数,形参为线程对象 该函数主要用于分配存储空间,并进行RunLoop对象相关初始化操作 */ static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { CFRunLoopRef loop = NULL; CFRunLoopModeRef rlm; uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL); if (NULL == loop) { return NULL; } (void)__CFRunLoopPushPerRunData(loop); __CFRunLoopLockInit(&loop->_lock); loop->_wakeUpPort = __CFPortAllocate(); if (CFPORT_NULL == loop->_wakeUpPort) HALT; __CFRunLoopSetIgnoreWakeUps(loop); loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode); loop->_commonModeItems = NULL; loop->_currentMode = NULL; loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); loop->_blocks_head = NULL; loop->_blocks_tail = NULL; loop->_counterpart = NULL; loop->_pthread = t; #if DEPLOYMENT_TARGET_WINDOWS loop->_winthread = GetCurrentThreadId(); #else loop->_winthread = 0; #endif rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true); if (NULL != rlm) __CFRunLoopModeUnlock(rlm); return loop; }
通过上面源码不难发现,
RunLoop对象保存在一个全局的字典中,该字典以线程对象
pthread_t为
key,以
RunLoop对象为
value,并且,在第一次获取
RunLoop对象时总会先把主线程关联的
RunLoop对象创建好,在获取其他线程关联的
RunLoop对象时都从这个全局的字典中获取,如果没有获取到就创建一个并且添加进字典中,所以每一个线程有且仅有一个与之关联的
RunLoop对象,重要的是,如果不获取线程关联的
RunLoop对象,那么这个
RunLoop对象就不会被创建。当线程退出时,也会将
RunLoop对象销毁。
接下来查看一下
CFRunLoopRef具体的数据结构如下:
struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // used for CFRunLoopWakeUp Boolean _unused; volatile _per_run_data *_perRunData; // reset for runs of the run loop pthread_t _pthread; uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; }; typedef struct __CFRunLoop * CFRunLoopRef;
上述数据结构中比较重要的就是
_commonModes、
_commonModeItems、
_currentMode以及
_modes。
上图很好的描述了
struct __CFRunLoop数据结构相关成员变量的关系,每一个
__CFRunLoop对象可以包含数个不同的
Mode,而每一个
Mode又包含了数个
Source、
Observer和
Timer,当一个
RunLoop运行时只能选择其中的某一个
Mode来执行,如果要切换
Mode则需要退出运行后指定一个新的
Mode后重新执行运行。通过这样的方式,可以在不同
Mode中设置不同的
Source/Observer/Timer而不同的
Mode中间的这三部分互不影响,也就是说,有些
Source/Observer/Timer只能在某一个
Mode中运行,当
RunLoop运行在其他
Mode中,该事件得不到处理。
Source CFRunLoopSourceRef
Source CFRunLoopSourceRef
Source即
CFRunLoopSourceRef类的对象,指代事件源,即前文官方结构图中的
Input Source,在官方文档中该事件源
Source分为三类:
Port-Based Sources 基于端口的,也称为
source1事件,通过内核和其他线程通信,接收到事件后包装为
source0事件后分发给其他线程处理。
Custom Input Sources 用户自定义
Cocoa Perform Selector Sources 调用诸如
perfromSelector:onThread:这样的方法产生的事件
按照调用栈来说其实只分成两类,
Source0不基于端口的和
Source1基于端口的,分类方式并不是很重要,了解即可。
Timer CFRunLoopTimerRef
Timer CFRunLoopTimerRef
Timer可以理解为定时器即
NSTimer,因为
CFRunLoopTimerRef和
NSTimer是
toll-free bridged,所以可以互相转换,将其理解为
NSTimer即可,
RunLoop对象会在注册的定时器时间到达时唤醒关联的线程对象来执行定时器的回调。
Observer CFRunLoopObserverRef
Observer CFRunLoopObserverRef
Observer就是监听器,用来监听
RunLoop的各种状态,在源码中有如下监听状态的枚举定义:
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { //即将进入RunLoop的执行循环 kCFRunLoopEntry = (1UL << 0), //即将处理Timer事件 kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Source事件 kCFRunLoopBeforeSources = (1UL << 2), //RunLoop即将进入休眠状态 kCFRunLoopBeforeWaiting = (1UL << 5), //RunLoop即将被唤醒 kCFRunLoopAfterWaiting = (1UL << 6), //RunLoop即将退出 kCFRunLoopExit = (1UL << 7), //监听RunLoop的全部状态 kCFRunLoopAllActivities = 0x0FFFFFFFU };
Observer中定义了一系列的监听器,开发者也可以使用监听器来监听具体的状态改变,具体栗子后文会介绍。
Mode CFRunLoopModeRef
Mode CFRunLoopModeRef
Mode是
RunLoop中比较重要的部分,系统默认为我们提供了五种
Mode:
kCFRunLoopDefaultMode 即 NSDefaultRunLoopMode,默认运行模式
UITrackingRunLoopMode 跟踪UIScrollView滑动时使用的运行模式,保证滑动时不受其他事件处理的影响,保证丝滑
UIInitializationRunLoopMode 启动应用时的运行模式,应用启动完成后就不会再使用
GSEventReceiveRunLoopMode 事件接收运行模式
kCFRunLoopCommonModes 即 NSRunLoopCommonModes 是一种标记的模式,还需要上述四种模式的支持
UITrackingRunLoopMode只有当用户滑动屏幕时,即滑动
UIScrollView时才会执行的模式,此时,不在该模式内的
Source/Timer/Observer都不会得到执行,它仅仅专注于滑动时产生的各种事件,通过这样的方式就可以保证用户在滑动页面时的流畅性,这也是分不同
Mode的优点。
具体数据结构如下:
typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; Boolean _stopped; char _padding[3]; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; CFMutableDictionaryRef _portToV1SourceMap; __CFPortSet _portSet; CFIndex _observerMask; #if USE_DISPATCH_SOURCE_FOR_TIMERS dispatch_source_t _timerSource; dispatch_queue_t _queue; Boolean _timerFired; // set to true by the source when a timer has fired Boolean _dispatchTimerArmed; #endif #if USE_MK_TIMER_TOO mach_port_t _timerPort; Boolean _mkTimerArmed; #endif #if DEPLOYMENT_TARGET_WINDOWS DWORD _msgQMask; void (*_msgPump)(void); #endif uint64_t _timerSoftDeadline; /* TSR */ uint64_t _timerHardDeadline; /* TSR */ };
从上述数据结构中可以看出,
Mode内部管理了一个
_source0的事件集合,一个
_source1的事件集合,一个
_observers的数组以及
_timers的数组,这也印证了前文中关于
Mode的图例,再结合之前讲的
__CFRunLoop中比较重要的几个成员变量:
struct __CFRunLoop { ... CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; ... }
其中
_currentMode即代表当前
RunLoop对象正在执行的
Mode即
CFRunLoopModeRef类的对象。
_mode是一个
Set集合保存了所有该
RunLoop对象可以执行的
Mode。
_commonModes保存的是具有
Common属性的
Mode的名称,前文
__CFRunLoopMode的结构体定义中可以看到,每个
Mode管理自己的
Source/Timer/Observer,而被标记为
Common属性的
Mode还有一个特性就是当
RunLoop对象在执
Common属性的
Mode时,会自动将
_commonModeItems中保存的
Source/Observer/Timer同步添加该
Mode中,标识
Common属性只需要将
__CFRunLoopModeRef的
_name成员变量的值添加进
_commonModes集合中即可。被标记为
Common属性的
Mode就是前文讲的
kCFRunLoopCommonModes模式,可以看出这种模式不是一种真正的模式,仅仅是标识其他模式是否需要同步添加
_commonModeItems中的
Source/Timer/Observer。
_commonModeItems中保存的就是那些需要同步添加到具有
Common属性的
Mode中的
Source/Timer/Observer集合。
系统默认将
kCFRunLoopDefaultMode和
UITrackingRunLoopMode添加到了
_commonModes中,即标识为
Common属性,所以当
RunLoop运行在这两种模式中会自动同步添加
_commonModeItems中的
Source/Timer/Observer。
举个常见的栗子:
- (void) viewWillAppear:(BOOL)animate { [super viewWilAppear:YES]; //创建一个NSTimer的对象,从当前时间开始每1s输出一次Hello,World NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"Hello, World"); }]; //将timer加入到当前线程关联的RunLoop对象的NSDefaultRunLoopMode中 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; //类方法,创建一个timer并添加到当前线程关联的RunLoop的NSDefaultRunLoopMode中 [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"Hello, World222"); }]; }
上面的栗子创建了两个
NSTimer,这两个定时器执行效果相同,但如果页面中有一个
UIScrollView或其子类的对象在滑动时,
NSTimer就不会再有任何输出,当停下滑动时又会有输出,因为上述代码创建的两个
NSTimer都加入到了
RunLoop对象的
NSDefaultRunLoopMode中,在滑动时
RunLoop会切换到
UITrackingRunLoopMode模式下执行,而
UITrackingRunLoopMode中没有上述定时器,所以不会执行,当停止滑动时
RunLoop对象又切换到了
NSDefaultRunLoopMode模式,所以可以继续执行定时器的回调。
为了解决这个问题,可以将
NSTimer即加入到
NSDefaultRunLoopMode中,又加入到
UITrackingRunLoopMode中,同一个
Source/Timer/Observer可以添加到不同的
Mode中,但同一个
Source/Timer/Observer不能添加到同一个
Mode中,这样不会有任何效果,但添加到两个
Mode中并不是最好的解决方案,还有一个方案就是利用前面的
Common属性,
NSDefaultRunLoopMode和
UITrackingRunLoopMode都被添加进了
_commonModes集合中被标识了具有
Common属性,所以在运行时就会自动将
_commonModeItems中的
Source/Timer/Observer同步添加到其中,因此,只需要将创建的
NSTimer加入到
_commonModeItems中即可,此时只需要使用
NSRunLoopCommonModes即可,代码如下:
- (void) viewWillAppear:(BOOL)animate { [super viewWilAppear:YES]; NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"Hello, World"); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; }
将
NSTimer加入到
NSRunLoopCommonModes中就是把其加入到
_commonModeItems集合中,这样在滑动时就会自动同步添加
NSTimer到
UITrackingRunLoopMode模式下,所以定时器也可会得到执行。如果需要注意使用类方法
scheduledTimerWithTimeInterval:repeats:block时要注意该方法默认是加入到
NSDefaultRunLoopMode模式中的。
在查看
RunLoop运行机制前,做一个小实验,创建一个视图控制器,并添加一个按钮,在按钮点击事件的回调函数中打一个断点,然后运行程序点击按钮,之后查看调用栈如下图所示:
从上图中可以看到程序在18处执行
main函数,17执行
UIApplicationMain函数,这就是程序启动过程,16是系统内部事件,15调用
CFRunLoopRunSpecific后文会详细讲解该函数,14开始执行
RunLoop进入循环,13开始处理
source0这个
source0就是点击按钮的事件,11是真正执行
source0的函数,10-0就是点击事件的整个转发处理过程,最终交由我们自定义的回调方法进行处理。
RunLoop 执行逻辑
RunLoop 执行逻辑
在官方文档中描述的
RunLoop循环中的执行逻辑如下:
通知监听器RunLoop进入循环
通知监听器即将处理Timer事件
通知监听器即将处理source0(不是基于端口的)事件
执行source0事件
如果有source1(基于端口的)事件则立即执行跳转到第九步
通知监听器RunLoop即将进入休眠状态
将线程休眠,直到以下事件发生才会被唤醒:
有source1事件到达 定时器触发时间到达 RunLoop对象的超时时间过期 被外部显示唤醒
通知监听器RunLoop对象即将被唤醒
处理添加进来的事件,包括:
如果用户定义的定时器时间到达,执行定时器时间并重启循环,跳转到第二步 如果有source1事件,传递这个事件 如果RunLoop被显示唤醒并且没有超时则重启RunLoop,跳转到第二步
上一篇: iOS限制输入框的字符长度实现方法
下一篇: iOS开发中打包.a静态库教程