OC-Runloop

作者: xiaoyouPrince | 来源:发表于2020-07-14 17:32 被阅读0次

    RunLoop 是什么

    runloop 就是一个运行循环,目的是让程序运行起来不会直接结束,能在有任务的时候处理任务,没有任务的时候等待处理任务。

    iOS 中有两套API 可以访问 runloop

    • Foundation : NSRunLoop
    • Core Foundation : CFRunLoopRef

    参考

    RunLoop 与线程的关系

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
    • RunLoop会在线程结束时销毁
    • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

    获取 RunLoop 对象

    Foundation
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
    
    Core Foundation
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    

    可以从 CFRunLoopGetCurrent() 源码中看到相关获取逻辑,代码简化如下

    
    CFRunLoopRef CFRunLoopGetCurrent(void) {
        // 返回根据当前线程取到的runloop
        return _CFRunLoopGet0(pthread_self());
    }
    
    // 核心逻辑如下,已简化代码
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        
        // 1.判断如果没有runloop字典
        if (!__CFRunLoops) {
            // 2.创建一个字典
            CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
            // 3.根据主线程创建runloop
            CFRunLoopRef mainLoop = __CFRunLoopCreate(_CFMainPThread);
            // 4.在dict中将主线程指针和当前创建的runloop以 key:value 形式保存
            CFDictionarySetValue(dict, pthreadPointer(_CFMainPThread), mainLoop);
            // 5.dict 赋值给 __CFRunLoops(代码经简化,源码并非如此简单)
            __CFRunLoops = dict;
        }
        
        // 6. 从 __CFRunLoops 中根据参数t指针获取runloop
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            // 7. 如果没有就创建一个新loop对象
            CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            // 8. __CFRunLoops 中添加参数t的指针和新loop对象保存
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            // 9. 新runloop 对象就是要返回的loop对象
            loop = newLoop;
        }
        return loop;
    }
    
    

    runloop 内部数据结构

    runloop 内部核心数据

    struct __CFRunLoop {
        pthread_t _pthread;                 // 对应的线程
        CFMutableSetRef _commonModes;       // 通用Mode
        CFMutableSetRef _commonModeItems;   // 通用mode的items
        CFRunLoopModeRef _currentMode;      // 当前mode
        CFMutableSetRef _modes;             // 所有的mode
    };
    

    从数据结构中可以看到,实际就是 runloop 中包含自己的 thread 和 mode, CFRunLoopModeRef 类型结构如下

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

    可以看到 runloop 的内部结构基本如图

    runloop结构

    runloop 的运行模式 CFRunLoopModeRef

    • CFRunLoopModeRef代表RunLoop的运行模式
    • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
    • RunLoop启动时只能选择其中一个Mode,作为currentMode
    • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
      • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
    • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
    • 常见的2种Mode
      • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
      • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

    Runloop 的运行逻辑

    说白了 runloop 就是在整循环中一直检查当前运行的 mode 内部有没有需要处理的事件,有就处理,没有就休眠

    runloop运行逻辑

    Runloop 的 mode 内部主要有下面4类事件要处理

    • Source0

      • 触摸事件处理
      • performSelector:onThread:
    • Source1

      • 基于Port的线程间通信
      • 系统事件捕捉
    • Timers

      • NSTimer
      • performSelector:withObject:afterDelay:
    • Observers

      • 用于监听RunLoop的状态
      • UI刷新(BeforeWaiting)
      • Autorelease pool(BeforeWaiting)

    一次循环处理事件的逻辑如下

    一次runloop的循环逻辑

    一个简单的线程保活

    @interface PermenantThread()
    @property (strong, nonatomic) NSThread *innerThread;
    @end
    
    @implementation PermenantThread
    #pragma mark - public methods
    - (instancetype)init
    {
        if (self = [super init]) {
            self.innerThread = [[NSThread alloc] initWithBlock:^{
                
                // 创建上下文(要初始化一下结构体)
                CFRunLoopSourceContext context = {0};
                // 创建source
                CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
                // 往Runloop中添加source
                CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
                // 销毁source
                CFRelease(source);
                
                // 启动
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
                
    //            while (weakSelf && !weakSelf.isStopped) {
    //                // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
    //                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
    //            }
                
                NSLog(@"end----");
            }];
            
            [self.innerThread start];
        }
        return self;
    }
    
    - (void)executeTask:(PermenantThreadTask)task
    {
        if (!self.innerThread || !task) return;
        
        [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
    }
    
    - (void)stop
    {
        if (!self.innerThread) return;
        
        [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        
        [self stop];
    }
    
    #pragma mark - private methods
    - (void)__stop
    {
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.innerThread = nil;
    }
    
    - (void)__executeTask:(PermenantThreadTask)task
    {
        task();
    }
    
    @end
    

    面试题

    • 讲讲 Runloop,项目中有用到吗?
    1. 用到过
    2. 线程保活,对于频繁需要在子线程做的操作,使用 runloo 进行线程保活 -- 适用于一个串行线程,非并发的情况。如需要一个后台线程持续做否些计算,网络请求。
    3. 如 AFNetworking 2.0版本的时候就是后台有一个常驻线程
    
    1. 实例
        1. 控制线程声明周期(线程保活)
        2. 解决NSTimer在滑动时停止工作的问题
        3. 监控应用卡顿
        4. 性能优化
    
    • runloop 内部实现逻辑?
    1. runloop 的创建逻辑,从内部的 dict 中获取,没有就新建,并保存,目的是一条线程只对应一个runloop
    2. runloop 的运行逻辑,本质上就是一个 while() 循环。每次进入循环的时候处理如上图事件。没有事件的时候进入休眠(通过调用内核接口实现)。
    
    • runloop 和线程的关系?
    是一一对应的关系。
    其实保存在 __CFRunLoops 全局 dict 中,以线程指针为key,线程对应的runloop地址为value
    
    • timer 和 线程的关系?
    • 程序中添加每3秒响应一次的NSTimer,当拖动 tableView 时候 timer 可能无法响应怎么解决?
    将 timer 添加到 commonMode中,这样就可以处理普通模式和tracking 模式下的事件了
    
    • runloop 是怎么影响用户操作的,具体流程是什么样的?
    由 source1 捕捉用户的触摸事件, 由 source0 处理用户的触摸事件。 V158T13
    
    • 说说 runloop 的几种状态?
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    
    • runloop 的 mode 作用是什么?
    不同 mode 将各自的 source、timer、observer 隔离开,这样同一时间只能执行一个 mode 下的 source、timer、observer,这样在某种模式下就会比较流畅。
    

    相关文章

      网友评论

        本文标题:OC-Runloop

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