iOS RunLoop知识整理

[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...


iOS RunLoop知识整理_第1张图片
结构

六个被调起方法

主线程 (有 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的构成

iOS RunLoop知识整理_第2张图片
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 *)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对象释放

iOS RunLoop知识整理_第3张图片
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的挂起与唤醒

iOS RunLoop知识整理_第4张图片
挂起与唤醒

指定用于唤醒的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. 通知被唤醒,根据端口类型去执行处理的事件


    iOS RunLoop知识整理_第5张图片
    过程思路

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知识整理)