孙源的Runloop视频整理

96
KevinTing
1.0 2017.02.19 19:32* 字数 1759

视频地址: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);  
        }  
    }
iOS开发