iOS系统提供了两个对象:NSRunLoop 和 CFRunLoopRef。NSRunLoop 是基于 CFRunLoopRef 的封装。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。这两个函数内部的逻辑大概是下面这样:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
从上面的代码可以看出,线程和RunLoop之间是一一对应的,其关系保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop的创建发生在第一次获取时,RunLoop的销毁发生在线程结束时。
线程获取RunLoop后还需要传入一种运行模式(Mode),让其跑起来。
系统默认注册了5种Mode:
- kCFRunLoopDefaultMode,默认模式。
- UITrackingRunLoopDefaultMode:界面跟踪Mode,用于scrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
- UIInitializationRunLoopMode:在刚启动App时的第一个Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode:接受系统内部事件的Mode,通常用不到。
- NSRunLoopCommonMode:这是一个占位用的Mode,不是一种真正的Mode。
RunLoop与Mode的关系如下:
struct __CFRunLoop { CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; ... };
一个RunLoop可以有多个Mode,每次启动RunLoop都需要指定一种Mode,这个Mode被称为CurrentMode。如果需要切换Mode,只能退出Loop,然后再重新指定一个Mode进入。这样做的目的就是为了分隔开不同组的Source/Observer/Timer,让其互不影响。
举个例子:我们有时候会使用定时器NSTimer:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
定时器启动后,每隔2秒执行一次doSomething方法。但是,如果屏幕上有个UITableView,当我们滑动UITableView时,定时器失效。这是因为上述代码等价于:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(doSomething) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
可以看出,定时器默认添加到NSDefaultRunLoopMode中。而当我们滑动UITableView时,RunLoop会切换UITrackingRunLoopDefaultMode,因此定时器失效。如何让定时器在滑动UITableView时也生效呢?
一种方法是将定时器添加到两种Mode中。另一种方法是将定时器添加到NSRunLoopCommonModes中,因为当前RunLoop的_commonModes默认包含NSDefaultRunLoopMode和UITrackingRunLoopDefaultMode。
参考资料: