第二十八节—RunLoop(三)

2020-11-19  本文已影响0人  L_Ares

本文为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的创建方式有两种 :

+ (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也仅被调用一次。

+ (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事件。

上一篇下一篇

猜你喜欢

热点阅读