RunLoop

RunLoop的核心,主要是涉及到用户态和内核态的切换(mach_msg())。

基本作用

保持程序运行(main()的UIApplicationMain函数中会启动主线程的Runloop)

处理事件(触摸、Timer)

节省CPU资源,提高性能(切换到内核态,休眠线程,等待事件/消息)


CFRunloopRef 对象

typedef struct __CFRunloop *CFRunLoopRef;

__CFRunloop属性:pthread-与线程一一对应;commonModes-模式名称的字符串集合;commonModeItems-通用模式下的事件源:Source01、Timer、Observer(Items一个都没有则直接退出Runloop);currentMode-当前模式;modes-Runloop中所有mode(default,tracking,common等)

Runloop对象的获取

_CFRunLoopGetMain & _CFRunLoopGetCurrent

__CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&lock) 自旋锁; 

第一次进入,初始化loopsDic,并为主线程创建一个Runloop(_CFRunLoopCreate()); 

CFDictionaryGetValue(loopsDic,thread),根据thread获取CFRunloopRef;

取不到就创建一个,CFDictionarySetValue,并注册回调销毁线程时顺便销毁对应的Runloop;

OSSpinUnLock(&lock)自旋锁结束;}


CFRunLoopModeRef 模式

一个Runloop包含几个mode(但每次只能启动一个,所以互不影响),一个mode包含name,source0/1,timer、observer等若干事件源和监听,mode中没有事件源Runloop会马上退出。

tracking下不去处理default中的source,做到了屏蔽效果,保证了trackingmode下,滚动的顺畅。

CommonModes:所有mode都可以标记为Common,每当Runloop内容发生变化时,自动将_commonModeItems同步到所有带有标记的Mode下(预置的default和track已被标记为common)。

例:把timer加入CommonModeItems自动更新同步到default和track,解决滚动时NSTimer不回调的问题。

管理mode只有两个方法:addCommonMode(只能增加不能删除)和RunInMode。


mode中的事件源和活动状态

CFRunLoopSourceRef 输入源(结构体union中的version0/1分别对应Source0/1)

Source0,只有一个回调指针(添加0到Runloop不会自动唤醒线程需手动wakeup,如触摸事件?、performSeletor: thread等);

Source1,有一个mach-port和一个回调指针(基于Port的线程通信、系统事件捕捉)

CFRunLoopTimerRef 定时器源

和NSTimer是toll-free-bridge桥接的,包含一个时间长度和一个回调,加入到Runloop时会注册对应的时间点,到点唤醒;performSeletor: afterDelay(本质也是创建一个NSTimer加到Runloop中)。

CFRunLoopObserverRef 活动状态

__CFRunLoopObserver结构体中的_activities(CFOptionFlags,6种状态如下)保存了当前mode的活动状态,状态发生改变时,通过结构体中的_callout回调(CFRunLoopObserverCallBack)通知状态的观察者。

Entry 即将进入

BeforeTimers //即将处理Timers

BeforeSource //即将处理Sources

BeforeWaiting //即将入休眠(UI刷新和AutoreleasePool执行的触发状态)

AfterWaiting //刚从休眠中唤醒

Exit //即将退出

mode管理item的接口有6个,add和remove各3个分别对应以上3个事件源。CFRunLoopAddSource/Timer/Obsever(rl, item, modeName),如果modeName没有则会创建一个。


RunLoop的内部逻辑:事件循环机制

主线程Runloop的启动过程  通过设置断点,LLDB中输入指令bt查看调用栈,发现UIApplicationMain函数中调用了CFRunLoopSpecific(RunLoop的入口)。

CFRunLoopSpecific的实现

→ currentMode = __CFRunLoopFindMode(rl, modeName, false)(找本次mode,找不到或mode中没有任何事件则直接Finished)

→ __CFRunLoopDoObservers(rl, currentModel, Entry)(即将进入的通知)

→ result = __CFRunLoopRun(rl, currentModel, seconds, returnAfterSourceHandled, previousMode)

→ __CFRunLoopDoObservers(rl, currentMode, Exit)(即将退出的通知)

__CFRunLoopRun(rl, mode, seconds, handler, previousMode)的实现

→ __CFRunLoopDoObservers(rl, rlm, BeforeTimers)、(rl, rlm, BeforeSources); __CFRunLoopDoBlocks(rl, rlm);(发出即将处理Tmers,Sources的通知,响应Blocks)

→ __CFRunLoopDoSources0,__CFRunLoopDoBlocks(处理Sources0,并响应blocks)

→ 判断如果有Source1则goto handle_msg(处理source1的逻辑)

→ __CFRunLoopDoObservers(rl, rlm, BeforeWaiting)(发出即将进入休眠的通知)→ __CFRunLoopSetSleeping(rl); 

→ __CFRunLoopServiceMachPort(正式进入休眠,并等待消息来唤醒线程)

注:休眠时,其中调用了mach_msg函数从用户态切换到了内核态,等待消息,避免CUP资源占用。有消息处理时,立刻唤醒线程,调用mach_msg切回用户态。

handle_msg的实现:

→ int retVal = 0,do-while循环

→ __CFRunLoopDoTimers,__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,__CFRunLoopDoSource1(被谁唤醒就处理谁)

→ __CFRunLoopDoBlocks(处理Block)

→ __CFRunLoopModeIsEmpty ? retVal = kCFRunLoopRunFinished;(根据判断条件,标记kCFRunLoopRunHandledSource=4、TimedOut=3、Stopped=2、Finished=1等返回值,return retVal (!=0))

总结:__CFRunLoopRun方法内部的逻辑,先发出即将处理事件源Timer/Sources的通知,doBlocks,处理Source0和Source1(如果有,跳转handle_msg),发出即将进入休眠的通知,machPort转移控制权到内核态,并sleep进入休眠。最后Afterwaiting从休眠中唤醒的通知,开始新一轮。

Runloop与线程

1、与线程一一对应,保存在全局的Dict中,线程(pthread)为key,runloop为value;

2、runloop为线程保活否则线程执行完任务就会退出(主线程执行完main()程序也会退出);

3、runloop创建时机:线程刚创建时没有,第一次CFDictionaryGetValue获取它的时候创建(主线程在UIApplicationMain函数中通过[NSRunLoop currentRunLoop]自动创建,子线程默认没有开启runloop);

4、销毁时机:线程结束时销毁。创建一个线程(子线程)并执行Test任务,[thread start]后线程会被销毁;runloop注册时会同时注册销毁的回调,当线程被销毁时执行。

5、常驻子线程:为子线程创建Runloop(addPort+addSource添加事件来维持循环→[run] / [runMode beforeDate]启动,注意,run和runUntilDate会永久运行,stop也无法停止),runMode:beforeDate可以用stop退出:在performSelector: onThread中,执行CFRunLoopStop(CFRunLoopGetCurrent());。

添加Source事件源

CFMessagePortRef localPort = CFMessagePortCreateLocal(nil, CFSTR("com.example.app.port.server"), Callback, nil, nil);

CFRunLoopSourceRef runLoopSource = CFMessagePortCreateRunLoopSource(nil, localPort, 0);

CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

CF方法:创建port,创建source,addSource到currentRunLoop

NS方法:[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; // 子线程,别忘了手动run/runMode

或使用信号量的方式

dispatch_semaphore_t sem;

sem = dispatch_semaphore_create(0);

dispatch_semaphore_signal发送信号;

while(true)循环内dispatch_semaphore_wait(sem, -1)等待信号,并执行:

dispatch_block_t block = [actions firstObject]; if (block) { [actions removeObject: block]; block(); }

6、GCD:dispatch_async(main)时,libDispatch会向主线程的RunLoop发消息,RunLoop会被唤醒并从消息中取得要执行的任务的block,并在回调中执行这个block。

注:此逻辑只限于dispatch到主线程,其他线程仍是libDispatch自己处理的。


Runloop与NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是toll-free bridged(等价桥接)的。

原理:在重复的时间点注册事件,RunLoop为了节省资源不会非常准确的回调(Tolerance宽容度,允许的时间误差)。

1、子线程上使用定时器:[[NSRunLoop currentRunLoop] run],手动启动子线程Runloop(invalidate时也要在子线程);

2、解决NSTimer在滚动时停止工作的问题:NSTimer默认加入DefaultMode,而滚动时的TrackingMode下,不会处理DefaultMode的消息,所以应设置为CommonMode,同步到所有mode包括Default和Tracking。

3、performSelector:afterDelay会内部创建一个Timer并加到当前线程的runloop中;onThread也会内部创建一个Timer并加到线程中。两种方法在当前线程没有启动Runloop时都会失效(如在未创建和启动Runloop的子线程中调用,回到1)

4、NSTimer的不准时:当RunLoop的任务繁重时,每次循环都会查看是否到达Timer设置的时间点,当循环时间过长时会导致超过Timer设置的时间,则不会延后执行回调而是等待下一个时间点。可以用GCD的定时器,不依赖Runloop直接与系统内核挂钩(dispatch_source_create→dispatch_source_set_timer→dispatch_source_set_event_handler→dispatch_resume)

注1:scheduledTimerWithTimeInterval自动添加到默认的mode下;timerWithTimeInterval不会添加到runloop中,需要手动添加并指定common模式,以避免滚动mode下停止工作的问题。

注2:NSTimer、CADisplayLink、dispatch_source_t的区别和优劣。

注3:CADisplayLink,和屏幕刷新率一致的定时器(内部实际是操作了一个Source),也需要添加到RunLoop,如果两次刷新之间有耗时任务,则会造成页面丢帧卡顿。


AutoreleasePool

主线程的RunLoop中注册了两个Observer:1、监听kCFRunLoopEntry,调用objc_autoreleasePoolPush创建自动释放池,优先级较高,保证在所有其他回调前;2、监听了kCFRunLoopBeforeWaiting,进入休眠前先调pop释放旧池再调push创建新池、和kCFRunLoopBeforeExit,调用pop释放自动释放池优先级低,保证在其他所有回调后。

主线程的代码一般是在事件/Timer回调内的,所以会被自动创建的autoreleasePool环绕,不会出现内存泄漏。但有些情况下比如for循环中创建大量临时对象,需要手动添加@autoreleasePool在下一次RunLoop前就处理这些对象。注:enumerateObjectsUsingBlock的容器循环内,默认添加了pool。


事件响应和手势识别

IOKit捕捉到事件后处理成IOHIDEvent通过mach port发送给SpringBoard,再通过mach port发送给当前前台运行的app,触发了app主线程RunLoop的Source1监听并触发Source0的回调,拿到了事件后,在回调内部封装为UIEvent(手势的封装和识别也在这一步),然后调用UIApplication的sendEvent传递给UIWindow,开始了事件的传递链,寻找最佳响应者。

事件的Source0回调中,处理事件时如果识别成Gesture手势,则标记手势对象为待处理,监听到BeforeWating事件时,会获取所有标记待处理的手势执行手势回调,每当手势变化时,都会进行update。


UI的绘制和刷新

更改frame、UI层级,或手动调用setNeedsLayout/Display后,UIView/CALayer将被标记为待处理并提交到全局容器中,在BeforeWaiting和Exit回调中执行一个callback函数,方法会遍历所有待处理的UI并执行绘制和调整,更新页面。


网络请求

网络请求接口的四层封装

1、CFSocket:最底层的接口,只负责Socket通信

2、CFNetwork:基于CFSocket的封装

3、NSURLConnection:基于CFNetwork的封装,面向对象,AFNetworking在这层

4、NSURLSession:iOS7新增,表面和3并列,底层实际还是用到了3部分功能,AFNetwork2,Alamofire在这层

NSURLConnection的工作原理:[connection start],start函数会获取CurrentRunLoop,并在Default里CFRunLoopAddSource加4个Source0(参见RunLoop与线程中的5、创建常驻线程)。CFMultiplexerSource负责各种请求的回调,CFHTTPCookieStorage负责处理Cookie。

网络开始传输时,NSURLConnection创建了com.apple.CFSocket.private和com.apple.NSURLConnectionLoader两个新线程,CFSocket处理底层Socket连接发个Source1事件,NSURLConnectionLoader接收这个Source1(基于mach Port),并通过之前添加的Source0通知到上层delegate,唤醒回调线程的RunLoop,执行实际的回调。

AFNetwork

AFURLConnectionOperation:基于NSURLConnection,单独创建了一个线程并启动了一个RunLoop,以便在后台线程接收Delegate回调。


相关问题

RunLoop的基本作用

主线程RunLoop保持程序运行;处理事件/Timer;通过调用mach_msg转移线程控制权切换内核态,进入休眠,节省CPU资源。


Runloop对象的数据结构

一个RunLoop对象有多个mode,如defaultMode、trackMode和commonMode标记,每个mode可以处理多个source和Timer,回调多个Observer。


整个RunLoop机制的运行过程,哪些事件源可以唤醒RunLoop

Entry通知,RunLoop运行(source和timer通知,处理blocks,处理source0.1,然后handle_msg内执行dowhile循环,被谁唤醒处理timer,GCD,Source1),Exit通知


获取RunLoop的两个方法和实现;管理Mode的两个方法和用处;管理ModeItem的6个方法和用处CFRunLoopGetMain/Current:从线程-RunLoop的字典中获取,获取不到就创建一个,并注册跟随线程销毁的回调。主线程会在UIApplicationMain中获取主线程RunLoop默认开启。

addCommonMode/runInMode方法:添加自定义mode和将事件或Timer放入指定mode。

CFRunLoopAddSource/Timer/Obsever(rl, item, modeName),如果modeName没有则会创建一个。


RunLoop和线程的关系及生命周期,主线程创建RunLoop的过程,如何实现一个常驻子线程(线程保活)

与线程一一对应;主线程RunLoop会在Main函数时通过currentRunLoop创建并开启;

在子线程内addPort / addSource维持事件循环,run开启后常驻  或  使用信号量的方式:while(true)循环内执行dispatch_semaphore_wait(sem, -1)等待信号,收到信号执行block。


NSTimer的原理和劣势;有哪些添加NSTimer的方法,分别有什么问题;能在子线程上使用NSTimer吗;PerformSelector的原理

NSTimer和CFRunLoopTimerRef是toll-bridge的,注册时间点每次RunLoop时检查,为了性能有一定的宽容度,有耗时任务时将不会准时回调;scheduled开头的timer添加方法会加到defaultMode下,track时不回调,而timerWithTimeInterval不会加到RunLoop中,需要手动添加[[NSRunLoop mainRunLoop] addTimer: timer forMode:NSRunLoopCommonModes];。PerformSelector的onThread和afterDelay都是添加一个timer到runLoop中,子线程中不会默认启动。

主线程中是如何通过RunLoop添加AutoreleasePool的,子线程呢

监听Entry调用Push创建,BeforeWaiting的回调中Pop并再push一个新的,于是主线程代码中的对象被pool包围,不会内存泄露。子线程中,iOS7前需要手动开启,后来会通过自动创建hotPoolPaged把对象加入page。


RunLoop和UI刷新的关系(绘制计算和提交渲染的时机)

被标记需要刷新的UI会在BeforeWaiting/Exit的回调中,CPU计算处理提交GPU渲染。


RunLoop是如何接收系统事件(识别手势),并开启传递链的

IOKit,SpringBoard,分发给APP,通过mach port触发主线程Source1并执行Source0回调,内部处理成UIEvent,调用UIApplication的sendEvent发给UIWindow开始事件的传递。

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