Runloop详解
RunLoop是iOS和OSX开发中非常基础的一个概念,这篇文章将从源码以及应用入手,介绍RunLoop的概念以及底层实现原理。本人看了一下RunLoop的英语源码,以及借鉴部分优秀博客,感谢!读完这篇文章大约15-25分钟,建议先保存一下。
1.RunLoop概念
根据苹果在文档里的说明(苹果关于RunLoop的官方文档)
一般来讲,一个线程只能执行一个任务,执行完就会退出。RunLoop是事件接收和分发机制的一个实现。RunLoop实际上就是一个对象,(比如触摸事件,UI刷新事件,定时器事件,Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。通常的代码逻辑是这样的:
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); }
这种模型通常被称作 Event Loop。EventLoop在很多系统和框架都有实现,比如Node.js的事件处理,windows程序的消息循环,再比如OSX/iOS里的RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
RunLoop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行了一个入口函数来执行上面的Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
2.RunLoop与线程的关系
RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或者多个任务,但是在默认的情况下:线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop。
(1)一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
(2)我们只能在当前线程中操作当前线程的RunLoop,而不能操作其他线程的RunLoop。
(3)RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
(4)主线程的RunLoop对象系统自动帮助我们创建好了,而子线程的RunLoop对象需要我们主动创建。
在iOS开发中能遇到两个线程对象:pthread_t 和 NSThread。 pthread_t 和 NSThread 是一一对应的。你可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。
苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数:
CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; /// 访问 loopsDic 时的锁 static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } /// 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { /// 取不到时,创建一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
从上面的代码可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
3.RunLoop对外的接口
在CoreFoundation里面关于RunLoop有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
一个RunLoop包含着若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode叫做CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef是时间产生的地方。Source有两个版本:Source0和Source1
Source0只包含了一个回调(函数指针)它并不能主动触发事件。使用时,你想要先调用CFRunLoopSourceSignal(source),将这个Source作为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1包含了一个mach_port和一个回调(函数指针)被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop。
CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer是toll-free bridged 的,可以混用。其包含了一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop }
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
4.RunLoop的Mode
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... };
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时候你想要一个Timer,有两个Mode中都能得到回调,一种办法就是将这个Timer分别加入这两个Mode。还有一种方式,就是将Timer加入顶层的RunLoop的“commonModeItems”中,commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
CFRunLoop对外暴露的管理Mode接口只有下面两个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
你只能通过mode name来操作内部的mode,当你传入一个新的mode name但RunLoop没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。
5.RunLoop的内部逻辑
RunLoop的内部逻辑:
- 通知观察者RunLoop已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
- 通知观察者线程进入休眠状态
- 将线程置于休眠知道任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
- RunLoop设置的时间已经超时
- RunLoop被显示唤醒
- 通知观察者线程将被唤醒
- 处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
- 如果输入源启动,传递相应的消息
- 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
- 通知观察者RunLoop结束。
其内部的代码整理如下:
/// 用DefaultMode启动 void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } /// 用指定的Mode启动,允许设置RunLoop超时时间 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } /// RunLoop的实现 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { /// 首先根据modeName找到对应mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); /// 如果mode里没有source/timer/observer, 直接返回。 if (__CFRunLoopModeIsEmpty(currentMode)) return; /// 1. 通知 Observers: RunLoop 即将进入 loop。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); /// 内部函数,进入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { Boolean sourceHandledThisLoop = NO; int retVal = 0; do { /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); /// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop 触发 Source0 (非port) 回调。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); /// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。 if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。 /// • 一个基于 port 的Source 的事件。 /// • 一个 Timer 到时间了 /// • RunLoop 自身的超时时间到了 /// • 被其他什么调用者手动唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); /// 收到消息,处理消息。 handle_msg: /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。 if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) } /// 9.2 如果有dispatch到main_queue的block,执行block。 else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } } /// 执行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode); if (sourceHandledThisLoop && stopAfterHandle) { /// 进入loop时参数说处理完事件就返回。 retVal = kCFRunLoopRunHandledSource; } else if (timeout) { /// 超出传入参数标记的超时时间了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) { /// 被外部调用者强制停止了 retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { /// source/timer/observer一个都没有了 retVal = kCFRunLoopRunFinished; } /// 如果没超时,mode里没空,loop也没被停止,那继续loop。 } while (retVal == 0); } /// 10. 通知 Observers: RunLoop 即将退出。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }
可以看到,实际上RunLoop就是这样的函数,内部就是个do-while循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
6.RunLoop的底层实现:
从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。
苹果官方将整个系统大致划分为上述4个层次:
应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
应用框架层即开发人员接触到的 Cocoa 等框架。
核心框架层包括各种核心框架、OpenGL 等内容。
Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。
7.苹果用RunLoop实现的功能
(1)AutoreleasePool
App启动之后,苹果在主线程RunLoop里注册了两个observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
(2)事件响应:
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
(3)界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
(4)定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
(5)PerformSelector
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
(6)关于GCD
实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
7.RunLoop的实际应用举例
(1)保证子线程数据回来更新UI的时候,不打断用户的滑动操作。
- tableView在滑动,处于UITrackingRunloopMode模式下。
- 子线程请求的数据,那么在和主线程处理的时候,我们将更新的逻辑加载defaultMode下,那么defaultMode下的操作是不会执行的。
- 滑动结束了,runloop由UITrackingRunloopMode又回到defaultMode下了,那么defaultMode下的更新操作就能执行了 。
(2)Runloop常驻线程
在项目开发中我们通常把耗时操作方在子线程中运行,我们就接着上面的那个代码中模拟耗时操作。在主线程操作这些,会看到比较卡
NSTimer * timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) { static int count = 0; [NSThread sleepForTimeInterval:1]; NSLog(@"%s - %d",__func__,count++); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
建立子线程(需要手动执行)
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer * timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) { static int count = 0; [NSThread sleepForTimeInterval:1]; //休息一秒钟,模拟耗时操作 NSLog(@"%s - %d",__func__,count++); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //子线程需要手动开启Runloop [[NSRunLoop currentRunLoop] run]; });
这样子我们就可以实现在子线程处理耗时操作,并且常驻线程了。运行起来比较顺畅。
拓展:Runloop的性能优化
tableView的Cell中有多个ImageView,同时加载大图,导致UI卡顿。
解决思路:使用Runloop每次循环址添加一张图片。
工具:这里我们需要使用到CFRunloop
实现过程:
1、把加载图片等代码保存起来,先不执行 (保存一段代码,block)
2、监听Runloop循环(CFRunloopObserver)
3、每次都从任务数组中取出一个加载图片等代码执行(执行block代码)
(1)监听Runloop
//添加runloop监听者 - (void)addRunloopObserver{ // 获取 当前的Runloop ref - 指针 CFRunLoopRef current = CFRunLoopGetCurrent(); //定义一个RunloopObserver CFRunLoopObserverRef defaultModeObserver; //上下文 /* typedef struct { CFIndex version; //版本号 long void * info; //这里我们要填写对象(self或者传进来的对象) const void *(*retain)(const void *info); //填写&CFRetain void (*release)(const void *info); //填写&CGFRelease CFStringRef (*copyDescription)(const void *info); //NULL } CFRunLoopObserverContext; */ CFRunLoopObserverContext context = { 0, (__bridge void *)(self), &CFRetain, &CFRelease, NULL }; /* 1 NULL空指针 nil空对象 这里填写NULL 2 模式 kCFRunLoopEntry = (1UL << 0), kCFRunLoopBeforeTimers = (1UL << 1), kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7), kCFRunLoopAllActivities = 0x0FFFFFFFU 3 是否重复 - YES 4 nil 或者 NSIntegerMax - 999 5 回调 6 上下文 */ // 创建观察者 defaultModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, NSIntegerMax - 999, &Callback, &context); //添加当前runloop的观察着 CFRunLoopAddObserver(current, defaultModeObserver, kCFRunLoopDefaultMode); //释放 CFRelease(defaultModeObserver); }
我们要实现回调方法
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ //通过info桥接为当前的对象 PQRunloop * runloop = (__bridge PQRunloop *)info; //如果没有任务,就直接返回 if (runloop.tasks.count == 0) { return; } BOOL result = NO; while (result == NO && runloop.tasks.count) { //取出任务 RunloopBlock unit = runloop.tasks.firstObject; //执行任务 result = unit(); //删除任务 [runloop.tasks removeObjectAtIndex:0]; } } 因为在C方法中没有办法调用OC对象,所以context中有一个void * info,为的就是解决这个问题,把OC的对象传入到回调方法中。
通过上面的两个方法我们可以做到监听Runloop循环,以及每次循环需要处理的事情,这个时候我们只需要对外提供一个添加任务的方法,用数组保存起来。
-
//add task 添加任务 - (void)addTask:(RunloopBlock)unit withId:(id)key{ //添加任务到数组 [self.tasks addObject:unit]; [self.taskKeys addObject:key]; //为了保证加载到图片最大数是18所以要删除 if (self.tasks.count > self.maxQueue) { [self.tasks removeObjectAtIndex:0]; [self.taskKeys removeObjectAtIndex:0]; } }
,Runloop在执行后如果没有唤醒操作,就会进入睡眠状态,也就是歇菜了,啥都不干了,所以为了能让Runloop能一直跑,我们需要创建一个定时器,但是最好不要在定时器中作操作(1 耗时,2 耗电)
所以我们还需要: -
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:YES block:^(NSTimer * _Nonnull timer) { }];
这样就可以实现较高的性能,性能提高10几倍。
以上就是Runloop的知识点,今天是端午节假日,祝大家端午节安康!