iOS开发 - RunLoop理解

RunLoop概念

运行循环,一个 run loop 就是一个事件处理的循环,用来不停的调度工作以及处理事件

作用

  • 保持程序的持续运行
  • 监听处理App中的各种事件(触摸事件,定时器事件,selector事件)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  • 一次RunLoop循环负责绘制屏幕上所有的点

入口函数

int main(int argc, char * argv[]) {
   @autoreleasepool {
       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
   }}

     UIApplicationMain()此函数内部就启动了一个RunLoop,所以此函数一直没有返回,保持了程序的持续运行,这个默认启动的RunLoop是跟主线程相关的

RunLoop对象     

  • Foundation框架下 : NSRunLoop (基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的)
  • Core Foundation框架下 : CFRunLoopRef (纯 C 函数的 API,所有这些 API 都是线程安全的)

RunLoop与线程

  • 每条线程有唯一的一个与之对应的RunLoop对象
  • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要手动创建
  • RunLoop在第一次获取时创建,在线程结束时销毁
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];//主线程对应的RunLoop
     [NSRunLoop mainRunLoop];//当前线程对应的RunLoop
     currentRunloop.getCFRunLoop;//转化为CFRunLoop
     
     CFRunLoopGetMain();
     CFRunLoopGetCurrent();
     
     //开启一个子线程
     [[[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil] start];
     
     -(void)run {
     
        //创建子线程对应的RunLoop,currentRunLoop 懒加载的
        [NSRunLoop currentRunLoop];
     
     }

以下是苹果官方源码,通过分析源代码可以看出利用pthread作为全局字典中的key,并创建与之对应的RunLoop作为Value,RunLoop在我们获取的时候创建,不获取不创建,主线程的RunLoop在一开始就自动创建。线程与RunLoop是一一对应的关系

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}



//全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问 Dictionary 时的锁
static CFLock_t loopsLock = CFLockInit;
//获取一个 pthread 对应的 RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //如果传进来的线程等于 0
    if (pthread_equal(t, kNilPthreadT)) {
        //当前线程等于主线程
        t = pthread_main_thread_np();
    }
    //给操作加锁
    __CFLock(&loopsLock);
    //如果当前RunLoop为空,创建。
    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);
    }
    // 从字典中获取当前线程的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 如果当前线程的runloop不存在,那么就为该线程创建一个对应的runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把当前子线程和对应的runloop保存到字典中
        if (!loop) {
            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);
        }
    }
    return loop;
}

RunLoop相关类

 Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef (基于时间的触发器)
  • CFRunLoopObserverRef

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

1.CFRunLoopSourceRef :事件源(输入源)

     - Source0:非基于Port的 (用户主动触发的事件)

     - Source1:基于Port的 (系统内部的消息事件)

(Port是线程间通信的一种方式,如果两个线程之间想通信,可以通过Port来通信。)

2.CFRunLoopTimerRef

基于时间触发器,当其加入RunLoop时,RunLoop会注册对应的时间点,当时间点到,RunLoop会被唤醒执行里面的回调。

3.CFRunLoopObserverRef 

观察者,能够监听RunLoop的状态改变。

/* Run Loop Observer Activities */

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry = (1UL << 0),//即将进入Loop

kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer

kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source

kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠

kCFRunLoopAfterWaiting = (1UL << 6),//刚从休眠中唤醒

kCFRunLoopExit = (1UL << 7),//即将退出Loop

kCFRunLoopAllActivities = 0x0FFFFFFFU

};

4.CFRunLoopModeRef :RunLoop的运行模式

在RunLoop中有多个运行模式,但是RunLoop只能选择一种模式运行,Mode里面至少要有Timer或者Source

系统默认注册了5个Mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
  • UIInitializationRunLoopMode:在刚进入App时进入的第一个Mode,启动完成后就不在使用
  • GCEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
  • NSRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode

RunLoop相关问题及解释 

1.Timer与滑动控件的问题

问题:当在不停地拖动滑动控件的时候,定时器不工作了

错误回答:runloop的优先级

分析:runloop的几种常用模式:DefaultMode默认模式,以及UITrackingMode模式,CommonModes占位模式,runloop进入一种模式的时候,另一种模式的事件不会去处理,当Timer在运行的时候处于DefaultMode模式,当拖动滑动控件的时候,runloop会立即处理UI事件,切换到UITrackingRunLoopMode模式后,此时DefaultMode模式下的Timer就不工作了

解决:将Timer置于NSRunLoopCommonModes占位模式下

- (void)viewDidLoad {

    [super viewDidLoad];

    //1.创建定时器

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    
    //2.将Timer添加到RunLoop中

    //[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

    //[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

}

- (void)run {

    NSLog(@"-------- %@",[NSThread currentThread]);

}
View Code

问题:在多线程开发中,耗时操作我们一般会放在子线程中执行,请问这种线程有什么特点?

实例:假如在上面的定时器的run方法中,执行一个耗时操作,此时会卡住主线程,拖动滑动控件会很不流畅,应该如何解决?

分析:子线程中默认不会开启RunLoop循环,所以子线程在执行完任务之后就会被回收

- (void)viewDidLoad {

    [super viewDidLoad];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        //1.创建定时器

        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

        //2.将Timer添加到RunLoop中

        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];        

        //3.让RunLoop运行起来

        [[NSRunLoop currentRunLoop] run];//死循环,后面的代码不会执行

        NSLog(@"+++++++++");

    });

}

- (void)run {

    //耗时操作

    [NSThread sleepForTimeInterval:1.0];

    NSLog(@"-------- %@",[NSThread currentThread]);

}
View Code

或者:

- (void)viewDidLoad {

    [super viewDidLoad];

   [NSThread detachNewThreadSelector:@selector(time2) toTarget:self withObject:nil];

}

-(void)time2{

    //创建当前线程的RunLoop

    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];

    //该方法内部自动添加到RunLoop中,并且运行模式是默认模式

    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //开启RunLoop

    [currentLoop run];

}

- (void)run {

    //耗时操作

    [NSThread sleepForTimeInterval:1.0];

    NSLog(@"run ----- %@ ---- %@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);

}
View Code

此博文会继续不断完善及更新关于涉及到runloop的知识,如果有理解不正确或者有涉及到runloop的应用实例,请留言吧,欢迎讨论

参考文章:http://blog.ibireme.com/2015/05/18/runloop/

相关推荐