面试遇到RunLoop的第一天-原理

2020/7/29 23:03:59

本文主要是介绍面试遇到RunLoop的第一天-原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

RunLoop是什么?你了解嘛。RunLoop也是作为一名iOS manager必须了解的一个知识点,开发中可能只有用到timer的时候,接触过runloop.其实,对于iOS App来说,runloop是一个非常重要的东西,可以说runloop是支持程序运行的不可缺少的一部分。

什么是RunLoop

RunLoop顾名思义,就是运行循环,一个如此抽象的描述,可以理解为在程序运行过程中,循环做一些事情。那么他的应用范畴有哪些呢?比如

  • Timer
  • performSelector
  • GCD
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • autoreleasePool

吃惊嘛?真的上面说到的这么多的事都是runloop去处理的嘛?你可能Timer,GCD都用的很溜,可是却没想过,是什么支撑他们可以实现他们本身的功能的。甚至没想过,App为何可以打开之后一直停留在App内,而一个命令行程序为什么执行完就退出了呢?

RunLoop的基本作用

没错,RunLoop的基本作用就是保持程序可以持续运行,处理App中的各种事件,比如触摸,定时器等,除此之外,RunLoop还节省CPU资源,帮助程序提高性能

但是RunLoop这块的知识,我们研究起来会感觉比较难,比较底层,而且源码都是C语言,理解起来也比较不容易,所以,下面我们是抱着了解的态度去学习吧,把重点的地方认真理解,其他比如runloop的处理流程等,作为一个了解就可以了。

iOS中有两套API来访问和使用RunLoop

  • Foundation: NSRunLoop
  • Core Foundation: CFRunLoopRef

NSRunLoopCFRunLoopRef都是RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层Objective-C的封装, CFRunLoopRef是完全开源的,源码在官网,大家感兴趣可以下载源码研究一下。

RunLoop与线程

RunLoop与线程的关系,也是面试中常遇到的问题,下面先说一下结论:

  1. 每条线程都有唯一一个与之对应的RunLoop对象
  2. 线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建
  3. RunLoop保存在全局的Dictionary中,线程为key,RunLoop是value
  4. RunLoop会在线程结束时销毁
  5. 主线程的RunLoop已经自动创建,子线程默认没有开启RunLoop

对于以上结论呢,在后面的源码分析中,会一步步证实。

先说一下主线程的RunLoop已经自动创建,但是上面有说了线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建,那主线程的RunLoop是在什么时候获取的呢?

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

主线程的RunLoop就是在UIApplicationMain这个函数中获取的,所以main函数执行完,主线程就有了自己的RunLoop,所以程序有RunLoop的支持也就不会退出。

获取RunLoop

FoundationCore Foundation都分别提供了获取RunLoop的方法

 [NSRunLoop mainRunLoop];//主线程对应的runloop
 [NSRunLoop currentRunLoop];//当前线程对应的runloop

 CFRunLoopGetCurrent();
复制代码

在源码中找到__CFRunLoop的定义:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    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;
};
复制代码

这里面我们最主要关注CFRunLoopModeRef

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
};
复制代码

这里的_sources0_sources1_observers_timers是不是开始变得熟悉了。CFMutableSetRef可以理解为一个set集合类,内部的元素无序且不重复。

RunLoop的运行逻辑

上面看到的_sources0_sources1_observers_timers都分别在RunLoop中处理什么逻辑呢?他们各司其职,分别处理文章开头说过的RunLoop应用范畴内的任务:

  • sources0:触摸事件处理、performSelector:onThread:
  • sources1: 基于port的线程间通信、系统事件捕捉
  • timers: NSTimer、performSelector:withObject:afterDelay:
  • observers: 用于监听RunLoop的状态、UI刷新、Autorelaese Pool

CFRunLoopModeRef

CFRunLoopModeRef 代表RunLoop的运行模式,一个RunLoop可以包含多个mode,每个mode又可以包含多个sources0,sources1,observers,timers

RunLoop启动时只能选择其中一个mode作为currentMode,如果需要切换mode,只能退出当前RunLoop,再重新选择一个mode。这里要注意,切换mode并不会导致程序退出,哪怕是主线程的RunLoop切换,也不会。

但是,mode中如果没有任何的sources0,sources1,observers,timers,RunLoop就会立刻退出。

常见的mode有两种:

  • kCFRunLoopDefaultMode :App的默认mode,通常主线程是在这个mode下运行
  • UITrackingRunLoopMode :界面跟踪mode,用于scrollview追踪滑动触摸,保证界面滑动时不受其他mode影响

CFRunLoopObserverRef

CFRunLoopObserverRef是用来监听RunLoop状态的,状态是一个CFRunLoopActivity类型的枚举,共有下面这几种:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),        //即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),         //即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码

创建observer 有两种方法,一种是带着block的,另外一种需要一个监听的方法

CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
#if __BLOCKS__
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity)) API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
#endif
复制代码

demo中我用了两种方法分别测试了监听,注意添加observer到runloop后,还需要调用CFRelease释放一下

    //kCFRunLoopCommonModes 默认包括kCFRunLoopDefaultMode UITrackingRunLoopMode
//   创建observer 的两种方法
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
//    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//        switch (activity) {
//            case kCFRunLoopExit:{
//                CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
//                NSLog(@"kCFRunLoopExit - %@",model);
//                CFRelease(model);
//            }
//                break;
//            case kCFRunLoopEntry:{
//                CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
//                NSLog(@"kCFRunLoopEntry- %@",model);
//                CFRelease(model);
//            }
//                break;
//
//            default:
//                break;
//        }
//    });
    // 添加observer到runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
复制代码

用上面这段demo的代码,我们就可以监听到,timer是可以唤醒RunLoop的,以及scrollview滑动前后,mode的切换是需要退出loop再进入的。

RunLoop处理逻辑

关于RunLoop处理逻辑,我们只做一个了解就可以了,可以看看下面这种图,是大体的处理步骤,在研究源码的时候,可以对照这张图,帮助理解。

源码分析

开始分析源码,第一步肯定是要找到RunLoop的入口,比如断点在touchesBegan中,控制台通过 bt命令,查看所有的调用栈,就可以找到CFRunLoopRunSpecific

然后可以在源码中通过搜索找到CFRunLoopRunSpecific函数的实现

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    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;
}
复制代码

源码确实晦涩难懂,我们找到关键代码主要看调用流程就可以了,核心是调用了__CFRunLoopRun函数,得到result最后返回。__CFRunLoopRun中的实现就更加复杂了,当然也是在__CFRunLoopRun中,就可以找到上面RunLoop处理逻辑的每一个步骤对应的源码。

RunLoop休眠

RunLoop休眠,就是线程阻塞和普通的线程阻塞是不一样的 ,他是真的会让线程休眠 ,不做任何事,CPU也不分配资源,一直等待线程被唤醒,要做到这样的休眠,只有在内核层面的API才能办到。

所以RunLoop的休眠这里还存在一个用户态和内核态的切换,从用户态切换到内核态进入休眠,当收到唤醒线程的消息后,又切换到用户态处理消息。



这篇关于面试遇到RunLoop的第一天-原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程