├── .gitignore └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS-runloop 2 | runLoop,正如其名,表示一直运行着的循环。 3 | 一般来说,一个线程只能执行一个任务,执行完就会退出,如果需要线程一直不推 4 | 出,那么就需要在线程中创建一个死循环,而在oc中runLoop就是这样一个死循环; 5 | runLoop实际上就是一个对象,这个对象管理了其需要处理的事件和消息 6 | ,并提供了一个入口函数来执行上面的逻辑。线程执行了这个函数之后,就会一直 7 | 处于“接收消息-等待-处理”的循环中,知道这个循环结束,函数返回。 8 | ios提供了两个这样的对象:NSRunLoop和CFRunLoopRef。 9 | 10 | ## 一、线程与runLoop 11 | ### 1.线程的终止 12 | 在oc中,我们一般使用属性来保存一个对象,防止这个对象的释放; 13 | 但是,使用属性的方式不能保存一个线程,保存线程唯一的方式,就是让这个线程 14 | 里面的任务一直在执行; 15 | 即使一个线程对象是一个局部变量,只要线程里面的任务没有执行完毕,这个线程 16 | 也不会释放掉; 17 |   18 | ## 2.线程与runLoop的关系 19 | ① runLoop和线程紧密相连,可以说,runLoop是为了线程而生的,没有线程, 20 | 就没有runLoop存在的必要; 21 | ② 每个线程都有其对应的runLoop对象; 22 | ③ 主线程的runLoop是默认启动的,而其他线程的runLoop是默认没有启动的; 23 | 使用[NSRunLoop currentRunLoop]可以得到当前线程的runLoop对象; 24 | ## 二、RunLoop的相关知识点 25 | ### 1.runLoop的模式 26 | runLoop中使用mode来指定事件在运行循环中的优先级,分为: 27 | ① NSDefaultRunLoopMode(kCFRunLoopDefaultMode): 默认,空闲状态; 28 | ② UITrackingRunLoopMode:UI模式(scrollView滑动),UI模式优先级 29 | 最高,而且只能通过触摸事件切换; 30 | ③ UIInitializationRunLoopMode: 启动时; 31 | ④ NSRunLoopCommonModes(kCFRunLoopCommonModes):占位模式,mode集合。 32 | 33 | ps:解决定时器与scrollView滑动时候,把timer添加到commonModes的原因? 34 | 每种mode有三种:source、observer、timer; 35 | 我们在创建定时器的时候,会默认把timer添加到default模式中,但是scrollView 36 | 在滑动的时候,模式会切换到UI模式,此时default模式中的timer就会停止; 37 | 解决的办法,就是把timer添加到UI模式中,但是UI模式只有在触摸事件,才会切换, 38 | 没有触摸时,会切换到default模式,在default模式中,timer不会进行; 39 | oc中,还有一个模式,就是commonModes,这其实不是runLoop中的一个模式,它 40 | 只是一种占位,是其他mode的集合; 41 | ### 2.runLoop的使用 42 | ``` 43 | NSThread *thread = [[NSThread alloc] initWithBlock:^{ 44 | NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self 45 | selector:@selector(timerMethod) userInfo:nil repeats:YES]; 46 | [[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes]; 47 | while(!_isFinished) { 48 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0.00001]; 49 | [[NSRunLoop mainRunLoop] runUntilDate:date]; 50 | } 51 | }]; 52 |        //这里可以使用isFinished属性,来控制runLoop停止,然后终止分线程; 53 | ``` 54 | ### 3.runLoop观察者(observer) 55 | runLoop观察者则是在runLoop本身运行的特定时候触发,你可以使用runLoop观察者为处 56 | 理某一特定事件或是进入休眠的程序做准备。可以将runLoop观察者和以下事件关联: 57 | ① runLoop入口 58 | ② runLoop何时处理一个定时器; 59 | ③ runLoop何时处理一个输入源; 60 | ④ runLoop何时进入睡眠状态; 61 | ⑤ runLoop何时被唤醒,但在唤醒之前要处理的事件; 62 | ⑥ runLoop终止; 63 | 在创建的时候,也可以指定runLoop观察者可以只用一次或者循环使用,若只用一 64 | 次,那么它在启动后,会把自己从runLoop中移除,而循环的观察者不会。 65 | ### 4.runLoop事件队列 66 | 每次运行runLoop,线程的runLoop会自动处理之前未处理的消息,并通知相关的 67 | 观察者,具体的顺序如下: 68 | ① 通知观察者runLoop已经启动; 69 | ② 通知观察者任何即将要开始的定时器; 70 | ③ 通知观察着任何即将启动的非基于端口的源; 71 | ④ 启动任何准备好的非基于端口的源; 72 | ⑤ 如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤⑨; 73 | ⑥ 通着观察者线程进入休眠; 74 | ⑦ 将线程至于休眠知道任意下面的事件发生: 75 | A.某一时间到达基于端口的源; 76 | B.定时器启动; 77 | C.runLoop设置的时间已经超过; 78 | D.runLoop被显示唤醒; 79 | ⑧ 通知观察者线程将被唤醒; 80 | ⑨ 处理未处理的事件 81 | A.如果用户定义的定时器启动,处理定时器并重启runLoop,进入步骤2 82 | B.如果输入源启动,传递响应的消息; 83 | C.如果runLoop被显示唤醒,而且时间还没超过,重启runLoop,进入步骤2 84 | ⑩ 通知观察者runLoop结束。 85 | 86 | ps: 87 | ① 如果事件到达,消息会被传递给响应的处理程序来处理,runLoop处理完 88 | 当次事件后,runLoop就会推出,而不管之前预定的时间到了没有。 89 | ② 可以重新启动runLoop来等待下一事件; 90 | ③ 如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会 91 | 休眠等待相应事件的发生。 92 | ## 三、RunLoop输入事件来源 93 | runLoop接收的输入事件来自两种来源:输入源和定时源; 94 | ### 1.输入源 95 | 传递异步事件,通常消息来自其他线程或程序。输入源传递异步消息给相应的处 96 | 理程序,并调用runUntilDate方法来退出; 97 | 当你创建输入源,需要将其分配给runLoop的一个或多个模式,模式只会在特定 98 | 事件影响监听的源。 99 | 以下是输入源的类型: 100 | ① 基于端口的输入源:基于端口的输入源是有内核自动发送; 101 | cocoa和Core Foundation内置支持使用端口相关的对象和函数来创建基于端 102 | 口的源。 103 | 例如:在Core Foundation中,使用端口相关的函数来创建端口和runLoop源; 104 | ② 自定义输入源:自定义源需要人工从其他线程发送。 105 | Core Foundation中可以使用CFRunLoopSourceRef等来创建源,也可以使用 106 | 回调函数来配置源。Core Foundation会在配置源的不同地方调用回调函数,处理 107 | 输入事件,在源从runLoop移除的时候清理它; 108 | ③ Cocoa上的selector源 109 | ### 2.定时源 110 | 定时源在预设的时间点同步传递消息,这些消息都会在特定事件或者重复的时间 111 | 间隔,定时源则传递消息个处理线程,不会立即退出runLoop。 112 | 定时器并不是实时机制,定时器和你的runLoop的特定模式相关,如果定时器所在 113 | 的模式当前未被runLoop监视,那么定时器将不会开始,知道runLoop运行在响应的模式 114 | 下。 115 | 116 | ## 四、CFRunLoop介绍 117 | ### 1.runLoop对外的接口 118 | CoreFoundation中有5个关于runLoop的类: 119 | ① CFRunLoopRef: 120 | ② CFRunLoopModeRef:该类并没有对外暴露; 121 | ③ CFRunLoopSourceRef:时间产生的地方,source有两个版本: 122 | A.source0:只包含一个回调,它并不能主动触发事件,使用时,需先调用 123 | CFRunLoopSourceSignal将这个source标记为待处理,后调用CFRunLoopWakeUp来 124 | 唤醒runLoop,让其处理这个事件; 125 | B.source1:包含一个mach_port和一个回调,被用于通过内核和其他线程相 126 | 互发送消息,这种source能主动唤醒runLoop线程。 127 | ④ CFRunLoopTimerRef:基于事件的触发器,他和NSTimer是可以混用的,其包含 128 | 一个事件长度和回调,当其加入到runLoop中是,runLoop会注册对应的时间点,当时 129 | 间点到时,runLoop会被唤醒以执行那个回调; 130 | ⑤ CFRunLoopObserverRef:观察者,包含了一个回调,当runLoop的状态发生变 131 | 化时,观察者就能通过回调接收到变化,可观测的时间点有: 132 | kCFRunLoopEntry //即将进入Loop 133 | kCFRunLoopBeforeTimers //即将处理Timer 134 | kCFRunLoopBeforeSources //即将处理Source 135 | kCFRunLoopBeforeWaiting //即将进入休眠 136 | kCFRunLoopAfterWaiting //刚从休眠中唤醒 137 | kCFRunLoopExit //即将退出Loop 138 | 139 | ps: 140 | ① 一个runLoop包含若干个Mode,每个Mode包含若干个Source/Timer/Observer; 141 | ② 每次调用runLoop的主函数,只能指定其中一个Mode,如果需要切换Mode, 142 | 只能退出Loop,再重新指定一个Mode进入; 143 | ③ Source/Timer/Observer统称为mode item,一个item可以同时加入多个mode 144 | ,但一个item被重复加入同一个mode是没效果的; 145 | ④ 如果一个mode钟一个item都没有,runLoop就会退出,不进入循环。 146 | ### 2.runLoop的Mode 147 | CFRunLoop的结构如下 148 | struct __CFRunLoop { 149 | CFMutableSetRef _commonModes; 150 | CFMutableSetRef _commonModeItems; 151 | CFRunLoopModeRef _currentMode; 152 | CFMutableSetRef _modes; 153 | ... 154 | }; 155 | CFRunLoopMode的结构如下: 156 | struct __CFRunLoopMode { 157 | CFStringRef _name; 158 | CFMutableSetRef _sources0; 159 | CFMutableSetRef _sources1; 160 | CFMutableArrayRef _observers; 161 | CFMutableArrayRef _timers; 162 | ... 163 | }; 164 | 其中,CFRunLoop对外暴露的管理Mode的接口有两个: 165 | CFRunLoopAddCommonMode 166 | CFRunLoopRunInMode 167 | Mode暴露的管理mode item的接口有下面几个: 168 | CFRunLoopAddSource 169 | CFRunLoopAddObserver 170 | CFRunLoopAddTimer 171 | CFRunLoopRemoveSource 172 | CFRunLoopRemoveObserver 173 | CFRunLoopRemoveTimer 174 | 175 | ps: 176 | ① 只能通过mode name来操作内部的mode,当你传入一个新的mode name但 177 | runLoop内部没有对应的Mode时,runLoop会自动帮你创建对应的CFRunlLoopModeRef。 178 | 对于一个runLoop来说,其内部的mode只能增加,而不能删除。 179 | ② commonModes:一个mode可以将自己标记为“Common”苏醒,每当runLoop的 180 | 内容发生变化时,runLoop都会自动将_commonModeItems里的item同步到具有 181 | “Common”标记的所有Mode里。 182 | ### 3.runLoop的内部逻辑 183 | 实际上,runLoop就是一个函数,其内部是一个do-while循环,当你调用CFRunLoop 184 | ()时,线程就会一直停留在这个循环中;直到超时或被手动停止,该函数才会返回。 185 | ## 五、runLoop的底层实现 186 | runLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg(),为了 187 | 解释这个逻辑,需要介绍下ios的系统框架; 188 | ### 1.系统层次 189 | 苹果官方将系统大致划分为4个层次: 190 | ① 应用层:包括用户能解除到的图层应用,例如Spotlight、Aqua、SpringBoard等 191 | ② 应用框架层:即开发人员解除到的Cocoa等框架; 192 | ③ 核心框架层:包括各种核心框架、OpenGL等内容; 193 | ④ Darwin:即操作系统的核心,包括系统内核、驱动、Shell等内容,这层是开源的 194 | ### 2.Darwin层 195 | 其中,在硬件层上面的三个组成部分:Mach,BSD,IOKit,共同组成了XNU内核。 196 | ① Mach:XNU内核的内环被称作Mach,其作为一个为内核,仅提供了诸如处理器调度 197 | 、IPC(进程间通信)等非常少量的基础服务; 198 | ② BSD:BSD层可以看做围绕Mach层的一个外环,提供了诸如进程管理、未见系统和 199 | 网络等功能; 200 | ③ IOKit:该层为设备驱动提供了一个面向对象(C++)的框架。 201 | Mach本身提供的API非常有限,而且苹果也不鼓励使用Mach的API,但是这些API非常 202 | 基础,如果没有这些API的话,其他任何工作都无法实施。在Mach中,所有的东西都是通 203 | 过自己的对象实现的,进程、线程和虚拟内存都被称为“对象”。和其他架构不公,Mach 204 | 对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。“消息”是Mach中最 205 | 基础的概念,消息在两个端口(port)之间传递,就是Mach的IPC的核心。 206 | 为了实现消息的发送和接收,mach_msg()函数实际上是调用了一个Mach陷阱(trap) 207 | 即函数mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用。当你在用户状态调用 208 | mach_msg_trap()时会触发陷阱机制,切换到内核态;内核态中内核实现mach_msg()函数 209 | 会完成实际的工作。 210 | ps:runLoop的核心就是一个mach_msg(),runLoop调用这个函数去接收消息,如果 211 | 没有别人发送port消息过来,内核会将线程置于等待撞他。 212 | 例如你在模拟器里跑起一个app,然后在app静止时点击暂停,你会看到主线程调用 213 | 栈是停留在mach_msg_trap()这个地方。 214 | ## 六、苹果用runLoop实现的功能 215 | ### 1.AutoReleasePool 216 | app启动后,苹果在主线程的runLoop里注册了两个Observer: 217 | ① 第一个Observer监视事件是Entry(即将进入Loop),其回调内会创建自动释放池 218 | ,优先级最高,保证释放池发生在所有回调之前; 219 | ② 第二个Observer监视了两个事件,BeforeWaiting(准备进入休眠)时释放旧的 220 | 池并创建新的池;Exit(即将推出loop)时释放自动释放池;优先级最低,保证其释放 221 | 池发生在其他回调之后。 222 | ### 2.事件响应 223 | 苹果注册了一个Source1(基于mach port)用来接收系统事件 224 | 当一个硬件事件(触摸/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent 225 | 事件,并由SpringBoard接收,随后用mach port转发给需要的App进程。随后苹果注册 226 | 的那个Source1会触发回调,并调用方法UIApplicationHandleEventQueue()进行应用内 227 | 部的分发, 228 | UIApplicationHandleEventQueue()方法会把IOHIDEvent处理并包装成UIEvent分发, 229 | 通常事件比如UIButton点击,touch事件都是在这个回调中完成的。 230 | ### 3.手势识别 231 | 当上边的UIApplicationHandleEventQueue()识别了手势后,首先会打断当前的touch 232 | 系统回调,随后系统将手势标记为待处理。 233 | 苹果注册了一个Observer检测BeforeWaiting,在这个事件的回调函数中,获取所有 234 | 刚被标记为待处理的手势,并执行手势的回调。 235 | ### 4.界面更新 236 | 当操作UI时,比如改变了Frame等,这个UIView/CALayer会被标记为待处理,并提交 237 | 到一个全局的容器中。 238 | 苹果注册了一个Observer监听BeforeWaiting和Exit,在回调用,会遍历所有待处理 239 | 的UIView/CALayer以执行实际的绘制和调整,并更新UI界面。 240 | ### 5.定时器 241 | 一个NSTimer注册到RunLoop后,runLoop会为其重复的时间点注册号时间,runLoop 242 | 为了节省资源,并不会再非常准确的时间点回调这个timer。timer有个属性叫做tolerance 243 | (宽容度),表示当时间点后,容许有多少最大误差。 244 | ### 6.PerformSelector 245 | 当调用NSObject的PerformSelector方法后,实际上其内部会创建一个timer并添加 246 | 到当前线程的runLoop中,如果当前线程没有runLoop,则这个方法会失效。 247 | ### 7.关于GCD 248 | 实际上runLoop底层,也用到了GCD的东西,比如runLoop用dispatch_source_t实现 249 | 的timer,但同时GCD提供的某些方法也用到了runLoop,例如dispatch_async()。 250 | ### 8.关于网络请求 251 | 关于网络请求的接口,自上之下有如下基层: 252 | ① CFSocket:是最底层的接口,只负责socket的通信; 253 | ② CFNetwork:是基于CFSocket等接口的上层封装,ASIHttpRequest工作与这层; 254 | ③ NSURLConnection:是基于CFNetwork的更高层的封装,提供面向对象的接口, 255 | AFNetworking工作于这一层; 256 | ④ NSURLSession:是ios7中新增的接口,表面上和NSURLConnection并列,但底层 257 | 仍然用到了NSURLConnection的部分功能,AFNetworking2和Alamofire工作于这层。 258 | ### 9.NSURLConnection的工作过程 259 | 通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection 260 | start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会 261 | 会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动 262 | 触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookie 263 | Storage 是处理各种 Cookie 的。 264 | 当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.app 265 | le.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是 266 | 处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来 267 | 接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。 268 | NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收 269 | 来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSour 270 | ce 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。 271 | CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。 272 | --------------------------------------------------------------------------------