**** 一.概述 ****
一般来讲,开一个线程执行某项任务,在任务执行完成后线程就会退出。如果我们需要让线程能不退出一直常驻,随时处理事件这就需要消息循环来实现了。
Runloop 是 iOS中的消息循环,Android也有Looper/Handler一套消息循环机制,这到底是啥,以前玩过单片机的小伙伴肯定记得很清楚,我们在配置完硬件,对各个部件进行初始化完毕后,都会在main中执行一个死循环,然后等待硬件中断,软件中断,在对应的中断函数中处理外部事件,其实这个死循环就是我们今天要介绍的Runloop,它会保持整个应用进程处于存活状态,然后在每个消息循环中处理各个事件。Runloop是我们平常看不见摸不着,但是又比不可少的一个系统组件。
Runloop 的思想可以用下面的伪代码来表示:
int main(void) { 初始化(); while (不满足退出条件) { 休眠前处理待处理事件(message); 睡眠ZZzzzz... 等待被事件唤醒 message = 获取需要处理的事件(); 处理事件(message) } return 0; }
|
依赖NSRunloop的类和框架
NSTimer UIEvent autorelease NSObject(NSDelaydPerforming) NSObject(NSThreadPerformAddtion) CADisplayLink CATransition CAAnimation dispatch_get_main_queue()
|
**** 二.RunLoop 需要注意的点:****
CFRunLoopRef基于C线程安全,NSRunLoop基于CFRunLoopRef面向对象的API是不安全的.
RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中,
主线程的 Runloop 会在应用启动的时候启动,其他线程的 Runloop需要我们手动创建并启动,RunLoop在第一次获取时创建,在线程结束时销毁,我们只能在一个线程的内部获取其RunLoop,并且苹果系统不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:
CFRunLoopRef CFRunLoopGetCurrent(void) CFRunLoopRef CFRunLoopGetMain(void) +(NSRunLoop *)currentRunLoop +(NSRunLoop *)mainRunLoop
|
Runloop Mode是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer隔绝开。Runloop 在某个时刻只能跑在一个Mode下,处理这一个Mode 当中的 Source,Timer 和 Observer,这也是为啥在滑动屏幕的时候有时候定时器会失效的原因,这个会在后续介绍。
Source0是内部事件 UIEvent、CFSocket都是source0,它区别于source1,它不是基于Port的,它不能主动触发事件。我们需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop处理这个事件。
Source1由RunLoop和内核管理,source1带有mach_port_t和一个回调,可以接收内核消息并触发回调,它能主动唤醒 RunLoop 的线程。
source0 有公开的 API 可供开发者调用,source1 却只能供系统使用
Timmer 包含一个时间长度和一个回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
NSTimer的创建通常有两种方式,一种是以timer开头,另一种是以schedued开头,第一种是没有添加到Runloop中的,需要我们自己去手动调用代码添加,第二种会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中。
一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环
三. RunLoop 关键源码解析:
3.1 RunLoop 关键数据结构
struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; __CFPort _wakeUpPort; Boolean _unused; volatile _per_run_data *_perRunData; 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; };
|
每个__CFRunLoop 会存储有:
当前RunLoop所对应的线程****_pthread****
当前RunLoop所能运行的所有模式****_modes以及它的锁_lock****
当前RunLoop当前运行模式****_modes****
当前RunLoop当前运行模式****_currentMode****
当前RunLoop的公用模式信息****_commonModes,_commonModeItems****
当前RunLoop唤醒端口****_wakeUpPort****
添加到当前Runloop的Block信息****_blocks_head,_blocks_tail****
当前RunLoop相关的统计信息****_runTime,_sleepTime****
__CFRunLoopMode
struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; 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; 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; uint64_t _timerHardDeadline; };
|
__CFRunLoopMode 里面最关键的元素包括:****_sources0****,_sources1,_observers,_timers,_portSet
iOS 系统中定义了如下几种Mode:
- NSDefaultRunLoopMode:
NSDefaultRunLoopMode是Runloop的默认的运行模式,主线程在启动的时候就是运行在这个模式下的,NSTimer与NSURLConnection默认运行该模式下。
- UITrackingRunLoopMode:在触摸滑动界面的时候会切换到这个模式用于保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- NSRunLoopCommonModes:这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode
- __CFRunLoopSource
struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order; CFMutableBagRef _runLoops; union { CFRunLoopSourceContext version0; CFRunLoopSourceContext1 version1; } _context; };
|
Source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,标记信息位于****_bits****
struct __CFRunLoopObserver { CFRuntimeBase _base; pthread_mutex_t _lock; CFRunLoopRef _runLoop; CFIndex _rlCount; CFOptionFlags _activities; CFIndex _order; CFRunLoopObserverCallBack _callout; CFRunLoopObserverContext _context; };
|
__CFRunLoopObserver 里面最关键的字段是****_callout****,它会在通知发出的时候被回调。
struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; pthread_mutex_t _lock; CFRunLoopRef _runLoop; CFMutableSetRef _rlModes; CFAbsoluteTime _nextFireDate; CFTimeInterval _interval; CFTimeInterval _tolerance; uint64_t _fireTSR; CFIndex _order; CFRunLoopTimerCallBack _callout; CFRunLoopTimerContext _context; };
|
__CFRunLoopTimer 里面主要包含了触发的时机,以及对应的回调方法。
在针对RunLoop源码进行解析的时候大家可以针对下面这张图进行理解。下面的解析也是围绕者这张图来分析的:
3.2 RunLoop 起点
前面提到除了主线程外,其他我们自己创建的线程是没有RunLoop的,我们可以通过currentRunLoop来创建,而主线程的RunLoop是通过mainRunLoop创建的,最终会调用CFRunLoopRef 下的CFRunLoopGetCurrent,CFRunLoopGetMain:
CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK(); static CFRunLoopRef __main = NULL; if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); return __main; }
CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; return _CFRunLoopGet0(pthread_self()); }
|
上面的这两个方法都会归到一处入口,****_CFRunLoopGet0****传入的是当前所在的线程:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (!__CFRunLoops) { __CFUnlock(&loopsLock); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); if (!loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } __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); } } return loop; }
|
只要是第一次调用****_CFRunLoopGet0,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建,创建出来的RunLoop会被添加到__CFRunLoops这个全局字典里面。这个字典以线程指针作为key,Runloop作为value进行存储的,后续创建的一系列的Runloop都会添加到这里。每次调用_CFRunLoopGet会先从这里看下是否有已经创建好的RunLoop如果有就直接返回,如果没有则新建一个,然后存到__CFRunLoops****作为缓存。在最后的时候会判断如果传入线程就是当前线程,则会注册一个回调,当线程销毁时,销毁对应的RunLoop。
3.3 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; }
|
这里没啥亮点。我们继续看下一步:
3.4 启动RunLoop
在什么都没有指定的时候,CFRunLoopRun默认是跑在kCFRunLoopDefaultMode模式下的
void CFRunLoopRun(void) { int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }
|
后续的各种run最终的入口都是归结到CFRunLoopRunSpecific:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { Boolean did = false; if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; }
|
在CFRunLoopRunSpecific中会通过传入的modeName,找到对应的CFRunLoopModeRef,并将其传入****__CFRunLoopRun作为当前RunLoop要跑的模式。进入和退出__CFRunLoopRun会发出kCFRunLoopEntry,和kCFRunLoopExit****通知外部的Observer Runloop将要开始,以及Runloop已经退出。
__CFRunLoopRun方法有点长,我省略了一些不必要的代码:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { do {
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(rl, rlm); Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl);
__CFRunLoopSetIgnoreWakeUps(rl);
__CFRunLoopUnsetSleeping(rl);
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg:; __CFRunLoopSetIgnoreWakeUps(rl);
if (MACH_PORT_NULL == livePort) { CFRUNLOOP_WAKEUP_FOR_NOTHING(); } else if (livePort == rl->_wakeUpPort) { CFRUNLOOP_WAKEUP_FOR_WAKEUP(); } else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { __CFArmNextTimerInMode(rlm, rl); } } else if (livePort == dispatchPort) { CFRUNLOOP_WAKEUP_FOR_DISPATCH(); __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { CFRUNLOOP_WAKEUP_FOR_SOURCE(); } } while (0 == retVal); return retVal; }
|
大体流程大家可以结合代码注释看下图中所示。
- 通知将要处理Timmer事件
- 通知将要处理Source事件
- 处理Source0事件,在处理Source0事件之前都会调用****__CFRunLoopDoBlocks****处理下block。
- 通知将要进入睡眠状态
- Runloop进入睡眠状态
- 当有事件源给Runloop发送消息,表示有事件处理的时候,就会唤醒Runloop。
- 唤醒Runloop。
- 通知Runloop已经唤醒。
- 根据事件源来调用不同的方法来执行处理,这里的事件源可以是Timmer,Source1,GCD事件。
- 继续上面循环。
四 RunLoop 应用相关
当调用了dispatch_async(dispatch_get_main_queue())时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。需要注意的是dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。
我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中
- NSTimer, GCD Timer,CADisplayLink
NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。而GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。CADisplayLink是一个执行频率和屏幕刷新相同的定时器,它也需要加入到RunLoop才能执行,通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅。
子线程启动定时器需要按照如下方式启动Runloop
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] run]; });
|
一般而言
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
|
等效于
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
|
当我们滚动ScrollView或者滑动屏幕的时候,RunLoop会切换到UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式的RunLoop,所以导致defaultMode下的定时器失效,所以这种情况的解决方式就是将定时器放到commonMode中,这样即使切换到UITrackingRunLoopMode也会被触发。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
或者使用GCD定时器,它不会受RunLoop的影响:
// 获得队列 dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象) self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次) // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒) // 比当前时间晚1秒开始执行 dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)); //每隔一秒执行一次 uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC); dispatch_source_set_timer(self.timer, start, interval, 0); // 设置回调 dispatch_source_set_event_handler(self.timer, ^{ }); // 启动定时器 dispatch_resume(self.timer);
|
当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,
在我们修改了View frame、或者调整了UI层级,或者手动设置了setNeedsDisplay/setNeedsLayout之后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去,_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 这个Observer会监听RunLoop即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。facebook推出的AsyncDisplayKit的机制和它也类似,它将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程,在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务更新界面。
我们有时候需要创建一个在后台一直存在的程序,来做一些需要频繁处理的任务
- (void)run { @autoreleasepool{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; } }
|
当需要这个后台线程执行任务时,通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
AFNetworking 就是通过这种方式接收 Delegate 回调
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }
+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; }
|
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { }); CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); CFRelease(observer);
|