RunLoop

作者: 堂吉诃德灬 | 来源:发表于2017-07-23 10:52 被阅读58次

    1.RunLoop的概念

    RunLoop其实就是一个大的do while循环,它的关键点在于如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。所以RunLoop实际上是一个对象,这个对象管理了其需要处理的时间和消息,并提供了一个函数来执行上面的事件逻辑。因此runLoop可以说就是为了线程而生。

    2.RunLoop 的作用:

    1.使程序一直运行并接受用户输入
    2.决定程序在何时应该处理哪些事件
    3.调用解耦(事件队列的分发与派放)
    4.节省CPU时间
    5.RunLoop也负责autorelease pool的创建与释放(当一个运行循环结束或者RunLoop退出和休眠的时候,它都会释放一次autorelease pool)

    其中RunLoop运行一次的时间为1/60 S

    与Runloop最密切相关的:NSTimer 、UIEvent 、Autorelease

    CFRunLoop是基于pthread来管理的。苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数CFRunLoopGetMain() 和 CFRunLoopGetCurrent(),当线程中没有RunLoop的时候,CFRunLoopGetCurrent()其实会创建一个RunLoop对象并返回。

    3.RunLoop的结构

    RunLoop接受事件来自两种不同的来源:输入源和定时源,输入源传递异步事件,通常消息来自其他线程和程序,定时源则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定处理例程来处理到达的事件。输入源包括两种,分别是基于端口的输入源和自定义输入源。基于端口的输入源监听程序相应的输入源,自定义输入源则监听自定义的事件源。基于端口的输入源由内核发送,而自定义的输入源需要人工从其他线程发送。

    RunLoop由线程和Mode组成,线程和RunLoop之间是一一对应的关系,其关系是保存在一个字典里面,线程刚创建时并没有RunLoop,如果你不主动获取那么它一直不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。主线程的RunLoop默认是开启的,当程序在运行的时候会产生大量的对象,这些对象存储在RunLoop的释放池里面,当RunLoop循环完一次之后会释放自动释放池同时创建新的自动释放池。子线程没有开启RunLoop需要手动获取,因为子线程的RunLoop是手动获取的,所以自动释放池默认也没有,当我们在子线程里面创建了大量的临时对象的时候就需要创建自动释放池。

    一个RunLoop包含若干个RunLoopMode,但是一个RunLoop每次只能加入一种Mode,每一个Mode里面包含若干个source/timer/observer。每次调用RunLoop的主函数时只能指定其中一个Mode,这个Mode被称为currentMode,如果需要切换Mode只能退出RunLoop然后再重新指定另外的Mode进入。这样做主要是为了分开不同组的source/timer/observer让其不受影响。

    Run loop模式是所有要监视的输入源和定时源以及要通知的run loop注册观察者的集合。每次运行你的run loop,你都要指定(无论显示还是隐式)其运行个模式。在run loop运行过程中,只有和模式相关的源才会被监视并允许他们传递事件消息。(类似的,只有和模式相关的观察者会通知run loop的进程)。和其他模式关联的源只有在run loop运行在其模式下才会运行,否则处于暂停状态。

    4.RunLoop的特点

    RunLoop在同一段时间只能且必须在一种特定的Mode下run
    更换Mode时,需要停止当前Loop,然后重启Loop
    Mode是iOS App滑动顺畅的关键
    当传入一个新的mode name 但是RunLoop内部没有对应的mode时,RunLoop会帮你创建对应的RunLoopMode

    5. RunLoopSource

    CFRunLoopSourceRef 是事件产生的地方。Souce是RunLoop的数据抽象类。Source有两个版本:Source0 和 Source1。
    · Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。(Souce0处理App内部事件,App自己负责管理触发,如UIEvent,CFSocket)
    · Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。(Souce1由RunLoop和内核管理,如CFMach,CFMessage)

    其实可以简单的理解为RunLoop通常处理的事件源有两大种类,分别是time souce和input source,input source是异步消息通常来自其他线程或者程序。time source是timer中的同步事件

    6.RunLoopTimer

    CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。(NSTimer受RunLoop的Mode影响,GCD的定时器不受RunLoop的Mode影响)

    7.RunLoopObserver

    CFRunLoopObserverRef 是观察者,(它向外部报告RunLoop当前状态的更改)每个 Observer 都包含了一个回调(函数指针),当 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
    };

    上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

    UIKit通过RunLoopObserver在RunLoop两次Sleep间对AutreleasePool进行push和pop,将这次Loop中产生的Autorelease对象释放。

    8.CFRunLoopMode 和 CFRunLoop 的结构

    CFRunLoopMode 和 CFRunLoop 的结构大致如下:
    struct __CFRunLoopMode {
    CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0; // Set
    CFMutableSetRef _sources1; // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers; // Array
    ...
    };

    struct __CFRunLoop {
    CFMutableSetRef _commonModes; // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode; // Current Runloop Mode
    CFMutableSetRef _modes; // Set
    ...
    };
    从上面可以看出CFRunLoop里面有一个commonModes,它是一个set集合。系统中共有5个Mode,每一个Mode可以将自己标记为Common属性(通过将其ModeName属性添加到RunLoop的commonModes中)。每当RunLoop的内部发生变化时,RunLoop都会将commonModeItems里面的Souce/Observer/Timer同步到具有Common标记的所有Mode里。

    系统默认注册了5个Mode(前两个跟最后一个常用)
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行(NSTimer scheduledTimerWithTime这个方法默认加入的就是KCFRunLoopDefaultMode)
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    • kCFRunLoopCommonModes:这个Mode其实包含了第一个和第二个Mode

    GCD中的任务队列被分配到main queue的block会被分发到main RunLoop中执行。

    当RunLoop挂起的时候会指定用于唤醒mach_port的端口。同时会调用mach_msg监听唤醒端口,被唤醒前,系统会将这个线程挂起,停留在mach_msg_trap状态

    由另一个线程或者另一个进程中的某个线程向内核发送这个端口的msg后,trap状态被唤醒,runLoop继续开始干活

    CFRunLoop的默认超时时间很长

    AutoreleasePool
    App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

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

    9.定时器与RunLoop的关系:

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

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

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    10.何时使用RunLoop

    我们知道当我们的程序启动的时候,主线程已经默认创建了一个runLoop,所以只有在二级线程中我们才有机会创建runLoop。RunLoop的主要作用是为了帮助线程常驻进程,所以仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个run loop。对于辅助线程,你需要判断一个run loop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的run loop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动run loop。Run loop在你要和线程有更多的交互时才需要,比如以下情况:
    使用端口或自定义输入源来和其他线程通信
    在线程中执行定时事件源的任务
    Cocoa中使用任何performSelector...的方法
    在线程中执行较为频繁的,周期性的任务
    如果你决定在程序中使用run loop,那么它的配置和启动都很简单。和所有线程编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。

    11.线程保活

    在AFN中,把网络的请求和解析都放在了一个子线程中,就是下面这段代码

    + (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;
    }
    
    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
    
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    

    这段代码用单例创建了一个线程同时将线程加入了RunLoop中,这是AFN中用来线程保活的方法。这里为什么要加入RunLoop是因为我们创建的线程是脱离线程,默认在执行完任务之后就会被系统回收,为了让线程一直存活下去必须让它加入RunLoop.至于RunLoop为什么要调用addport forMode方法是因为如果RunLoop里面没有任何的modelItem的话,RunLoop会直接退出。

    我们可以试着来仿照AFN中的线程保活来仿写一段代码:

    -(void)threadTest
    {
        for (int i = 0; i < 100000; i ++) {
            NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
            [thread start];
        }
    }
    
    -(void)addToRunLoop
    {
        NSLog(@"test");
        [[NSThread currentThread]setName:@"test"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
    

    当我们运行程序的时候我们会发现,内存在不断的上涨,同时控制台会输出[NSThread start]: Thread creation failed with error 35这个错误。我们尝试把addToRunLoop这个方法里面的代码封掉,发现程序运行正常并且内存并不会一直上涨,那么可以猜测,是因为线程加入了RunLoop导致了线程不能销毁因此内存上涨。

    那么我们取消RunLoop和线程,那么看看会有什么变化呢:

    -(void)threadTest
    {
        for (int i = 0; i < 1000000; i ++) {
            NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
            [thread start];
            [self performSelector:@selector(stopRunLoopAndThread) onThread:thread withObject:nil waitUntilDone:YES];
        }
    }
    -(void)addToRunLoop
    {
        NSLog(@"test");
        [[NSThread currentThread]setName:@"test"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
    -(void)stopRunLoopAndThread
    {
        CFRunLoopStop(CFRunLoopGetCurrent());
        NSThread *thread = [NSThread currentThread];
        [thread cancel];
    }
    

    运行程序发现内存还是在增长,并且控制台也会输出[NSThread start]: Thread creation failed with error 35这个错误,看来我们没有正确的取消RunLoop。

    RunLoop的启动方式:
    (1)run (直接进入,但会使线程进入死循环从而不利于控制RunLoop,结束RunLoop的唯一方式就是kill它)

    (2)runUntilDate(RunLoop会在处理完事件或者超时时间后结束)

    (3)runMode:beforeDate: (指定RunLoop的超时时间以及运行在何种模式下)

    runMode:beforeDate:是单次调用,其他两种是循环调用runMode:beforeDate:方法。

    相关文章

      网友评论

          本文标题:RunLoop

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