孙源的Runloop视频整理

视频地址: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);  
        }  
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容