将runloop的理解都写到这里

作者: Corbin___ | 来源:发表于2020-05-10 22:54 被阅读0次

    项目代码

    runloopDemo

    CFCoreFoundation源码

    目录

    • 从主线程runloop啥时候开启
    • runloop对象是怎么存储的
    • runloop怎么跑起来的,又是怎么退出的
    • Runloop do-while做了什么
    • 监听Runloop的状态
    • 常驻线程以及怎么销毁常驻线程
    • runloopperformSelector
    • 网络请求主线程回调,实现同步
    • runloop优化tableview滚动坑点
    • runloop卡顿监测
    • runloopautoreleasepool
    • 界面更新

    从主线程runloop啥时候开启

    appmain函数中的UIApplicationMain走进去,就一直在里面循环了,NSLog(@"会走吗");是不会被调用的

    这里我就有一个疑惑:那为啥这个main还要return int类型呢?既然都死循环,return不了

    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            // Setup code that might create autoreleased objects goes here.
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
        }
        int i = UIApplicationMain(argc, argv, nil, appDelegateClassName);
        
        NSLog(@"会走吗");
        return i;
    }
    
    

    进入UIApplicationMain后,就会接着调用application:didFinishLaunchingWithOptions:方法,在这个方法里就开启runloop,通过监听runloop状态,在即将进入runloop回调打上断点,看堆栈即可得知


    runloop对象是怎么存储的

    runloop跑起来,得先获取runloop对象,我们从CFRunloop.c的源码中,找到CFRunLoopGetCurrent

    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());
    }
    

    从这两个方法,获取Runloop的入参是线程对象,可以判定,线程与runloop一一对应的关系,具体,我们再看下_CFRunLoopGet0的实现

    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        // 如果参数为空,那么就默认是主线程
        if (pthread_equal(t, kNilPthreadT)) {
            t = pthread_main_thread_np();
        }
        
        __CFLock(&loopsLock);
        
        // static CFMutableDictionaryRef __CFRunLoops = NULL;
        // 存放Runloop对象的字典
        // 先判断这个Runloop字典存在不,不存在就创建一个,并加主线程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);
        
        // 如果不存在,就创建一个Runloop,并加到字典中
        if (!loop) {
            CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
            loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
            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;
    }
    
    

    大概的代码逻辑就是

    实现思路
    1.先判断这个全局字典存不存在,不存在,创建一个,并将主线程的runloop加进去
    2.直接去字典里取这个loop
    3.如果loop不存在,就创建一个loop加入到全局字典中
    // 伪代码
    if(!__CFRunLoops) {
          1.创建全局字典
          2.创建主线程loop,并加入到全局字典中
    }
    根据线程pthread_t为key,去字典取对应的loop
    if(!loop) {
          1.创建loop
          2.加入到字典中
    }
    return loop
    

    所以:

    • runloop对象和线程是一一对应的关系
    • runloop对象是储存在一个全局字典中的,这个全局字段的key是线程对象,valuerunloop对象

    runloop怎么跑起来的,又是怎么退出的

    先说下runloop跑一圈是做了什么事情

    首先runloop有六个状态变化

    
    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
    };
    
    
    image

    所以,当启动runloop的时候,就是监听输入源(端口port、source0、source1)、定时器、如果有事件,处理事件,没有就休眠

    但是实际上并不是这样的,而是一直在重复的进入runloop(使用run方法启动runloop的情况)

    我们先从开启runloop的函数入手,从CFRunLoopRun函数,我们看到了runloop确实是一个do-while操作,那么里面的CFRunLoopRunSpecific每走一次,就算runloop迭代一次,那么这个runloop迭代一次后,会退出runloop,退出runloop后,因为CFRunLoopRun函数有do-while操作,所以又会重新进入runloop

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

    CFRunLoopRunSpecific中做了一些前置判断,比如判断当前Mode为空,直接return,这个也可以说明一点启动runloop之前,runloop中一定要有输入源或者定时器

    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
        ...
        
        //  前置判断,比如判断当前`Mode`为空,直接`return`
        if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
            Boolean did = false;
            if (currentMode) __CFRunLoopModeUnlock(currentMode);
            __CFRunLoopUnlock(rl);
            return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
        }
        
        
       ...
       
        // 回调即将进入runloop
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        
        // 进入runloop
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        
        // 即将退出runloop
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
        
        ...
        
        return result;
    }
    

    接下来再看下__CFRunLoopRun函数

    // 简化代码,详细直接搜源码
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        do {
            // 监听source、timer
            if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
            if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
            
           // 处理source0
           Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
           
           // 即将进入休眠
           if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    
            ...
            
             // 退出runloop的条件
             if (sourceHandledThisLoop && stopAfterHandle) {
                  // 处理完source后sourceHandledThisLoop会为YES
                  // stopAfterHandle,如果是CFRunloop调用的话,是为NO
                  // 可以回头看下CFRunLoopRun函数
                  // 
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                // 自身超时时间到了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                // 被外部调用CFRunloop停止
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                // 被 _CFRunLoopStopMode 停止
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 检查上一个 mode 有没有执行完所有事件源
                retVal = kCFRunLoopRunFinished;
            }
           
        } while(0 = retVal);
    }
    
    

    退出runloop有四个条件

    • 入参stopAfterHandle为YES的时候,那么处理完source就会退出runloop
    • 自身超时时间到了
    • 被外部调用CFRunloop停止
    • _CFRunLoopStopMode 停止

    CFRunLoopRun指定stopAfterHandleNO,说明使用run方法开启runloop,处理完source后不会退出runloop

    如果是使用CFRunLoopRunInMode则可以指定是否需要处理完source后就退出runloop


    Runloop do-while做了什么

    do-while的过程中,做了以下操作

    • 监听source(source1是基于port的线程通信(触摸/锁屏/摇晃等),source0是不基于port的,包括:UIEvent、performSelector),监听到就处理
    • 监听timer的事件,监听到就处理
    • 没有source和timer的时候,就休眠,休眠不是不监听,还是保持监听的,只是当有事件的时候,才唤醒,继续处理

    当我们触发了事件(触摸/锁屏/摇晃等)后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,并专门处理用户交互设备,由IOHIDServices和IOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,接着用mach port转发给需要的App进程,随后 Source1就会接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()内触发Source0,Source0 再触发 _UIApplicationHandleEventQueue()。所以触摸事件看到是在 Source0 内的。

    总结:触摸事件先通过 mach port 发送,封装为 source1,之后又转换为 source0

    1.一个runloop对应一个线程,多个mode,一个mode下对应多个sourceobservertimer

    struct __CFRunLoop {
        pthread_t _pthread; // 线程对象
        CFMutableSetRef _commonModes; // 
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        ...
        // 简化
    };
    
    struct __CFRunLoopMode {
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        ...
        // 简化
    };
    
    

    2.常见有五种mode

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

    除了以上5个mode,还有其他mode,但是很少遇见这里

    4.子线程不自动开启runloop,手动开启runloop前,必须得有输入源和定时器(输入源就是通过监听端口,可以获取不同的事件),通过CFRunloop源码中的CFRunLoopRunSpecific函数,其中判断了当modenull或者modeItem为空,直接return

    监听Runloop的状态

    runloop有六大状态

    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
    };
    

    可以通过这就代码监听这六个状态

    CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
    

    其中的参数分别为

    CFRunLoopObserverCreate参数

    1.不懂

    2.监听runloop变化状态

    3.是否重复监听

    4.不懂,传0

    5.回调的函数指针(需要自己写一个函数)

    6.CFRunLoopObserverContext对象

    定义函数指针

    static void runLoopOserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
       //void *info正是我们要用来与OC传值的,这边可以转成OC对象,前面我们传进来的时候是self
        RunloopObserver *target = (__bridge RunloopObserver *)(info);//void *info即是我们前面传递的self(ViewController)
        
        if (target.callback) {
            target.callback(observer, activity);
        }
    }
    

    定义CFRunLoopObserverContext对象,其实这个参数是用于通信的

    // 从CFRunLoopObserverRef点进去找
        
        typedef struct {
            CFIndex    version; // 传0,不知道是什么
            void *    info; // 数据传递用的,void *,指就是可以接受所有指针
            const void *(*retain)(const void *info); // 引用
            void    (*release)(const void *info); // 回收
            CFStringRef    (*copyDescription)(const void *info); // 描述,没用到
        } CFRunLoopObserverContext;
    

    创建监听

    //创建一个监听
    static CFRunLoopObserverRef observer;
        
    // CFRunLoopObserverCreate参数。1.不懂  2.监听runloop变化状态  3.是否重复监听  4.不懂,传0 5.函数指针  6.CFRunLoopObserverContext对象
    observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopOserverCallBack, &context);
        
    //注册监听
    CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopCommonModes);
        
    //销毁
    CFRelease(observer);
        
    

    常驻线程以及怎么销毁常驻线程

    先说下performSelector和子线程的,perform...AfterDelayperform..onThread都需要在开启runloop的线程执行

    因为其实现原理,都是往runloop添加一个不重复的定时器

    - (void)test1
    {
        [self.myThread setName:@"StopRunloopThread"];
        self.myRunloop = [NSRunLoop currentRunLoop];
        
        // performSelector:afterDelay:的原理是往runloop添加不重复执行的定时器
        [self performSelector:@selector(performSelAferDelayClick) withObject:nil afterDelay:1];
        
        [self.myRunloop run];
        
        NSLog(@"我会走吗");
    }
    

    如果将开启runloop的代码,写到perform前,那么会开启不成功,因为开启runloop需要有输入源或者定时器的情况才可以开启

    获取runloop会调用CFRunLoopRunSpecific函数(具体搜下CFRunloop.c

    从这个函数中找到以下代码,当currentMode为空的时候,(也就没有输入源或者定时器),直接return kCFRunLoopRunFinished,开启失败

    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    

    以下代码实现了一个常驻线程

    原理就是往当前线程的runloop中添加一个端口,让其监听这个端口(理解为监听某个端口的输入源,比如系统内核端口,监听一些系统事件),因为可以一直监听这个端口,那么runloop就不会退出

    其实就是保持runloop不退出,就达到常驻线程的效果了,那么要让runloop不退出,就得有输入源或者重复的定时器让其监听

    - (void)test2
    {
        [self.myThread setName:@"StopRunloopThread"];
        self.myRunloop = [NSRunLoop currentRunLoop];
        self.myPort = [NSMachPort port];
        
        [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
        
        [self.myRunloop run];
        
        // 因为run之后,这个线程就一直在做do-while操作
        // 相当上面的代码被do-while包围了,那么以下代码不会走
        NSLog(@"我会走吗");
    }
    

    当开启一个线程,就会对应创建一个runloop对象吗?

    不是的,调用获取当前runloop的方法,内部实现:如果当前runloop不存在就创建一个,存在就返回当前runloop

    所以走这句代码self.myRunloop = [NSRunLoop currentRunLoop];就生成当前线程对应的runloop


    怎么销毁常驻线程?

    1.要销毁常驻线程,首先得先把runloop退出?

    当没有输入源或者定时器可以监听的时候,退出runloop

    如果我们调用[NSThread exit];,这时候线程是销毁了,但是线程中的代码还是不会执行,比如NSLog(@"我会走吗");,

    说明这时候的runloop并没有退出,那么这样会导致一些问题,例如以下代码

    - (void)test2
    {
        [self.myThread setName:@"TestThread"];
        self.myRunloop = [NSRunLoop currentRunLoop];
        self.myPort = [NSMachPort port];
        
        // 添加监听NSMachPort的端口(这个端口可以理解为输入源,因为可以一直监听这个,所以这时候的runloop不会退出,会一直在做do-while)
        [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
        
        [self.myRunloop run];
        
        // [self.myRunloop run]; 会导致以下代码没法走,因为runloop就是一个do-while的循环,do-while监听源,处理源
        [self.testPtr release];
    }
    

    因为runloop没有退出,[self.testPtr release];不会执行,那么就会导致testPtr对象没法释放

    2.怎么退出runloop呢

    如果常驻线程是通过监听端口实现的,那么就调用[self.myRunloop removePort:self.myPort forMode:NSDefaultRunLoopMode];,移除端口,就可以销毁了

    其实这时候还不一定能成功销毁,因为可能系统加入了一些其他源的监听

    如果是通过添加重复定时器,实现常驻线程(这种方式不可取,因为比添加监听端口耗性能,需要一次又一次的唤醒runloop)

    - (void)test11
    {
        [self.myThread setName:@"TestThread"];
        self.myRunloop = [NSRunLoop currentRunLoop];
        self.myPort = [NSMachPort port];
        
        // 首先,需要有一点,当runloop监视输入源或者定时器的时候,才不会退出
        // 开启runloop之前,需要有输入源或者定时器
        // 定时器(如果是添加定时器,不重复,那么监听一次就退出了)
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer执行");
        }];
        
        [self.myRunloop addTimer:timer forMode:NSDefaultRunLoopMode];
        
        
        
        [self.myRunloop run];
        
        // [self.myRunloop run]; 会导致以下代码没法走,因为runloop就是一个do-while的循环,do-while监听源,处理源
        NSLog(@"我会走吗");
        
    }
    

    如果NSTimerrepeatsNO,那么执行一次timer的事件后,就会退出runloop

    以上,如果通过移除端口,结束timer,反正以移除已知的输入源或者定时器来退出runloop都是不太靠谱的,因为系统内部有可能会在当前线程的runloop中添加一些输入源,也就是还有未知的输入源,我们没有移除

    3.使用CFRunLoopStop退出Runloop

    - (void)test3
    {
        [self.myThread setName:@"TestThread"];
        self.myRunloop = [NSRunLoop currentRunLoop];
        self.myPort = [NSMachPort port];
        
        // 添加监听NSMachPort的端口(这个端口可以理解为输入源,因为可以一直监听这个,所以这时候的runloop不会退出,会一直在做do-while)
        [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
        [self performSelector:@selector(runloopStop) withObject:nil afterDelay:1];
    
        [self.myRunloop run];
        
        // [self.myRunloop run]; 会导致以下代码没法走,因为runloop就是一个do-while的循环,do-while监听源,处理源
        NSLog(@"我会走吗");
    }
    
    - (void)runloopStop
    {
        NSLog(@"执行stop");
        CFRunLoopStop(self.myRunloop.getCFRunLoop);
    }
    

    输出:

    2020-05-03 20:10:12.614130+0800 Runloop[60465:2827474] 即将进入Loop,
    2020-05-03 20:10:12.614465+0800 Runloop[60465:2827474] 即将处理 Timer,
    2020-05-03 20:10:12.615214+0800 Runloop[60465:2827474] 即将处理 Source,
    2020-05-03 20:10:12.615634+0800 Runloop[60465:2827474] 即将进入休眠,
    2020-05-03 20:10:13.615638+0800 Runloop[60465:2827474] 刚从休眠中唤醒,
    2020-05-03 20:10:13.616005+0800 Runloop[60465:2827474] 执行stop
    2020-05-03 20:10:13.616194+0800 Runloop[60465:2827474] 即将退出Loop,
    2020-05-03 20:10:13.616360+0800 Runloop[60465:2827474] 即将进入Loop,
    2020-05-03 20:10:13.616511+0800 Runloop[60465:2827474] 即将处理 Timer,
    2020-05-03 20:10:13.616648+0800 Runloop[60465:2827474] 即将处理 Source,
    2020-05-03 20:10:13.616765+0800 Runloop[60465:2827474] 即将进入休眠,
    
    

    确实是退出了runloop,但是又马上进入了

    原因是:

    开启线程有三种方式

    // 不会退出runloop
    - (void)run; 
    
    // 超时时候到退出runloop
    - (void)runUntilDate:(NSDate *)limitDate; 
    
    // 处理完source会退出或者时间到也会退出
    - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
    
    // 上三个方法分别对应CFRunloop
    
    void CFRunLoopRun(void)
    
    SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled为NO
    
    SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled为YES
    
    
    
    

    runrunUntilDate:都会重复的调runMode:beforeDate:

    具体的解释看NSRunLoop的退出方式

    所以刚才stop之后,确实是退出runloop了,但是因为我们是用run启动的,所以会重复的调用runMode:beforeDate:又启动了

    3.用runMode:beforeDate:启动runloop,再用CFRunLoopStop退出runloop试试

    将上一段代码[self.myRunloop run];替换成[self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    *****成功退出runloop并且线程run后的代码也走了,这时候通过打个暂停断点,看堆栈,发现我们的线程不在了,说明已经被销毁了(runloop退出后,线程没有任务,自然就销毁了)*****

    2020-05-03 20:21:30.330067+0800 Runloop[60593:2834891] 即将进入Loop,
    2020-05-03 20:21:30.330303+0800 Runloop[60593:2834891] 即将处理 Timer,
    2020-05-03 20:21:30.330639+0800 Runloop[60593:2834891] 即将处理 Source,
    2020-05-03 20:21:30.330906+0800 Runloop[60593:2834891] 即将进入休眠,
    2020-05-03 20:21:31.330956+0800 Runloop[60593:2834891] 刚从休眠中唤醒,
    2020-05-03 20:21:31.331329+0800 Runloop[60593:2834891] 执行stop
    2020-05-03 20:21:31.331591+0800 Runloop[60593:2834891] 即将退出Loop,
    2020-05-03 20:21:31.331783+0800 Runloop[60593:2834891] 我会走吗
    
    

    虽然使用self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]可以成功退出runloop,但是还是有问题,当runloop处理完source后,就退出runloop了,而且这时候,也不会想调用run方法那样,重新进入runloop

    所以这种方式还是不行

    最后一个最佳方式,既能手动退出runloop,有不会处理完source就退出runloop,不再进来

    BOOL shouldKeepRunning = YES; // global
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    while (shouldKeepRunning) {
        // runMode是有返回值的,当启动runloop后,是不会返回的,所以不会一直在调这个方法,runloop退出了,才会再调
        [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
    }
    

    当想退出runloop的时候,将shouldKeepRunning置为NO就可以了


    runloopperformSelector

    performSelector:withObject:afterDelay:原理,往runloop添加一个不重复的定时器

    所以子线程调用这个方法,是需要开启runloop才有效的

    顺便看看performSelector:onThread:withObject:waitUntilDone:

    // myThread是常驻线程
    self.myThread = [[PermanentThread alloc] initWithTarget:self selector:@selector(myThreadStart) object:nil];
    
    [self.myThread start];
        
    NSLog(@"1");
    [self performSelector:@selector(performWait) onThread:self.myThread withObject:nil waitUntilDone:NO];
    NSLog(@"2");
    
    - (void)performWait
    {
        NSLog(@"3");
    }
    
    

    如果waitUntilDone为NO,那么就是不等待sel执行完,才往下走

    输出为1、2、3

    如果为YES,那么就是会卡住当前线程,等待sel执行完才走

    输出为1、3、2


    网络请求主线程回调,实现同步

    需求描述:

    给你一个接口,这个接口是网络请求,回调是主线程回来的,现在要求调用这个接口后,需要等待回调回来后,后面的代码才可以接着往下走

    - (void)netRequestComplete:(void(^)(void))complete
    {
        // 模拟网络请求,主线程回调
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                if (complete) {
                    complete();
                }
            });
        });
    }
    
    

    使用信号量,会导致死锁

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    [self netRequestComplete:^{
        NSLog(@"3");
        // 因为主线程被卡住,这里不会走了,所以死锁
        dispatch_semaphore_signal(sema);
    }];
        
    // 卡住主线程
    dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)));
    NSLog(@"2");   
    

    正确方式使用CFRunloopRun

    [self netRequestComplete:^{
        NSLog(@"3");
        // stop,退出runloop,主线程runloop退出后,又会自动加入,就像前面讲的,开启runloop是使用run的方法
        CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
    }];
        
    // CFRunLoopRun()相当加了do-while,这时候下面的代码执行不了
    CFRunLoopRun();
    NSLog(@"2");
        
    

    runloop优化tableview滚动坑点

    这个点是从这个文章得知的UITableView性能优化-中级篇

    做了一个实验

    首先,用perform确实可以滑动tableview滚动的时候,不加载图片,达到优化的效果

    但是通过这个实验发现,当我停止滚动的时候,前面滑过的indexPath,都会触发logIndexRow:方法

    如果这时候是加载图片,那么是多余的了,因为cell都划出界面了,没有必要加载

    由这个现象,可以大概的判断,performSelector:inModes,如果是在defaultmode下调用,虽然现在是在滚动,不会触发方法,但是perform就往runloop的defaultMode添加输入源,但滚动结束的时候,切换回defaultMode,这些输入源都会被触发

    // 这个selector,可以是loadImg的方法
    [self performSelector:@selector(logIndexRow:)
                   withObject:indexPath
                   afterDelay:0
                      inModes:@[NSDefaultRunLoopMode]];
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"123"];
        
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"123"];
        }
        
        // 滑动的时候,不会调用logIndexRow:,因为这时候mode是滑动,但是perform也是属于输入源,这些事件会被积累在NSDefaultRunLoopMode下,当切换到NSDefaultRunLoopMode下后,就会执行这些输入源事件
        [self performSelector:@selector(logIndexRow:)
                   withObject:indexPath
                   afterDelay:0
                      inModes:@[NSDefaultRunLoopMode]];
        
        cell.textLabel.text = @"123";
        cell.textLabel.textColor = [UIColor redColor];
        
        return cell;
    }
    

    runloop卡顿监测

    引用源码__CFRunLoopRun分析中的说法

    从 kCFRunLoopBeforeSources 为起点到 kCFRunLoopBeforeWaiting 休眠前,这其中处理了大量的工作————执行 block,处理 source0,更新界面…做完这些之后 RunLoop 就休眠了,直到 RunLoop 被 timer、source、libdispatch 唤醒,唤醒后会发送休眠结束的 kCFRunLoopAfterWaiting 通知。我们知道屏幕的刷新率是 60fps,即 1/60s ≈ 16ms,假如一次 RunLoop 超过了这个时间,UI 线程有可能出现了卡顿,BeforeSources 到 AfterWaiting 可以粗略认为是一次 RunLoop 的起止。至于其他状态,譬如 BeforeWaiting,它在更新完界面之后有可能休眠了,此时 APP 已是不活跃的状态,不太可能造成卡顿;而 kCFRunLoopExit,它在 RunLoop 退出之后触发,主线程的 RunLoop 除了换 mode 又不太可能主动退出,这也不能用作卡顿检测。

    监听即将处理source,到结束睡眠,如果这个过程超过一帧的时间,就可能出现丢帧的情况(丢帧就会导致卡顿)

    那么为什么这个过程如果一帧的时间,就可能卡顿呢?

    首先我们要理解屏幕显示原理,大概就是CPU计算文本、布局、绘制、图片解码,之后就提交位图到GPU,GPU就进行渲染,渲染完成后,根据V-sync信号,更新缓冲区,同时,视频控制器的指针,也会根据V-sync信号去缓冲区读取一帧的缓存,显示到屏幕上

    也就是说从cpu绘制->GPU渲染,要在16ms内完成,才能保证在指定时间内,给视频控制器读取,否则,视频控制器就会读到上一帧的画面,这就导致卡顿了

    所以在即将处理source,到结束睡眠这段时间内,如果CPU一直在处理一件任务,如果超过了16ms,那么可能就来不及在16ms内完成一帧画面的渲染

    runloopautoreleasepool

    App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

    设置_wrapRunLoopWithAutoreleasePoolHandler符号断点,可以从汇编代码,看到autoreleasepush、pop

    界面更新

    不知道怎么证明。。。。


    参考文章

    NSRunLoop的退出方式

    戴铭写的Runloop

    NSURLConnection的执行过程

    很多runloop的问题和例子

    深入理解RunLoop(YYModel作者)

    源码__CFRunLoopRun分析

    相关文章

      网友评论

        本文标题:将runloop的理解都写到这里

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