美文网首页iOS开发进阶
iOS开发进阶:RunLoop相关分析总结

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

作者: __Null | 来源:发表于2021-12-13 22:58 被阅读0次

    什么是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相关分析总结

        本文链接:https://www.haomeiwen.com/subject/minsfrtx.html