ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

iOS: RunLoop入门

iOS: RunLoop入门 文章目录RunLoopRunLoop 和 线程的关系为什么 RunLoop 能让线程休眠RunLoop Mode (运行模式)Timer 在滚动的时候会失效NSRunLoopCommonModesRunLoop 的 SourceTimerObserverRunLoop 一次完整运行过程RunLoop 和 线程保活为什么必须要有 addPort没有 addPort 的情况有 addPort 的情况RunLoop 与 performSelector常见的几种形式RunLoop 与 GCD 区别RunLoopRunLoop 本质上是一个事件循环机制, 让线程在没有任务是休眠, 有事件时被唤醒并处理事件普通线程-(void)threadMain{NSLog(开始执行);// 执行完// 线程结束}如果希望线程一直存活while(YES){// 等待任务}这种方式会导致 CPU 空转。RunLoop 解决的就是没有事件 ↓ 线程进入休眠 ↓ 有事件到来 ↓ 唤醒线程 ↓ 处理事件 ↓ 继续等待所以可以把它理解成RunLoop 线程的“事件调度中心” 休眠/唤醒机制RunLooP作用保持线程 (尤其是主线程) 持续运行, 不退出处理 App 中的各种事件 (触摸, Timer, performSelector, GCD主队列任务, 界面刷新)节省CPU 资源: 有事做事, 无事休眠RunLoop 和 线程的关系一个线程对应一个RunLoop, 但不是说每个线程启动后都有一个已经运行的RunLoop,主线程:NSRunLoop*runLoop[NSRunLoop mainRunLoop];主线程的RunLoop会被系统自动启动子线程则不同:NSThread*thread[[NSThread alloc]initWithBlock:^{NSLog(子线程);[[NSRunLoop currentRunLoop]run];}];[thread start];如果不调用:[[NSRunLoop currentRunLoop] run];,那么子线程执行完任务之后就结束了主线程的 RunLoop 在App启动时自动创建并执行子线程默认没有RunLoop, 需要手动获取并启动 (否则线程执行完成任务就退出)为什么 RunLoop 能让线程休眠底层并不是简单的while(1)而是RunLoop ↓ 检查有没有事件 ↓ 有事件 → 处理 ↓ 没事件 → 让线程进入休眠 ↓ Mach Port / Source / Timer 等事件到来 ↓ 系统唤醒线程所以 RunLoop 的核心价值之一就是避免线程在没有任务的时候持续占用 CPU。RunLoop的核心组成: Mode, Source, Timer, ObserverRunLoop Mode (运行模式)RunLoop 并不是把所有的事件全部一起处理, 它通过Mode对事件进行分类ModeMode 名称说明kCFRunLoopDefaultMode / NSDefaultRunLoopMode默认模式App 平时运行在这里UITrackingRunLoopMode界面追踪模式UIScrollView 滑动时切换到这个模式kCFRunLoopCommonModes / NSRunLoopCommonModes伪模式 / 标记不是真实 Mode。默认包含 Default TrackingUIInitializationRunLoopModeApp 启动时的临时 Mode启动完成后不再使用GSEventReceiveRunLoopMode系统内部接收事件用一般碰不到常见的 Mode 有:NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode主线程的 RunLoop 是在 UIApplicationMain 内部自动创建并启动的 (最终调用CFRunLoop), 所以App可以一直运行Timer 在滚动的时候会失效例如:[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:selector(test)userInfo:nil repeats:YES];默认Timer被加入NSDefaultRunLoopMode那么这时候有一个TableView, 在滑动TableView的时候, Timer就会失效这是因为在UITableView上滑动的时候:RunLoop ↓ 进入 UITrackingRunLoopMode ↓ Default Mode 暂时不处理 ↓ Timer 不触发那么如何解决这个问题呢?只需要将 Timer 加入到NSRunLoopCommonModes就可以解决这个问题NSTimer*timer[NSTimer timerWithTimeInterval:1target:selfselector:selector(test)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];NSRunLoopCommonModesNSRunLoopCommonModes不是一个独立运行的 Mode它可以理解为 Mode集合, 包含多个Common Mode当把 Timer 加入到NSRunLoopCommonModes, 就是相当于就把 Timer 添加到当前RunLoop标记为 Common 的那些 Mode 中RunLoop 的 SourceSource 是 RunLoop 的事件来源常见可以分为:Source 0需要手动唤醒/处理的事件源例如:performSelector这类机制可以和 Source 0 联系起来Source 1基于 Mach Port 等底层事件机制例如:触摸事件 IPC 系统事件可以粗略理解为Source 1 ↓ 底层事件到达 ↓ 唤醒 RunLoop ↓ 处理事件TimerNSTimer也是RunLoop 的一个输入源, 这里需要注意的是: NSTimer 本身不是一个独立线程例如:NSTimer*timer[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:selector(sayHello)userInfo:nil repeats:YES];Timer 是注册到 RunLoop 中NSTimer ↓ RunLoop ↓ 时间到了 ↓ RunLoop 检查 ↓ 执行 selector如果 RunLoop 不运行, NSTimer 就不会正常触发-(void)viewDidLoad{[superviewDidLoad];// 1. 创建一个 Timer每隔 1 秒执行一次 tick 方法NSTimer*timer[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:selector(tick)userInfo:nil repeats:YES];for(inti0;i5;i){sleep(1);// 休眠 1 秒模拟重度计算NSLog(正在计算... 第 %d 秒,i1);}}-(void)tick{NSLog(定时器触发);}打印:正在计算... 第 1 秒 正在计算... 第 2 秒 正在计算... 第 3 秒 正在计算... 第 4 秒 正在计算... 第 5 秒 定时器触发从上面的代码运行结果可以观察到, 定时器事件在for循环结束之前没有触发让我们看看 CPU 实际执行指令的顺序:// RunLoop 的伪代码 function CFRunLoopRun() { while (true) { // ← RunLoop 的 while 循环 // 1. 检查 Timer // 2. 检查 Source // 3. 如果没有事件休眠 // 4. 如果有事件执行它 if (有事件需要处理) { // 这里会调用你的代码 [target performSelector:action]; // ← 在这里调用了 viewDidLoad // 你的代码执行完后才继续下一轮循环 } } }ObserverObserver 用来监听 RunLoop 的状态变化例如:RunLoop 即将进入 ↓ 处理 Timer ↓ 处理 Source ↓ 即将休眠 ↓ 被唤醒 ↓ 退出 RunLoop可以通过CFRunLoopObserver监听常见状态:kCFRunLoopEntry kCFRunLoopBeforeTimers kCFRunLoopBeforeSources kCFRunLoopBeforeWaiting kCFRunLoopAfterWaiting kCFRunLoopExitRunLoop 一次完整运行过程RunLoop 开始 ↓ BeforeTimers ↓ BeforeSources ↓ 处理 Timer / Source ↓ 是否还有事件 ↙ ↘ 有 没有 ↓ ↓ 继续 BeforeWaiting ↓ 休眠 ↓ 事件到达 / Timer 到期 ↓ AfterWaiting ↓ 处理事件 ↓ 循环可以把它浓缩成进入 ↓ 处理事件 ↓ 没有事件 ↓ 休眠 ↓ 被唤醒 ↓ 处理事件 ↓ 循环RunLoop 和 线程保活比如创建一个常驻线程:NSThread*thread[[NSThread alloc]initWithBlock:^{[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];}];NSThread* thread [[NSThread alloc] initWithBlock:^{ ... }];作用: 创建一个新的子线程对象initWithBlock: 告诉这个线程; “当启动时候,执行 Block 里面的代码”此时,线程还没有启动, 知识准别好了[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];[NSRunLoop currentRunLoop]获取当前线程的 RunLoop因为这段代码写在 Block 里, 而 Block 会在子线程中执行, 所以这里拿到的是子线程的 RunLoop[NSPort port]创建一个 Port (端口)可以把它理解成一个虚拟的 “信箱“, 有了这个信箱, RunLoop 就会认为: “我有东西要监听,不能退出”add:forMode:把上面的信箱加入到 RunLoop 中作用: 给RunLoop添加一个永久有效的事件源, 让它有事可做, 不会空转退出[thread start]启动子线程这时, 子线程开始执行 Block 里的代码为什么必须要有addPort这是一个极其关键的问题。没有addPort的情况[[NSRunLoop currentRunLoop] run];RunLoop 启动后会检查自己有没有事件源比如 Timer、Port、Observer。如果什么都没有RunLoop 会认为“我没事可做没必要继续跑。”于是RunLoop 立即退出子线程执行完所有代码线程被销毁。有addPort的情况[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run];添加 Port 后RunLoop 发现“我有一个 Port 要监听我不能退出。”于是RunLoop 进入永久等待状态线程永不销毁。RunLoop 与 performSelectorperformSelector是NSObject提供的一系列方法允许你延迟执行或在指定线程上执行某个方法。[selfperformSelector:selector(test)onThread:thread withObject:nil waitUntilDone:NO];常见的几种形式方法作用[obj performSelector:selector(method)]在当前线程立即执行[obj performSelector:withObject:afterDelay:]延迟执行依赖 RunLoop[obj performSelector:onThread:withObject:waitUntilDone:]在指定线程执行[obj performSelectorOnMainThread:withObject:waitUntilDone:]在主线程执行这个机制和目标线程的RunLoop 有密切关系主线程 ↓ performSelector ↓ 目标线程 RunLoop ↓ 唤醒 ↓ 执行 test如果创建了一个子线程:NSThread*thread...但是如果没有正确启动它的 RunLoop:[[NSRunloop currentRunLoop]run];那么一些基于RunLoop的任务就有可能无法进行.RunLoop 与 GCD 区别GCD更偏向 任务调度例如:dispatch_async(dispatch_get_global_queue(0,0),^{// 后台任务});关注任务 - Queue - ThreadRunLoop 更偏向 事件循环关注:事件 - RunLoop - 处理 - 休眠 - 唤醒两者不是互相替代的关系 ,GCD 和 RunLoop 在iOS 系统中是协同工作的
返回列表