美文网首页
第二十八节—RunLoop(三)

第二十八节—RunLoop(三)

作者: L_Ares | 来源:发表于2020-11-19 00:34 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    经历过前两节对RunLoop对象的本质结构RunLoop逻辑的探索,对RunLoop的基本概念和性质也有了初步的了解,本节就开始根据平常常见的RunLoop的使用场景,探索一些RunLoop的应用场景。

    一、RunLoop与线程

    1. RunLoop和线程的关系

    先说一下RunLoop和线程的关系 :

    1. RunLoop与线程是一一对应的,一个RunLoop对应一个核心线程。为什么说对应一个核心线程,是因为RunLoop是可以嵌套的,但是核心的线程只有一个,也就是第一节在CFRunLoop结构体里面看到的pthread
    2. RunLoop和线程的关系是存储在一个全局的Dictionary里面的,线程是key,RunLoop是value
    3. RunLoop是用来管理线程的,当线程的RunLoop被开启后,线程会在执行完成任务后进入休眠状态,直到有新的消息来唤醒线程执行新消息。
    4. RunLoop的创建是通过第一次获取它来完成的,在线程任务结束时被销毁。
    5. 对于主线程来说,RunLoop是程序一启动的时候就通过UIApplicationMain中的[NSRunLoop currentRunLoop]来获取,如果没有就创建。
    6. 对于子线程来说,RunLoop是懒加载的,只有我们获取子线程的RunLoop的时候才会被创建。

    2. 线程获取RunLoop的实现

    这里我们看一下,在第一节RunLoop提到的获取线程的方式 :

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

    因为找不到Foundation框架的开源源码,所以这里我们看一下Core Foundation框架中的两个获取RunLoop对象的方法是怎么实现的。

    CFRunLoopRef CFRunLoopGetMain(void) {
        CHECK_FOR_FORK();
        //一个静态的mainRunLoop
        static CFRunLoopRef __main = NULL; // no retain needed
        //如果mainRunLoop不存在,则去创建主线程的RunLoop
        if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
        //返回mainRunLoop
        return __main;
    }
    
    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        //获取当前线程的RunLoop
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        //如果当前线程的RunLoop是存在的,直接返回
        if (rl) return rl;
        //如果当前线程的RunLoop不存在了,则创建当前线程的RunLoop再返回
        return _CFRunLoopGet0(pthread_self());
    }
    

    也就是说获取线程的核心方法是_CFRunLoopGet0();看一下它的实现 :

    static CFMutableDictionaryRef __CFRunLoops = NULL;
    static CFLock_t loopsLock = CFLockInit;
    
    // should only be called by Foundation
    // t==0 is a synonym for "main thread" that always works
    //t==0等同于让主线程始终工作
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        //判断传入的线程是不是t==0
        if (pthread_equal(t, kNilPthreadT)) {
            //如果是t==0,那么获取主线程
            t = pthread_main_thread_np();
        }
    
        //加一个runloop用的锁
        __CFLock(&loopsLock);
    
        //如果不存在保存RunLoop和线程关系的字典
        if (!__CFRunLoops) {
            //解锁
            __CFUnlock(&loopsLock);
            //创建一个全局的可变字典
            CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
            //创建主线程的RunLoop
            CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
            //设置key=主线程的地址指针,value=主线程的RunLoop,存入全局可变字典
            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) {
            //给当前线程创建一个新的RunLoop
            CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            //上锁
            __CFLock(&loopsLock);
            //重新获取当前线程的RunLoop
            loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
            //如果RunLoop并没有在创建期间被加入全局字典
            if (!loop) {
                //那么就把创建的新的RunLoop作为value,当前线程作为key,存入到全局字典中去
                CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
                //并且设置当前线程的RunLoop是新创建的RunLoop
                loop = newLoop;
            }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            //不要在loopsLock中释放RunLoop,因为CFRunLoopDeallocate可能最终会使用它
            __CFUnlock(&loopsLock);
            //解锁了再释放
            CFRelease(newLoop);
        }
        //判断一下现在还是不是在当前线程操作
        if (pthread_equal(t, pthread_self())) {
            //设置当前线程的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        //返回RunLoop
        return loop;
    }
    

    里面都有加了注释,理解起来并不困难,就不多说了。

    3. RunLoop常驻线程

    为什么要常驻线程?

    因为如果你要频繁的使用子线程的时候,最好不要频繁的创建和销毁,这样很消耗性能。

    思路就是利用输入源一直传输事件,不让RunLoop休眠。因为如果一个线程任务执行完毕了,RunLoop没有事件源要处理了,RunLoop会随着线程的消亡而消亡。

    #import "ViewController.h"
    #import "JDThread.h"
    
    @interface ViewController ()
    
    @property (nonatomic,strong) JDThread *myThread;
    
    @property (nonatomic,assign) BOOL thread_stop;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        
        self.thread_stop = NO;
        
        [self jd_runloop_alwaysLoop];
        
        // Do any additional setup after loading the view.
    }
    
    - (void)jd_runloop_alwaysLoop
    {
        //上面给了属性是strong,强引用持有,所以给个弱引用
        __weak typeof(self) weakSelf = self;
        
        //创建线程
        self.myThread = [[JDThread alloc] initWithBlock:^{
            
            NSLog(@"开始 : --- 当前线程 : %@",[NSThread currentThread]);
            
            //获取当前线程的RunLoop
            //并且添加一个source/port : 事件源
            //或者也可以添加observer或者timer,总之目的就是维持mode的存在,有事情能做
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            //只要当前控制器还没释放,并且没有让线程停止,那么就一直循环执行一个事件
            while (weakSelf && !weakSelf.thread_stop) {
                //可以用[[NSRunLoop currentRunLoop] run];
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
            
            NSLog(@"结束 : --- 当前线程 : %@",[NSThread currentThread]);
            
        }];
        
        //别忘了开始执行线程
        [self.myThread start];
        
    }
    
    - (void)testRun
    {
        NSLog(@"当前线程 : %@ --- 执行的方法 : %s",[NSThread currentThread],__func__);
    }
    
    - (void)stop_thread
    {
        self.thread_stop = YES;
        //停止线程
        CFRunLoopStop(CFRunLoopGetCurrent());
        NSLog(@"当前线程 : %@ --- 执行的方法 : %s",[NSThread currentThread],__func__);
        self.myThread = nil;
        
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        if (!self.myThread) return;
        
        [self performSelector:@selector(testRun) onThread:self.myThread withObject:nil waitUntilDone:NO];
        
    }
    
    - (void)dealloc
    {
        NSLog(@"当前方法 : %s",__func__);
        if (!self.myThread) return;
        [self performSelector:@selector(stop_thread) onThread:self.myThread withObject:nil waitUntilDone:YES];
    }
    
    
    @end
    
    

    JDThread.m :

    #import "JDThread.h"
    
    @implementation JDThread
    
    - (void)dealloc
    {
        NSLog(@"%s",__func__);
    }
    
    @end
    
    

    二、RunLoop与NSTimer

    1. RunLoop与NSTimer简单实用

    • 前面的文章中,我们已经知道了NSTimer本质上是一个CFRunLoopTimerRef,而它的本质又是指向__CFRunLoopTimer结构体的指针。
    • 与主线程不同,子线程的RunLoop因为不是默认就开启的,所以当我们在子线程上使用NSTimer的时候,需要手动的将NSTimer添加到子线程的RunLoop上。
    • 即便是主线程,根据NSTimer的初始化方法不同,也不是全部都可以直接被加入到主线程的。

    那么NSTimer和RunLoop到底存在着怎样的关系,我们可以通过NSTimer的初始化方法来探索。

    一般情况下,被经常使用的NSTimer的创建方式有两种 :

    • timerWithTimeInterval开头的 :
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    不会直接将timer加入到当前线程,当调用了[timer fire]之后,即使是repeat:YES也仅被调用一次。

    • scheduledTimerWithTimeInterval开头的 :
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    会直接将timer加入到当前的RunLoop中,mode为 : NSDefaultRunLoopMode

    问题 :
    那么经常会出现的问题是在操作UI的时候,如果将timer加入到主线程的RunLoop,并且只将mode设置为 : NSDefaultRunLoopMode,则timer不会执行事件,一直到主线程不再被占用,并且到达了给timer设置的执行时间点的情况下才会调用。

    解决方法 :
    解决的方法是将mode变为 : NSRunLoopCommonModes,因为这个模式是一个集合,包括了NSDefaultRunLoopModeUITrackingRunLoopMode,所以两个mode的事件源都会响应,并且都是在主线程。

    2. 为什么NSRunLoopCommonModes可以解决上面的问题

    看一下Core Foundation的源码,搜索CFRunLoopAddTimer :

    /**
     * @param CFRunLoopRef rl         : 添加timer的runloop对象
     * @param CFRunLoopTimerRef rlt   : timer对象
     * @param CFStringRef modeName    : runloopmode的名字
     **/ 
    void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
        CHECK_FOR_FORK();
    
        //如果要添加timer的Runloop对象已经正在释放了,就不要添加了,直接返回
        if (__CFRunLoopIsDeallocating(rl)) return;
    
        //判断timer对象是否存在,timer关联的runloop是否存在,
        //并且timer当前关联的runloop不能是要添加它的runloop,如果是的话直接返回,不需要添加了
        if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
    
        //给当前的runloop加锁,防止在其他地方操作
        __CFRunLoopLock(rl);
    
        //如果当前要将timer添加到Runloop的commonModes集合下的话
        if (modeName == kCFRunLoopCommonModes) {
            //先判断Runloop对象是否有commonModes集合
            //如果有 : 则直接拿到集合,否则set为NULL
            CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
            //如果RunLoop对象没有commonModesItems
            if (NULL == rl->_commonModeItems) {
                //创建一个RunLoop的commonModes集合
                rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            }
            //将timer添加到RunLoop的commonModeItems集合里面
            CFSetAddValue(rl->_commonModeItems, rlt);
            //如果RunLoop的commonModes集合不为空
            if (NULL != set) {
                //把runloop对象和timer包装成数组
                CFTypeRef context[2] = {rl, rlt};
                //添加一个新的commonModesItems,也就是添加一个新的事件到RunLoop里面
                /* add new item to all common-modes */
                //这里就是遍历commonModes集合,然后对每一个标示(defaultMode和trackingMode)调用
                //第二个参数那个函数,也就是在每一个commonModes的mode对象中都注册了一个timer的事件源
                CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
                CFRelease(set);
            }
        } else {
            //如果timer不是加到commonModes集合下面
    
            //根据传入的mode名字查找runLoop里面对应的mode
            CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
            //如果可以找到这个mode
            if (NULL != rlm) {
                //并且这个mode的timers数组为空
                if (NULL == rlm->_timers) {
    
                    //这是一个存放回调的集合。
                    //存放着当CFArray数组中的元素都是CFType类型时,适用的回调
                    CFArrayCallBacks cb = kCFTypeArrayCallBacks;
                    cb.equal = NULL;
                    //创建一个可变的RunLoopMode的定时器数组
                    rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
                }
            }
    
            //如果RunLoop没有这个mode,并且timer的集合中也不包含这个mode
            if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
                //给timer加个锁
                __CFRunLoopTimerLock(rlt);
    
                //判断如果timer结构中的runloop不存在
                if (NULL == rlt->_runLoop) {
                    //将timer结构中的runloop设置成当前的runloop对象
                    rlt->_runLoop = rl;
                } else if (rl != rlt->_runLoop) {
                    //如果timer结构中的runloop和当前的runloop对象不一样
                    //解锁
                    __CFRunLoopTimerUnlock(rlt);
                    __CFRunLoopModeUnlock(rlm);
                    __CFRunLoopUnlock(rl);
                    //直接返回
                    return;
                }
    
                //给mode中的定时器数组添加这个timer
                CFSetAddValue(rlt->_rlModes, rlm->_name);
                __CFRunLoopTimerUnlock(rlt);
                //执行一次
                __CFRunLoopTimerFireTSRLock();
                __CFRepositionTimerInMode(rlm, rlt, false);
                __CFRunLoopTimerFireTSRUnlock();
                if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
                    // Normally we don't do this on behalf of clients, but for
                    // backwards compatibility due to the change in timer handling...
                    if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
                }
            }
            if (NULL != rlm) {
                __CFRunLoopModeUnlock(rlm);
            }
        }
        __CFRunLoopUnlock(rl);
    }
    

    注释写的很详细了,重点看一下注释,然后你会发现,就算是通过NSRunLoopCommonModes进来,也会有一行代码 : CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);,这句代码看一下第二个参数__CFRunLoopAddItemToCommonModes的源码 :

    static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
        CFStringRef modeName = (CFStringRef)value;
        CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
        CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
        if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
        CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
        } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
        CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
        } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
            //只看这里就够了
        CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
        }
    }
    

    就看我加了注释的那一句,就算你是NSRunLoopCommonModes进来,也会给NSDefaultRunLoopModeUITrackingRunLoopMode全部都添加timer事件源。
    所以,NSRunLoopCommonModes下,commonModes里面的所有元素都会有timer事件源,也就都会执行NSTimer事件。

    相关文章

      网友评论

          本文标题:第二十八节—RunLoop(三)

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