runloop

作者: 李波小丑 | 来源:发表于2017-02-24 09:05 被阅读0次

    前言:先查看苹果的API,搞懂其作用,这是通往资深的一条路
    runloop,管理事件/消息,让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源

    runloop.png

    在runloop中,需要处理的事件分两种,一种是输入源,一种是定时器。
    输入源分三类:performSelector源,基于端口(Mach port)的源,以及自定义的源

    runloop与线程

    apple不允许直接创建runloop。但提供了两个自动获取的函数

    CFRunLoopGetMain()     [NSRunLoop mainRunLoop];
    CFRunLoopGetCurrent()  [NSRunLoop currentRunLoop];
    
    // class 可以用类实例方法的形式获取(此处需纠正)
    @property (class, readonly, strong) NSRunLoop *mainRunLoop
    

    这两个函数内部的大概逻辑

    //全局的dictionary,key是pthread_t(线程),value是CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    //访问loopDic时的锁
    static CFSpinLock_t loopsLock;
    
    // 获取一个pthread对应的RunLoop
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
        OSSpinLockLock(&loopsLock);
        if (!loopsDic) {
            // 第一次进入时,初始化全局dic,并先为主线程创建一个Runloop
            loopDic = CFDictionaryCreateMutable();
            CFRunLoopRef mainLoop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop)
        }
        CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread);
        if (!loop) {
            loop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, thread, loop);
            // 注册一个回调,当线程销毁时,顺便也销毁其对应的RunLoop
            _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
        }
        OSSpinLockUnLock(&loopsLock);
        return loop;
    }
    
    CFRunLoopRef CFRunLoopGetMain(){
        return _CFRunLoopGet(pthread_main_thread_np());
    }
    
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    

    从上面的代码可以看出,线程和runloop之间是一一对应的,其关系是保存在一个全局的dictionary里。线程刚创建时并没有runloop,如果你不主动获取,那它一直都不会有。runloop的创建发生在第一次获取时,runloop的销毁是发生在线程结束时,你只能在一个线程的内部获取其runloop(主线程除外)

    runloop对外的接口

    一个runloop包含若干个mode,每个mode又包含若干个Source/Timer/Observer.每次调用runloop的主函数时,只能指定其中一个mode,这个mode被称为CurrentMode.如果要切换mode,只能退出runloop,再重新指定一个mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
    上面的Source/Timer/Observer被统称为mode item,一个 item可以被同时加入多个mode,如果一个mode中一个item都没有,则runloop会直接退出,不进入循环。

    这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
    应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
    有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

    kCFRunLoopDefaultMode app的默认mode,通常主线程是在这个mode下运行
    UITrackingRunLoopMode 界面跟踪mode,用于scrollview追踪触摸滑动,保证界面滑动时不受其他mode影响

    线程启动

    线程启动之后,就进入三个状态中的一种:运行(running)、就绪(ready)、阻塞(blocked)

    线程间的通信

    Cocoa为ios线程间通信提供了2中方式
    1.performSelector

    @interface NSObject (NSThreadPerformAdditions)
    
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
        // equivalent to the first method with kCFRunLoopCommonModes
    
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
        // equivalent to the first method with kCFRunLoopCommonModes
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
    
    @end·
    

    2.NSMachPort
    NSPort有三个子类 NSSocketPot NSMessagePort NSMachPort 但在ios下只有NSMachPort可用,
    使用的方式为接收线程中注册NSMachPort,在另外的线程中使用此port发送消息,则被注册线程会收到相应消息,然后最终在主线程里调用某个回调函数。可以看到,使用NSMachPort的结果为调用了其它线程的1个函数,而这正是performSelector所做的事情,所以,NSMachPort是个鸡肋。线程间通信应该都通过performSelector来搞定

    AutoreleasePool

    在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    PerformSelecter
    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    如何保证子线程不退出?
    开启runloop,单纯开始runloop还不行,还需调用addPort方法
    AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

    // AFNetWorking 2.0
    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    
    + (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
    

    runloop启动前,内部必须要有至少一个 Source/Timer/Observer,所以AFNetworking在[runloop run]之前先创建了一个新的NSMachPort添加去了。通常情况下,调用者需要持有这个NSMachPort并在外部线程通过这个port发送消息到loop内;但此处添加port只是为了让runloop不至于退出,并有用于实际的发送消息

    - (void)start {
        [self.lock lock];
        if ([self isCancelled]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        } else if ([self isReady]) {
            self.state = AFOperationExecutingState;
            [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
        [self.lock unlock];
    }
    
    

    当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

    每一个线程都有其对应的RunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后去run它。一般情况下我们是没有必要去启用线程的RunLoop的,除非你在一个单独的线程中需要长久的检测某个事件。

    每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。

    每个线程(包含主线程)都有一个Runloop。对于每一个Runloop,系统会隐式创建一个Autorelease pool,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。

    相关文章

      网友评论

          本文标题:runloop

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