教你如何轻松搞定 Runloop
bearman12345
8年前
<p><strong>认识 Runloop</strong></p> <ul> <li>Runloop 就是运行循环,如果没有 Runloop,程序一运行就会退出,有 Runloop 就相当于在程序内部开了一个死循环</li> <li>在 iOS 开发中,有两套 API 可以访问 Runloop: <strong> NSRunloop </strong> 和 <strong> CFRunloopRef </strong> ,它们是等价的,可以相互转换</li> <li>NSRunloop 是基于 CFRunloopRef 的 OC 包装</li> <li>参考资料: <a href="/misc/goto?guid=4959719770461533777" rel="nofollow,noindex">苹果官方文档</a> 、 <a href="/misc/goto?guid=4959677503364339078" rel="nofollow,noindex">CFRunloopRef 源码</a></li> </ul> <p><strong>Runloop 的本质</strong></p> <ul> <li>Mach 是 XNU 的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop 通过 mach_msg() 函数发送消息</li> <li>如果没有 port 消息,内核会将线程置于等待状态 mach_msg_trap()</li> <li>如果有消息,判断消息类型处理事件,并通过 modeItem 的 callback 进行回调</li> </ul> <p><strong>Runloop 的作用</strong></p> <ul> <li>保证程序的持续运行</li> <li>处理 APP 中的各类事件</li> <li>节省 CPU 资源,提高程序性能(有事情就做,没事情就休息)</li> </ul> <p><strong>Runloop 与线程的关系</strong></p> <ul> <li>NSRUnloop 与线程一一对应</li> <li>主线程中的 runloop 在程序运行时已经创建并启动了</li> <li>子线程中的 runloop 需要我们手动创建并开启</li> <li>runloop 在线程结束时,也会销毁</li> </ul> <p><strong>获取 Runloop 对象</strong></p> <ul> <li> <p>获取主线程 runloop 对象</p> <pre> <code class="language-objectivec">NSRunLoop *mainRL = [NSRunLoop mainRunLoop]; CFRunLoopRef mainRLRef = CFRunLoopGetMain();</code></pre> </li> <li> <p>获取当前线程 runloop 对象</p> <pre> <code class="language-objectivec">NSRunLoop *currentRL = [NSRunLoop currentRunLoop]; CFRunLoopRef currentRlRef = CFRunLoopGetCurrent();</code></pre> </li> <li> <p>通过子线程创建 runloop</p> <pre> <code class="language-objectivec">NSRunLoop *curRunloop = [NSRunLoop currentRunLoop];</code></pre> <p>这个方法本身是懒加载的,如果是第一次调用该方法,那么就创建子线程对应的 runloop。</p> </li> <li>补充: runloop 对象是利用字典进行存储的,key 值对应线程对象,value 值对应该线程的 runloop,在子线程中runloop 不会自动创建。</li> </ul> <p><strong>Runloop 的相关类</strong></p> <p>与 runloop 相关的共有五个类:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopTimerRef、CFRunLoopSourceRef、CFRunLoopObserverRef</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b8c1165a3744befd7d8a7cd1d5987da4.png"></p> <p style="text-align:center">Runloop 五个相关类之间的关系</p> <ol> <li> <p><strong>CFRunLoopRef</strong> ( Runloop 对象)</p> <ul> <li> <p>Runloop 对象就是Runloop 本身</p> </li> </ul> </li> <li> <p><strong>CFRunLoopModeRef</strong> ( Runloop 的运行模式)</p> <ul> <li> <p>一个 Runloop 包含若干个 Mode,而每个 Mode 又包含若干个 Source/Timer/Observer,每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入,这样就可以分隔开不同组的 Source/Timer/Observer,让其互不影响。</p> </li> <li> <p>Mode 的分类,系统默认注册了 5 个 Mode:</p> <ul> <li> <p>KCFRunLoopDefaultMode :App的默认Mode,通常主线程是在这个 Mode 下运行</p> </li> <li> <p>UITrackingRunLoopMode :界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响</p> </li> <li> <p>UIInitializationRunLoopMode : 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用</p> </li> <li> <p>GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,通常用不到</p> </li> <li> <p>kCFRunLoopCommonModes : 占位用的 Mode,不是一种真正的 Mode,就相当于 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode的合体</p> </li> </ul> </li> </ul> </li> <li> <p>CFRunLoopTimerRef( Timer 事件)</p> <ul> <li>基于时间的触发器,基本等同于 NSTimer</li> <li> <p>NSTimer 在各种模式下的运行效果</p> <pre> <code class="language-objectivec">[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];</code></pre> <p>以上 scheduledTimerWithTimeInterval 方法内部默认把创建的定时器对象添加到当前的 Runloop 中,并且指定运行模式为 NSDefaultRunLoopMode</p> <pre> <code class="language-objectivec">NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];</code></pre> <p>以上 timerWithTimeInterval 方法创建定时器,如果想要定时器工作,还需要添加到 Runloop 中,并指定运行模式</p> </li> <li> <p><strong>注意:</strong> 当 Runloop 切换到非指定模式,定时器就会停止工作</p> </li> </ul> </li> <li> <p><strong>CFRunLoopSourceRef</strong> ( Runloop 要处理的事件源)</p> <ul> <li> <p>事件源也就是输入源,只需要对它的分类有所了解就可以了</p> </li> <li> <p>以前的分法(根据官方文档)</p> <ul> <li> <p>Port-Based Sources</p> </li> <li> <p>Custom Input Sources</p> </li> <li> <p>Cocoa Perform Selector Sources</p> </li> </ul> </li> <li> <p>现在的分法(基于函数的调用栈)</p> <ul> <li> <p>Source0:非基于 Port 的</p> </li> <li> <p>Source1:基于 Port 的</p> </li> </ul> </li> </ul> </li> <li> <p><strong>CFRunLoopObserverRef</strong>( Runloop 的监听者)</p> <ul> <li> <p>主要用于监听 Runloop 的状态</p> </li> <li> <p>Runloop 的状态主要有:</p> <pre> <code class="language-objectivec">typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即将进入 Runloop kCFRunLoopBeforeTimers = (1UL << 1), //即将处理 NSTimer kCFRunLoopBeforeSources = (1UL << 2), //即将处理 Sources kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), //即将退出 Runloop kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变 };</code></pre> </li> <li> <p>实现 Runloop 的监听</p> <pre> <code class="language-objectivec">//创建监听对象,当 Runloop 的状态改变时就会调用该方法 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"对 Runloop 的状态改变进行监听", activity); }); //设置监听 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);</code></pre> </li> </ul> </li> </ol> <p><strong>Runloop 的启动</strong></p> <ol> <li> <p>选择一个运行模式,且只能选择一个</p> </li> <li> <p>判断当前选择的运行模式是否为空</p> </li> <li> <p>检查当前运行模式里面是否有 Source 或 Timer,如果都没有,则 Runloop 立即退出</p> </li> <li> <p>至少有 Source 或 Timer 中的任意一个,则 Runloop 开启</p> </li> <li> <p>在检查的时候不会检查 Observer</p> </li> </ol> <p><strong>Runloop 的运行处理逻辑</strong></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/31222954cb7dc55d6308612ebd1f705e.png"></p> <p style="text-align:center">Runloop 的运行处理逻辑</p> <ul> <li> <p>在运行 Runloop 时,RUnloop 会自动处理之前未处理的消息,并通知相关的监听者,具体的处理逻辑如下:</p> <ol> <li> <p>通知观察者 Runloop 已经启动</p> </li> <li> <p>通知观察者即将要开始的定时器</p> </li> <li> <p>通知观察者即将启动的非基于端口的源</p> </li> <li> <p>启动已经准备好的非基于端口的源</p> </li> <li> <p>如果有基于端口的源并处于等待状态,立即启动,跳到第九步</p> </li> <li> <p>通知观察者 Runloop 进入休眠</p> </li> <li> <p>Runloop 进入休眠,等待发生以下事件时唤醒</p> <ul> <li> <p>有事件到达基于端口的源</p> </li> <li> <p>定时器启动</p> </li> <li> <p>Runloop 超时</p> </li> <li> <p>Runloop 被外界手动唤醒</p> </li> </ul> </li> <li> <p>通知观察者,线程刚被唤醒</p> </li> <li> <p>处理唤醒时收到的消息,之后跳到第二步</p> <ul> <li> <p>如果有用户定义的定时器启动,处理定时器事件并重启 Runloop</p> </li> <li> <p>如果输入源启动,传递相应的信息</p> </li> <li> <p>如果 Runloop 被外界手动唤醒且未超时,重启 Runloop</p> </li> </ul> </li> <li> <p>通知观察者,Runloop 即将结束</p> </li> </ol> </li> </ul> <p><strong>Runloop 的应用</strong></p> <ul> <li> <p>常驻线程</p> <ul> <li> <p>在子线程中创建一个 Runloop</p> <pre> <code class="language-objectivec">NSRunLoop *currentRunloop= [NSRunLoop currentRunLoop];</code></pre> </li> <li>需要至少指定 Runloop 的 Source 或者 Timer 中的任意一个(一般情况下指定 Source,比较简单)</li> <li> <p>需要指定 Runloop 的运行模式(保证 Runloop 不退出)</p> <pre> <code class="language-objectivec">[currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];</code></pre> </li> <li> <p>需要手动开启 Runloop</p> <pre> <code class="language-objectivec">[currentRunloop run];</code></pre> </li> </ul> </li> <li> <p>imageView 的显示</p> <ul> <li> <p>控制方法在特定模式下可用</p> <pre> <code class="language-objectivec">[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip2016"] afterDelay:3.0];</code></pre> <p>以上方法默认添加到当前的 Runloop 中,并且指定运行模式为默认 KCFRunLoopDefaultMode,如果 Runloop 切换运行模式,则图片不会加载到 imageView 上。</p> <pre> <code class="language-objectivec">[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20161009_32"] afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];</code></pre> <p>以上方法添加到当前 Runloop 中,且指定运行模式为 NSRunLoopCommonModes</p> </li> </ul> </li> <li> <p>自动释放池</p> <ul> <li>进入 Runloop 的时候第一次创建</li> <li>退出 Runloop 的时候最后一次释放(超时或线程销毁)</li> <li> <p>其他时候的创建与释放</p> <ul> <li>当 Runloop 即将休眠的时候会把之前的自动释放池释放,再重新创建一个新的自动释放池</li> </ul> <pre> <code class="language-objectivec">void msg(int n) { NSLog(@"runloop被唤醒"); NSLog(@"runloop处理事件---%zd",n); } int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"runloop启动了"); do { NSLog(@"runloop询问,还有事情需要我处理吗?"); NSLog(@"没有事情的话,我就睡觉了"); NSLog(@"runloop进入到休眠"); int number = 0; scanf("%zd",&number); msg(number); } while (1); } return 0; }</code></pre> </li> </ul> </li> </ul> <p> </p> <p>来自:http://www.jianshu.com/p/4bad817df9ae</p> <p> </p>