iOS开发进阶:RunLoop相关分析总结

什么是Runloop?
Runloop是通过内部维护的事件循环来对事件和消息进行管理的一种机制。当没有消息需要处理的时候,线程进入休眠以避免占用资源,有消息需要处理时,立即被唤醒。

runloop循环不是单独的do-while循环,而是发生一个用户态到内核态切换,以及内核态到用户态切换。它维护的事件循环可以用来不断的处理消息和事件,当没有消息和事件需要处理时会从用户态切换到内核态,由此可以用来休眠线程,避免资源占用。当有消息需要处理时会从内核态切换到用户态,当前线程会被唤醒,所以状态切换才是runloop的关键。

iOS中提供了两套Runloop接口,一个是NSRunLoop基于Objective-C,在Foundation框架中,另一个是CFRunLoopRef基于C,在CoreFoundation中。而NSRunLoop是对CFRunLoopRef的封装,两者接口基本都是对应的。CFRunLoopRef runloop = [nsrunloop getCFRunLoop]可以获取对应的CFRunLoopRef。通过一个表格来对比一下,后面我们将通过对CFRunloop的解读来更深刻的理解Runloop

特征 NSRunLoop CFRunLoopRef
所属框架 Objective-C/Foundation C/CoreFoundation
获取Runloop [NSRunLoop currentRunLoop]
[NSRunLoop mainRunLoop]
CFRunLoopGetCurrent()
CFRunLoopGetMain()
Source事件 addPort:forMode:
removePort:forMode:
CFRunLoopAddSource(...)
CFRunLoopRemoveSource(...)
Timer事件 addTimer:forMode: CFRunLoopAddTimer(...)
Observer事件 CFRunLoopAddObserver(...)
CFRunLoopRemoveObserver(...)
run run
runUntilDate:
runMode:beforeDate:
CFRunLoopRun()
CFRunLoopRunInMode(...)
CFRunLoopRunSpecific(...)

1. __CFRunLoop相关数据结构

struct __CFRunLoop {
    ...
    pthread_t _pthread;//对应的线程
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
};

__CFRunLooprunloop本身:typedef struct __CFRunLoop *CFRunLoopRef__CFRunLoop对应多个__CFRunLoopMode

struct __CFRunLoopMode {
    ...
    CFStringRef _name;
    CFMutableSetRef _sources0;//
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
};

__CFRunLoopMode是runloop的运行模式:typedef struct __CFRunLoopMode *CFRunLoopModeRef。每一个__CFRunLoopMode又包含多个_sources0、_sources1、_observers、_timers事件。_sources0:非基于Port的,也就是用户主动发出的事件。_sources1:基于Port的,也就是系统内部的消息事件。_observers:观察者。
_timers:定时器事件。
系统默认注册了5中类型的Mode

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

主线程默认运行在kCFRunLoopDefaultMode下,滑动scrollView,就变成了UITrackingRunLoopMode,手指离开又变成了kCFRunLoopDefaultMode

相关的类的成员变量与关系:


__CFRunLoop.png

如上图时Runloop中用到的基础结构,再对应关系方面:一个__CFRunLoop实例可以包含多个__CFRunLoopMode;一个__CFRunLoopMode又包含多个CFRunLoopSourceRef、CFRunLoopObserverRef、CFRunLoopTimerRef事件。一个Runloop要想跑起来,内部必须要有一个Mode,并且这个Mode里边必须包含一个Source/Observer/Timer事件。

CFRunLoop的状态:

名称 说明
kCFRunLoopEntry 即将进入runloop
kCFRunLoopBeforeTimers 即将处理timer事件
kCFRunLoopBeforeSources 即将处理source事件
kCFRunLoopBeforeWaiting 即将进入睡眠
kCFRunLoopAfterWaiting 被唤醒
kCFRunLoopExit runloop退出

2._CFRunLoop的创建、运行、退出

-创建

[NSRunLoop mainRunLoop]对应底层的CFRunLoopGetMain(),[NSRunLoop currentRunLoop]对应底层的CFRunLoopGetCurrent(),内部都是通过CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)获取的runloop,分别传入pthread_main_thread_np()pthread_self()也就是主线程和当前线程的id。

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
        //如果外部传入无效的0,则将主线程ID赋值给t。
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        //如果__CFRunLoops为空,则创建主线程对应的runloop
        __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //__CFRunLoopCreate做线程的初始化
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); 
        // 将mainLoop保存到dict中,以线程id为key,mainLoop为value
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //将dict中的内容复制到__CFRunLoops地址上
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //通过线程id获取runloop
    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) {
            //如果创建后还不能获取到则使用刚才创建的,并将newLoop保存到__CFRunLoops中
            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;
}

__CFRunLoopCreate中通过_CFRuntimeCreateInstance实例华再进行其他变量的一些初始化。其中loop->_pthread = t;将线程id绑定到了runloop上。runloop通过线程id去查找,如果没有则进行创建并将线程id绑定到runloop上,通过这个规则我们知道:线程与runloop一一对应;线程不一定都有runloop,首个runloop创建时会检查__CFRunLoops是否为空,为空则先创建主线程的runloop,再创建指定线程的runloop

-运行

启动Runloop,调用CFRunLoopRun()即可,Runloop进入运行循环,运行状态只要不是kCFRunLoopRunStoppedkCFRunLoopRunFinished就会一直运行下去不退出。

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//将runloop的运行状态切换到指定的Mode

//代码较长,只列出重要步骤的
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    ...
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果没有获取到mode或者mode中的事件为空(无sources0/sources1等)返回kCFRunLoopRunFinished
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        return kCFRunLoopRunFinished;
    }
    //保存previousPerRun、previousMode
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    //通知Observer即将进入循环
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知Observer即将退出循环
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    //恢复previousPerRun,previousMode
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    return result;
}

3.Runloop的使用

3.1.main函数为何能保持不退出?

main函数中,会调用UIApplicationMain函数,在内部会启动主线程的Runloop,可以不断的接收消息,比如点击屏幕事件,滑动列表以及处理网络请求的返回等接收消息后对事件进行处理,处理完之后,就会继续等待。

3.2.NSTimer相关案例

案例1:

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

我们主线程执行如下代码,我们Timer能够正常运行,但是如果我们在进行scrollView滑动的时候定时器会停止,这是什么原因呢?在新开子线程调用它运行不起来,这是什么原因呢?

这里我们需要明白:1.scheduledTimerWithTimeInterval:方法会自动把当前初始化的Timer加入到currentRunLoopkCFRunLoopDefaultMode模式下,主线程的runloop已经在run状态了,所以定时器会立即启动。如果手动滑动scrollView,则主线程的runloop的状态切换为UITrackingRunLoopMode模式了,添加在kCFRunLoopDefaultMode模式的Timer自然就没有回调了。解决办法:将Timer手动添加到UITrackingRunLoopMode模式或者kCFRunLoopCommonModes模式即可。

如果在新开的子线程执行上面的代码,由于新开的子线程并不会主动创建runloop,所以定时器自然运行不起来。解决办法:手动将Timer加入到currentRunLoopkCFRunLoopCommonModes模式,并且执行run方法。

3.3监听runloop的运行状态
-(void)addObserver{
    /*1.创建监听者
      第一个参数:怎么分配存储空间
      第二个参数:要监听的状态 。kCFRunLoopAllActivities表示所有的状态
      第三个参数:是否持续监听
      第四个参数:优先级 总是传0
      第五个参数:当状态改变时候的回调
      */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即将进入runloop"); break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理timer事件"); break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理source事件");break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入睡眠"); break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"被唤醒"); break;
            case kCFRunLoopExit:
                NSLog(@"runloop退出");break;
            default:
                break;
        }
    });
    /*2.添加监听者
       第一个参数:要监听哪个runloop
       第二个参数:观察者
       第三个参数:运行模式
       */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
3.4常驻线程

一个线程要想跑起来,则需要至少一个mode,一个事件。所以我们可以使用Timer或者Source事件。
第一步创建一个新的线程

- (void)createThread {
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];
    [self.thread start];
}

第二步在新开的线程添加Timer或者Source事件。

- (void)task1{
  [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
  [[NSRunLoop currentRunLoop] run];
}

第三部测试线程是否正常运行。可以通过点击等连续触发,也可以在主线程多次调用test来测试

- (void)test {
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)task2{
    NSLog(@"task2---%@",[NSThread currentThread]);
}

你可能感兴趣的:(iOS开发进阶:RunLoop相关分析总结)