NSRunLoop

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:

  1. kCFRunLoopDefaultMode,默认模式。
  2. UITrackingRunLoopDefaultMode:界面跟踪Mode,用于scrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
  3. UIInitializationRunLoopMode:在刚启动App时的第一个Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode:接受系统内部事件的Mode,通常用不到。
  5. 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。

 

参考资料:  

  RunLoop

posted @ 2016-11-20 18:23  Sawyer Ford  阅读(442)  评论(0编辑  收藏  举报