孙源的Runloop视频整理

作者: KevinTing | 来源:发表于2017-02-19 19:32 被阅读1511次

    视频地址:http://v.youku.com/v_show/id_XODgxODkzODI0.html

    1、Runloop是什么东西?

    普通的命令式执行如下所示,程序顺序执行代码,执行完了就结束了:

    int main(int argc, char *argv[]) {
        NSLog(@"hello world!");
        return 0;
    }
    

    Runloop就是一个循环,跑圈,一个死循环,程序会一直运行并不会退出,如下面的Event驱动,平时处于睡眠状态,如果有Event唤醒了,那么就执行事件Event。

    int main(int argc, char *argv[]) {
        while (AppIsRunning) {
            id whoWakesMe = SleepForWakingUp();
            id event = GetEvent(whoWakesMe);
            HandleEvent(event);
        }
        return 0;
    }
    

    2、Runloop的作用

    为什么要有Runloop呢?Runloop主要由以下几个方面的作用:
    1、使程序一直运行并接受用户输入:我们的app必然不能像命令式执行一样,执行完就退出了,我们需要app在我们不杀死它的时候一直运行着,并在由用户事件的时候能够响应,比如网络输入,用户点击等等,这是Runloop的首要任务;
    2、决定程序在何时应该处理哪些事件:实际上程序会有很多事件,Runloop会有一定的机制来管理时间的处理时机等;
    3、调用解耦(Message Queue):比方说手指点击滑动会产生UIEvent事件,对于主调方来说,我不可能等到这个事件被执行了才去产生下一个事件,也就是主调方不能被被调方卡住。那么在实际实现中,被调方会有一个消息队列,主调方会把消息扔到消息队列中,然后不管了,消息的处理是由被调方不断去从消息队列中取消息,然后执行的。这样的好处是主调方不需要知道消息是具体是怎么执行的,只要产生消息即可,从而实现了解耦;
    4、节省CPU时间:在app没设可干的时候,让CPU闲着。
    Runloop机制并不是iOS特有的,Android和Windows上面都有,只要有这种需要接受事件的程序都有这种实现机制,只是其他平台上面不叫Runloop而已。

    3、Runloop的封装结构

    如下图所示是NSRunloop的实现:


    Paste_Image.png

    1、NSRunloop:最上层的NSRunloop层实际上是对C语言实现的CFRunloop的一个封装,实际上它没干什么事,比如CFRunloop有一个过期时间是double类型,NSRunloop把它变味了NSDate类型;
    2、CFRunloop:这是真正干事的一层,源代码是开源的,并且是跨平台的;
    3、系统层:底层实现用到了GCD,mach kernel是苹果的内核,比如runloop的睡眠和唤醒就是用mach kernel来实现的。
    下面是跟Runloop有关的,我们平时用到的一些模块,功能等等:
    1)NSTimer计时器;
    2)UIEvent事件;
    3)Autorelease机制;
    4)NSObject(NSDelayedPerforming):比如这些方法:performSelector:withObject:afterDelay:,performSelector:withObject:afterDelay:inModes:,cancelPreviousPerformRequestsWithTarget:selector:object:等方法都是和Runloop有关的;
    5)NSObject(NSThreadPerformAddition):比如这些方法:performSelectorInBackground:withObject:,performSelectorOnMainThread:withObject:waitUntilDone:,performSelector:onThread:withObject:waitUntilDone:等方法都是和Runloop有关的;
    4、Core Animation层的一些东西:CADisplayLink,CATransition,CAAnimation等;
    5、dispatch_get_main_queue();
    6、NSURLConnection;

    4、从调用堆栈来看Runloop

    如下图是常见的调用堆栈:

    Paste_Image.png

    从下往上一层层的看,最开始的start是dyld干的,然后是main函数,main函数接着调用UIApplicationMain,然后的GSEventRunModal是Graphics Services是处理硬件输入,比如点击,所有的UI事件都是它发出来的。紧接着的就是Runloop了,从图中的可以看到从13到104个调用都是Runloop相关的。再上面的就是事件队列处理,以及UI层的事件分发了。

    5、Runloop的调用

    几乎所有线程的所有函数都是从下面六个函数之一调起:

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();  
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();  
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();  
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();  
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();  
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
    

    6、Runloop的构成

    Paste_Image.png
    1、Runloop与Thread是一一绑定的,但是并不是一个Thread只能起一个Runloop,它可以起很多,但是必须是嵌套结构,根Runloop只有一个;
    2、RunloopMode是指的一个事件循环必须在某种模式下跑,系统会预定义几个模式。一个Runloop有多个Mode;
    3、CFRunloopSource,CFRunloopTimer,CFRunloopObserver这些元素是在Mode里面的,Mode与这些元素的对应关系也是1对多的;
    CFRunloopTimer:比如下面的方法都是CFRunloopTimer的封装: Paste_Image.png

    CFRunloopSource:source是RunLoop的数据源(输入源)的抽象类(protocol),Runloop定义了两个Version的Source:
    1、Source0:处理App内部事件,App自己负责管理(触发),如UIEvent,CFSocket;
    2、Source1:由Runloop和内核管理,mach port驱动,如CFMachPort(轻量级的进程间通信的方式,NSPort就是对它的封装,还有Runloop的睡眠和唤醒就是通过它来做的),CFMessagePort;
    CFRunloopObserver:这个是用来向外界报告Runloop当前的状态的更改。

    kCFRunLoopEntry = (1UL << 0),// 即将进入Loop  
    kCFRunLoopBeforeTimers = (1UL << 1),// 即将处理 Timer  
    kCFRunLoopBeforeSources = (1UL << 2),// 即将处理 Source  
    kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠  
    kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒  
    kCFRunLoopExit = (1UL << 7),// 即将退出Loop  
    kCFRunLoopAllActivities = 0x0FFFFFFFU//所有状态
    

    我们可以去设自己的Observer,来观察Runloop的状态变化。很多机制由RunloopObserver来触发,比如CAAnimation,动画并不是马上就会被调用的,而是会等到kCFRunLoopBeforeWaiting或者kCFRunLoopAfterWaiting来执行,会等到Runloop执行一圈,收集所有的Animation之后一起来调用。
    ** CFRunloopObserver与Autorelease Pool

    Paste_Image.png
    ** CFRunloopMode
    :Runloop在同一时间只能且必须在某一种特定的Mode下面Run,更换Mode时,必须要停止当前的Loop,然后重启新的Loop,重启的意思是退出当前的while循环,然后重新设置一个新的while。Mode是iOS App滑动流畅的关键,我们也可以自己创建一个Mode,但是基本不会这样去做。NSDefaultRunLoopMode:默认状态,空闲状态,点击事件,普通的回调等;NSTrackingRunLoopMode:ScrollView滑动时;UIInitializationRunLoopMode:私有的,App启动的时候,第一个页面加载之后就切换为NSDefaultRunLoopMode了,避免启动的时候受到影响;NSRunLoopCommonModes:这个mode包含第一个和第二个,都可以跑。
    ScrollView滑动过程:下面看看scrollView在开始滑动和停止滑动时候的调用堆栈,设置符号断点CFRunLoopWakeUp,查看主线程的调用堆栈,如下所示:
    Paste_Image.png Paste_Image.png
    查看调用堆栈可以看到开始滑动的时候有一个pushRunLoopMode方法的调用,在停止滑动的时候有一个pushRunLoopMode方法的调用,实际上Mode的切换过程是这样的:
    NSDefaultRunLoopMode -> UITrakingRunLoopMode -> NSDefaultRunLoopMode
    RunLoop和dispatch_get_main_queue():GCD中分发到main queue中的block辈分发到main runloop执行,这是main queue的特别之处,dispatch_after到main queue同理:
    Paste_Image.png

    5、RunLoop的挂起和唤醒

    空闲时刻暂停程序时(按下debug的暂停键),会看到如下的堆栈:


    Paste_Image.png

    这就是Runloop的挂起,实际上是指定一个端口,给内核发消息,这里是一个等待消息,就是等待被唤醒。
    1、指定用于被唤醒的mach port端口;
    2、调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态;
    3、由另一个线程(或者另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。

    6、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);//如果没被外部干掉或者时间没到,继续循环
    

    跑while循环之前需要通过GCD来设置过期时间,不然真成死循环了;然后告诉observer要开始执行timer和source的状态了;然后遍历消息队列中source0的消息并执行;然后询问GCD中有没有分到主线程的任务需要执行;然后通知要进入睡眠挂起状态了;然后会一直卡在SleepAndWaitForWakingUpPorts函数这里,直到有事件唤醒并返回唤醒的端口;然后通知我要醒了;然后根据唤醒端口类型来进行相应的处理;

    7、RunLoop实践

    这是AFNetworking中的Runloop的创建代码

    Paste_Image.png
    所以说currentRunLoop方法是获得RunLoop,如果没有就会创建RunLoop,后面一行代码是为了让线程活下来,如果没有这一行代码,RunLoop并不会挂起,线程运行完就会退出;这是创建一个常驻服务线程得很好的样例代码。
    TableView延迟加载图片的新思路
    将setImage放到NSDefaultRunLoopMode去做,也就是在滑动的时候并不会去调用这个方法,而是会等到滑动完毕切换到NSDefaultRunLoopMode下面才会调用。
    UIImage *downLoadImage = ...;  
    [self.avatarImageView performSelector:@selector(setImage:)  
                            withObject:downloadImage  
                            afterDelay:0  
                            inModes:@[NSDefaultRunLoopMode]];
    

    让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);  
            }  
        }
    

    相关文章

      网友评论

      • jiaxw32:Runloop的封装结构一节插图中,Foundation都拼错了。。。

      本文标题:孙源的Runloop视频整理

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