美文网首页
iOS RunLoop知识整理

iOS RunLoop知识整理

作者: Fisland_枫 | 来源:发表于2018-12-18 00:32 被阅读48次

    [TOC]

    为什么是RunLoop

    因为iOS是事件驱动,类似需要一个死循环在底下跑着,没事就闲着,有事才唤醒干活。

    1. 使程序一直活着,并接受用户输入
    2. 决定程序在何时应该处理哪些事件
    3. 调用解耦(Message Queue)
    4. 节省CPU时间

    Run Loop in Cocoa

    1. Foundation
      1. NSRunLoop
    2. Core Foundation
      1. CFRunLoop

    System: GCD(有点不一样),mach kernel,Block,Pthread...


    结构

    六个被调起方法

    主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:

    1. CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
    • CFRunloop is calling out to an abserver callback function

    • 用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation

    1. CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
    • CFRunloop is calling out to a block

    • 消息通知、非延迟的perform、dispatch调用、block回调、KVO

    1. CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    • CFRunloop is servicing the main desipatch queue
    1. CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
    • CFRunloop is calling out to a timer callback function

    • 延迟的perform, 延迟dispatch调用

    1. CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
    • CFRunloop is calling out to a source 0 perform function

    • 处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用

    1. CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
    • CFRunloop is calling out to a source 1 perform function

    • 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort

    (lldb) bt
    // 例子 打印iOS在普通状态下直接点击暂停
    CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
        frame #29: 0x00000001bc08414c CoreFoundation`__CFRunLoopDoSource0 + 88
        frame #30: 0x00000001bc083a30 CoreFoundation`__CFRunLoopDoSources0 + 176
        frame #31: 0x00000001bc07e8fc CoreFoundation`__CFRunLoopRun + 1040
        frame #32: 0x00000001bc07e1cc CoreFoundation`CFRunLoopRunSpecific + 436
        frame #33: 0x00000001be2f5584 GraphicsServices`GSEventRunModal + 100
        frame #34: 0x00000001e9179054 UIKitCore`UIApplicationMain + 212
        frame #35: 0x0000000102cba7a0 testcopy`main(argc=1, argv=0x000000016d14b970) at main.m:14
        frame #36: 0x00000001bbb3ebb4 libdyld.dylib`start + 4
    

    Run Loop的构成

    Run Loop的构成

    RunLoopTimer

    RunLoopTimer的封装

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
    
    /* Create a new display link object for the main display. It will
     * invoke the method called 'sel' on 'target', the method has the
     * signature '(void)selector:(CADisplayLink *)sender'. */
    + (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
    
    /* Adds the receiver to the given run-loop and mode. Unless paused, it
     * will fire every vsync until removed. Each object may only be added
     * to a single run-loop, but it may be added in multiple modes at once.
     * While added to a run-loop it will implicitly be retained. */
    - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
    

    source

    • Source是RunLoop的数据源抽象类(protocol)
    • RunLoop定义了两个Version的Source;
      1. Source0:处理App内部事件,App自己负责管理(触发),如UIevent、CFSocket
      2. Source1:由RunLoop和内核管理,Mach port驱动,如CFMachport和CFMessagePort

    Observer

    1. kCFRunLoopEntry -- 进入runloop循环
    2. kCFRunLoopBeforeTimers -- 处理定时调用前回调
    3. kCFRunLoopBeforeSources -- 处理input sources的事件
    4. kCFRunLoopBeforeWaiting -- runloop睡眠前调用
    5. kCFRunLoopAfterWaiting -- runloop唤醒后调用
    6. kCFRunLoopExit -- 退出runloop
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    
    

    知识点

    Observer 与autoreleasePool

    UIKit通过RunLoopObserver在RunLoop在两次Sleep之间对AutoReleasePool进行Pop和Push,将这次Loop中产生的AutoRelease对象释放

    autoreleasePool堆栈信息

    App启动之后,系统启动主线程并创建了RunLoop,在 main thread 中注册了两个 observer ,回调都是_wrapRunLoopWithAutoreleasePoolHandler()

    1. 第一个observer监听了一个事件:
      1. 即将进入Loop(kCFRunLoopEntry)其回调会调用 _objc_autoreleasePoolPush() 创建一个栈自动释放池,这个优先级最高,保证创建释放池在其他操作之前。
    2. 第二个observer监听了两个事件:
      1. 准备进入休眠(kCFRunLoopBeforeWaiting)此时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 来释放旧的池并创建新的池。
      2. 即将退出Loop(kCFRunLoopExit)此时调用 _objc_autoreleasePoolPop()释放自动释放池。这个 observer 的优先级最低,确保池子释放在所有回调之后。

    在主线程中执行代码一般都是写在事件回调或Timer回调中的,这些回调都被加入了main thread的自动释放池中,所以在ARC模式下我们不用关心对象什么时候释放,也不用去创建和管理pool。(如果事件不在主线程中要注意创建自动释放池,否则可能会出现内存泄漏)。

    RunLoop的挂起与唤醒

    挂起与唤醒

    指定用于唤醒的mach_port端口
    调用mach_port监听唤醒端口,被唤醒前系统内核,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态
    由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作

    CFRunloopMode

    特点

    • RunLoop在同一段时间只能且必须在一种特定的Mode下run
    • 更换Mode时,需要停止当前Loop,然后重启Loop
    • Mode是iOS App流畅的关键

    RunLoop的mode类型

    • NSDefauleRunLoopMode :默认状态,不滑动,空闲状态下程序就会自动切换到这个mode
    • UITrackingRunLoopMode :滑动状态
    • UIInitializationRunLoopMode :私有的,可以追踪的,app启动的时候就是这个状态,第一个页面加载之后才回到defaultMode
    • NSRunLoopCommonModes:默认状态下包括NSDefauleRunLoopMode和UITrackingRunLoopMode

    定时器与scrollview卡顿问题

    默认定时器是加到NSDefaultRunLoopMode中的,而scrollview滑动的时候是在UITrackingRunLoopMode。

        [NSTimer scheduledTimerWithTimeInterval:1.0
                                        repeats:YES
                                          block:^(NSTimer * _Nonnull timer) {
            //do something
        }];
    

    如果不希望Timer被scrollview的滑动影响,需要添加到NSRunLoopCommonModes下就可以了。

        NSTimer *timer =  [NSTimer scheduledTimerWithTimeInterval:1.0
                                        repeats:YES
                                          block:^(NSTimer * _Nonnull timer) {
            //do something
        }];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    RunLoop与GCD

    dispatch_get_main_queue()

    GCD中dispatch到main queue的block被分发到main RunLoop执行

    //关键堆栈--->_dispatch_main_queue_callback_,__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    * frame #0: 0x0000000104cd6690 testcopy`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000104cd80b0) at ViewController.m:37
    frame #1: 0x00000001050b3824 libdispatch.dylib`_dispatch_call_block_and_release + 24
    frame #2: 0x00000001050b4dc8 libdispatch.dylib`_dispatch_client_callout + 16
    frame #3: 0x00000001050c2a78 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1360
    frame #4: 0x00000001bc083dd0 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
    frame #5: 0x00000001bc07ec98 CoreFoundation`__CFRunLoopRun + 1964
    frame #6: 0x00000001bc07e1cc CoreFoundation`CFRunLoopRunSpecific + 436
    frame #7: 0x00000001be2f5584 GraphicsServices`GSEventRunModal + 100
    frame #8: 0x00000001e9179054 UIKitCore`UIApplicationMain + 212
    frame #9: 0x0000000104cd6758 testcopy`main(argc=1, argv=0x000000016b12f970) at main.m:14
    frame #10: 0x00000001bbb3ebb4 libdyld.dylib`start + 4
    

    RunLoop迭代执行顺序

    //设定过期时间  
    SetupThisRunLoopRunTimeOutTimer();  //by GCD timer  
    do{  
        //通知Observer要跑timer跟source  
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);  
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);  
           
        __CFRunLoopDoBlocks();  
        //运行到此刻,去检测当前加到消息队列source0的消息,此方法遍历source0去执行  
        __CFRunLoopDoSource0();  
           
        //询问GCD有没有分到主线程的东西需要调用  
        CheckIfExistMessageInMainDispatchQueue();   //GCD  
           
        //通知Observer要进入睡眠  
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);  
        //此刻获取到是哪个端口把我叫醒  
        var wakeUpPort = SleepAndWaitForWakingUpPorts();  
        //  mach_msg_trap  
        //  Zzz...  
        //  Received mach_msg,  wake up!  
           
        //通知Observer我要醒了~  
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);  
        //Handler msgs  
        if(wakeUpPort == timerPort){  
            //如果是timer唤醒就去执行timer  
            __CFRunLoopDoTimer();  
        }else if(wakeUpPort == mainDispatchQueuePort){  
            //GCD需要我,就去调GCD的事件  
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();  
        }else{  
            //比如说网络来数据了就会用这个端口唤醒,然后做数据处理  
            __CFRunloopDoSource1();  
        }  
        __CFRunLoopDoBlocks();  
    }while (!stop && !timeOut);//如果没被外部干掉或者时间没到,继续循环
    

    过程思路

    1. 跑while循环之前需要设置GCD来设置时间,不然会成为死循环
    2. 然后告诉Observer要跑timer和source
    3. 然后遍历消息队列中的source0的消息并执行
    4. 询问GCD有没有分到主线程的东西需要调用
    5. 通知进入睡眠挂起状态
    6. 然后卡在函数SleepAndWaitForWakingUpPorts这里直至被唤醒
    7. 通知被唤醒,根据端口类型去执行处理的事件


      过程思路

    RunLoop实践

    RunLoop与AFNetworking

    //旧版2.6之前,使用AFURLConnectionOperation类,自己创建线程并添加RunLoop
    + (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;
    }
    

    Topic: Tableview卡顿与RunLoop

    思路:将需要的耗时动作类似图片下载放到RunLoop中defaultRunLoopMode中处理,因为滑动是在UItrackMode下的,就不会在滑动的线程下下载,只有滑动完毕回到defaultRunLoopMode下才会调用

    UIImage *downLoadImage = ...;  
    [self.avatarImageView performSelector:@selector(setImage:)  
                            withObject:downloadImage  
                            afterDelay:0  
                            inModes:@[NSDefaultRunLoopMode]];
    

    Topic: 让Crash的app回光返照

    1. program received signal:SIGABRT SIGABRT一般是过度release或者发送unrecogized selector导致。
    2. EXC_BAD_ACCESS是访问已被释放的内存导致,野指针错误。
      由 SIGABRT 引起的Crash 是系统发这个signal给App,程序收到这个signal后,就会把主线程的RunLoop杀死,程序就Crash了 该例只针对 SIGABRT引起的Crash有效。
    CFRunLoopRef runloop = CFRunLoopGetCurrent();  
        //获取所有Mode,因为可能有很多Mode,每个Mode都需要跑,此处可以选择提交下崩溃信息之类的  
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩溃了" message:@"崩溃信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];  
        [alertView show];  
        NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));  
        while (1) {  
            //快速切换Mode  
            for (NSString *mode in allModes) {  
                CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);  
            }  
        }
    

    资料来源
    孙源的Runloop视频整理
    iOS线下分享Runloop--孙源
    IOS开发日志之RunLoop的原理和使用
    RunLoop的前世今生

    相关文章

      网友评论

          本文标题:iOS RunLoop知识整理

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