【iOS】-- Runloop初学

打印 上一主题 下一主题

主题 802|帖子 802|积分 2406

RunLoop简介

简单的说RunLoop是一种高级的循环机制,让步伐持续运行,并处理步伐中的各种变乱,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
在App运行的过程中,主线程的Runloop保证了主线程不被烧毁从而保证应用的存活,从而能实时接收到用户的相应变乱,可以或许触发定时变乱。如果没有Runloop的话,步伐执行完代码就会立马return。
RunLoop基本使用



  • 保持步伐的持续运行。
  • 处理app中各种变乱。
  • 节省CPU资源,提高步伐性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU。
RunLoop的伪代码

  1. int main(int argc, char *argv[]) {
  2.         @atuoreleasepool {
  3.                 int retVal = 0;
  4.                 do {
  5.                         // 睡眠中等待消息
  6.                         int message = sleep_and_wait();
  7.                         // 处理消息
  8.                         retVal = process_message(message);
  9.                 } while (0 == retVal);
  10.                 return 0;
  11.         }
  12. }
复制代码
Runloop会一直在do-while循环中执行,这也就是我们写的步伐不会在执行完一次代码之后就退出的缘故原由了。
Runloop模子图

官方给的图:

Runloop对象

runloop现实上是一个对象,该对象管理其需要处理的消息和变乱,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。
Runloop对象的获取

Runloop对象重要有两种获取方式:
  1. // Foundation
  2. NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
  3. NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
复制代码
NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
  1. // Core Foundation
  2. CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
  3. CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象
复制代码
CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,全部这些API都是线程安全。

CoreFoundation框架中这两个函数的具体实现:
  1. CFRunLoopRef CFRunLoopGetCurrent(void) {
  2.     CHECK_FOR_FORK();
  3.     CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
  4.     if (rl) return rl;
  5.     return _CFRunLoopGet0(pthread_self());
  6. }
  7. CFRunLoopRef CFRunLoopGetMain(void) {
  8.     CHECK_FOR_FORK();
  9.     static CFRunLoopRef __main = NULL; // no retain needed
  10.     if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
  11.     return __main;
  12. }
复制代码
可以看出上面这两个方法,都掉用了_CFRunLoopGet0这个方法。
_CFRunLoopGet0方法

  1. //全局的Dictionary,key是pthread_t,value是CFRunLoopRef
  2. static CFMutableDictionaryRef __CFRunLoops = NULL;
  3. //访问__CFRunLoops的锁
  4. static CFSpinLock_t loopsLock = CFSpinLockInit;
  5. // should only be called by Foundation
  6. // t==0 is a synonym for "main thread" that always works
  7. //t==0是始终有效的“主线程”的同义词
  8. //获取pthread对应的RunLoop
  9. CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
  10.     if (pthread_equal(t, kNilPthreadT)) {
  11.     //pthread为空时,获取主线程
  12.     t = pthread_main_thread_np();
  13.     }
  14.     __CFSpinLock(&loopsLock);
  15.     //如果这个__CFRunLoops字典不存在,即程序刚开始运行
  16.     if (!__CFRunLoops) {
  17.         __CFSpinUnlock(&loopsLock);
  18.     //第一次进入时,创建一个临时字典dict
  19.     CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
  20.     //根据传入的主线程,获取主线程对应的RunLoop
  21.     CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
  22.     //保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中
  23.     CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
  24.     //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
  25.     if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
  26.             //释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉
  27.         CFRelease(dict);
  28.     }
  29.     //释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了
  30.     CFRelease(mainLoop);
  31.         __CFSpinLock(&loopsLock);
  32.     }
  33.     //以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建
  34.         //从全局字典里获取对应的RunLoop
  35.     CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  36.     __CFSpinUnlock(&loopsLock);
  37.     //如果找不到对应的Runloop
  38.     if (!loop) {
  39.     //创建一个该线程的Runloop
  40.     CFRunLoopRef newLoop = __CFRunLoopCreate(t);
  41.         __CFSpinLock(&loopsLock);
  42.     //再次在__CFRunLoops中查找该线程的Runloop
  43.     loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  44.     //如果在字典中还是找不到该线程的Runloop
  45.     if (!loop) {
  46.             //把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程t
  47.         CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
  48.         //并且让loop指向刚才创建的Runloop
  49.         loop = newLoop;
  50.     }
  51.         // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
  52.         __CFSpinUnlock(&loopsLock);
  53.             //loop已经指向这个newLoop了,他也就可以释放了
  54.             CFRelease(newLoop);
  55.     }
  56.         //如果传入线程就是当前线程
  57.     if (pthread_equal(t, pthread_self())) {
  58.         _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
  59.         if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
  60.                 //注册一个回调,当线程销毁时,销毁对应的RunLoop
  61.             _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
  62.         }
  63.     }
  64.     //返回该线程对应的Runloop
  65.     return loop;
  66. }
复制代码
上面这个方法就是为了获取对应线程的Runloop,然后将该RunLoop添加到上面这个全局变量的字典中,方便后面获取runloop时,可以直接在字典中查找。
大概流程如下图所示:

从这部分源码我们可以得知:


  • 每条线程都有唯一的一个与之对应的RunLoop对象。
  • RunLoop保存一个全局的Dictionary里,线程作为key,RunLoop作为value。
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取线程的RunLoop时创建,RunLoop会在线程结束时烧毁。
  • 主线程的RunLoop已经主动获取(创建),子线程默认没有开启RunLoop。
  • RunLoop会在线程结束时烧毁。
Runloop的相关类

与RunLoop相关的类有5个:


  • CFRunLoopRef 代表了RunLoop的对象。
  • CFRunLoopModeRef RunLoop的运行模式。
  • CFRunLoopSourceRef 就是RunLoop模子图中提到的输入源(变乱源)。
  • CFRunLoopTimerRef (定时源)。
  • CFRunLoopObserverRef观察者,监听RunLoop状态的改变。
1.一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer

2.每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作CurrentMode
3.如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做重要是为了分隔开差别组的Source/Timer/Observer,让其互不影响
4.如果一个mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环。
RunLoop相关类的实现

CFRunLoopRef

看一下CFRunLoopRef的源码:
  1. struct __CFRunLoop {
  2.     CFRuntimeBase _base;
  3.     pthread_mutex_t _lock;                        /* locked for accessing mode list */
  4.     __CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
  5.     Boolean _unused;
  6.     volatile _per_run_data *_perRunData;              // reset for runs of the run loop
  7.     pthread_t _pthread;                 // runloop对应的线程
  8.     uint32_t _winthread;
  9.     CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的mode
  10.     CFMutableSetRef _commonModeItems; // 存储所有commonMode的item(source、timer、observer)
  11.     CFRunLoopModeRef _currentMode; // 当前运行的mode
  12.     CFMutableSetRef _modes;           // 装着一堆CFRunLoopModeRef类型
  13.     struct _block_item *_blocks_head; // do blocks时用到
  14.     struct _block_item *_blocks_tail;
  15.     CFAbsoluteTime _runTime;
  16.     CFAbsoluteTime _sleepTime;
  17.     CFTypeRef _counterpart;
  18. };
复制代码
Runloop中除了纪录了一些属性外,重点还是一下几个:
  1. pthread_t _pthread;                 // runloop对应的线程
  2. CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的mode
  3. CFMutableSetRef _commonModeItems; // 存储所有common标记的mode的item(source、timer、observer)
  4. CFRunLoopModeRef _currentMode; // 当前运行的mode
  5. CFMutableSetRef _modes;           // 装着一堆CFRunLoopModeRef类型,runloop中的所有模式
复制代码
RunLoop中重要的变量就是_pthread、_currentMode、_modes,_currentMode重要就是为了在_modes中找当前对应的mode的item,然后发送消息。而_commonModes和_commonModeItems完全就是为了common标记mode准备的,如果我们选择的mode是commonMode,那么就不消在_modes中找每个mode对应的item了,由于被标记的mode的item都在_commonModeItems中,直接给他里边的全部item发消息就完了。
CFRunLoopModeRef

  1. typedef struct __CFRunLoopMode *CFRunLoopModeRef;
  2. struct __CFRunLoopMode {
  3.     CFRuntimeBase _base;
  4.     pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
  5.     CFStringRef _name; //mode名称,运行模式是通过名称来识别的
  6.     Boolean _stopped; //mode是否被终止
  7.     char _padding[3];
  8.     //整个结构体最核心的部分
  9. ------------------------------------------
  10.     CFMutableSetRef _sources0; // Sources0
  11.     CFMutableSetRef _sources1; // Sources1
  12.     CFMutableArrayRef _observers; // 观察者
  13.     CFMutableArrayRef _timers; // 定时器
  14. ------------------------------------------
  15.     CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef
  16.     __CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
  17.     CFIndex _observerMask;
  18. #if USE_DISPATCH_SOURCE_FOR_TIMERS
  19.     dispatch_source_t _timerSource;
  20.     dispatch_queue_t _queue;
  21.     Boolean _timerFired; // set to true by the source when a timer has fired
  22.     Boolean _dispatchTimerArmed;
  23. #endif
  24. #if USE_MK_TIMER_TOO
  25.     mach_port_t _timerPort;
  26.     Boolean _mkTimerArmed;
  27. #endif
  28. #if DEPLOYMENT_TARGET_WINDOWS
  29.     DWORD _msgQMask;
  30.     void (*_msgPump)(void);
  31. #endif
  32.     uint64_t _timerSoftDeadline; /* TSR */
  33.     uint64_t _timerHardDeadline; /* TSR */
  34. };
复制代码
一个CFRunLoopModeRef对象有一个name,若干source0,source1,timer,observer和port,可以看出来变乱都是由mode在管理,而RunLoop管理着Mode。
Mode现实上是Source,Timer 和 Observer 的聚集,差别的Mode把差别组的Source、timer、Observer隔绝开来。runloop在某一时候只能运行在一个mode下,处理这一个mode中的source、timer、observer。

五种运行模式

体系默认注册的五个mode:
  1. NSRunLoopCommonModes
  2. // 一个伪模式,包含一个或多个其他运行循环模式。
  3. NSString * const NSRunLoopCommonModes = @"kCFRunLoopCommonModes";
  4. NSDefaultRunLoopMode
  5. // 用于处理除连接对象之外的输入源的模式。
  6. NSString * const NSDefaultRunLoopMode = @"kCFRunLoopDefaultMode";
  7. NSEventTrackingRunLoopMode
  8. // 在模态跟踪事件(例如鼠标拖动)时设置的模式。
  9. NSString * const NSEventTrackingRunLoopMode = @"kCFRunLoopEventTrackingMode";
  10. NSModalPanelRunLoopMode
  11. // 等待模态面板(例如保存或打开面板)的输入时设置的模式。
  12. NSString * const NSModalPanelRunLoopMode = @"kCFRunLoopModalPanelMode";
  13. UITrackingRunLoopMode
  14. // 在进行控件跟踪时设置的模式。
  15. NSString * const UITrackingRunLoopMode = @"kCFRunLoopUITrackingMode";
复制代码
另有UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode。


  • 常用的3个Mode:

    • NSDefaultRunLoopMode, 默认的模式, 有变乱相应的时候, 会阻塞旧变乱。
    • NSRunLoopCommonModes, 平常模式, 不会影响任何变乱。
    • UITrackingRunLoopMode, 只能是有变乱的时候才会相应的模式。

CommonModes

在RunLoop对象中,前面有一个有一个叫CommonModes的概念,它纪录了全部标记为common的mode:
  1. //简化版本
  2. struct __CFRunLoop {
  3.     pthread_t _pthread;
  4.     CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
  5.     CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
  6.     CFRunLoopModeRef _currentMode;//当前运行的mode
  7.     CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
  8. };
复制代码
一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的全部Mode里。其底层原理如下:
  1. void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
  2.     CHECK_FOR_FORK();
  3.     if (__CFRunLoopIsDeallocating(rl)) return;
  4.     __CFRunLoopLock(rl);
  5.     if (!CFSetContainsValue(rl->_commonModes, modeName)) {
  6.     //获取所有的_commonModeItems
  7.     CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
  8.     //获取所有的_commonModes
  9.     CFSetAddValue(rl->_commonModes, modeName);
  10.     if (NULL != set) {
  11.         CFTypeRef context[2] = {rl, modeName};
  12.         //将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
  13.         CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
  14.         CFRelease(set);
  15.     }
  16.     } else {
  17.     }
  18.     __CFRunLoopUnlock(rl);
  19. }
复制代码
CFRunLoop对外暴露的管理Mode接口只有下面两个:
  1. CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
  2. CFRunLoopRunInMode(CFStringRef modeName, ...);
复制代码
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName):
该函数用于向指定的 RunLoop 添加一个常用运行模式(Common Mode)。运行模式是用于标识一组变乱源和定时器的聚集,以确定哪些变乱应该被 RunLoop 处理。通过添加常用运行模式,可以将变乱源和定时器同时添加到多个运行模式中,以确保在这些运行模式下都能被精确处理。
CFRunLoopRunInMode(CFStringRef modeName, …):
该函数用于运行指定的运行模式下的 RunLoop它会使当前线程进入一个循环,不停处理该运行模式下的变乱,直到满足退出条件。
这两个函数结合使用可以实现对 RunLoop 的管理和调理。CFRunLoopAddCommonMode 用于添加常用的运行模式,将变乱源和定时器添加到多个运行模式中,以确保它们都能被精确处理。而 CFRunLoopRunInMode 则用于运行指定的运行模式下的 RunLoop,使当前线程进入循环,并处理该运行模式下的变乱。
什么是Mode Item?Mode到底包含哪些类型的元素?


  • RunLoop需要处理的消息,包括time以及source消息,他们都属于Mode item。
  • RunLoop也可以被监听,被监听的对象是observer对象,也属于Mode item。
  • 全部的mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有用果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环。
  • mode暴露的mode item的接口有下面几个:
  1. CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
  2. CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
  3. CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
  4. CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
  5. CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
  6. CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
复制代码
我们仅能通过操作mode name来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应的mode时,RunLoop会主动帮你创建对应的CFRunLoopModeRef
CFRunLoopSourceRef

根据官方描述,CFRunLoopSourceRef是input sources的抽象。
CFRunLoopSource分为Source0和Source1两个版本。
它的布局如下:
  1. struct __CFRunLoopSource {
  2.     CFRuntimeBase _base;
  3.     uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态才会被处理
  4.     pthread_mutex_t _lock;
  5.     CFIndex _order;   //执行顺序
  6.     CFMutableBagRef _runLoops;//包含多个RunLoop
  7.     //版本
  8.     union {
  9.     CFRunLoopSourceContext version0;    /* immutable, except invalidation */
  10.         CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
  11.     } _context;
  12. };
复制代码
可一通过共用体union看出,它有两个版本,Source0和Source1:
Source0

Source0只包含了一个回调(函数指针),它并不能主动触发变乱。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来叫醒RunLoop,让其处理这个变乱。
Source0是App内部变乱,由App自己管理的UIEvent、CFSocket都是source0。当一个source0变乱准备执行时,必须要先把它标为signal状态,以下是source0布局体:
  1. typedef struct {
  2.     CFIndex        version;
  3.     void *        info;
  4.     const void *(*retain)(const void *info);
  5.     void        (*release)(const void *info);
  6.     CFStringRef        (*copyDescription)(const void *info);
  7.     Boolean        (*equal)(const void *info1, const void *info2);
  8.     CFHashCode        (*hash)(const void *info);
  9.     void        (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
  10.     void        (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
  11.     void        (*perform)(void *info);
  12. } CFRunLoopSourceContext;
复制代码
Source1

Source1包含了mach_port和一个回调(函数指针),Source1可以监听体系端口,通过内核和其他线程通讯,接收、分发体系变乱,他能主动叫醒RunLoop(由操作体系内核举行管理)
Source1在处理的时候会分发一些操作给Source0去处理。
source1布局体:
  1. typedef struct {
  2.     CFIndex        version;
  3.     void *        info;
  4.     const void *(*retain)(const void *info);
  5.     void        (*release)(const void *info);
  6.     CFStringRef        (*copyDescription)(const void *info);
  7.     Boolean        (*equal)(const void *info1, const void *info2);
  8.     CFHashCode        (*hash)(const void *info);
  9. #if TARGET_OS_OSX || TARGET_OS_IPHONE
  10.     mach_port_t        (*getPort)(void *info);
  11.     void *        (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
  12. #else
  13.     void *        (*getPort)(void *info);
  14.     void        (*perform)(void *info);
  15. #endif
  16. } CFRunLoopSourceContext1;
复制代码
两者关系与区别:

Source0:


  • Source0是非基于Port的变乱源,用于处理应用步伐内部生成的变乱
  • 它通常用于相应和处理一些特定的应用步伐级变乱,如触摸变乱、用户交互变乱等。
  • Source0变乱通过调用相应的变乱处理方法来执行特定的操作,比如调用视图的touchesBegan:withEvent:方法来处理触摸变乱。
  • Source0变乱是由应用步伐自身生成和处理的,不涉及历程间通讯
Source1:


  • Source1是基于Port的变乱源,用于处理与其他历程或内核之间的交互
  • 它用于处理一些体系级变乱,如网络数据到达、定时器变乱、文件描述符的可读或可写状态等。
  • Source1变乱通常由体系或其他历程发送给当前应用步伐,需要通过RunLoop举行接收和处理。
  • Source1变乱的处理涉及与其他历程或内核的通讯,比如举行网络数据的读写、处理文件描述符的状态变化等。
例:
一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的变乱会先包装成Event,Event先告诉source1(mach_port),source1叫醒RunLoop,然后将变乱Event分发给source0,然后由source0来处理。
CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被叫醒以执行谁人回调。
并且CFRunLoopTimer和NSTimer是toll-free bridged(对象桥接),可以相互转换。其布局如下:
  1. struct __CFRunLoopTimer {
  2.     CFRuntimeBase _base;
  3.     uint16_t _bits;
  4.     pthread_mutex_t _lock;
  5.     CFRunLoopRef _runLoop;
  6.     CFMutableSetRef _rlModes;//包含timer的mode集合
  7.     CFAbsoluteTime _nextFireDate;
  8.     CFTimeInterval _interval;        /* immutable */
  9.     CFTimeInterval _tolerance;          /* mutable */
  10.     uint64_t _fireTSR;            /* TSR units */
  11.     CFIndex _order;            /* immutable */
  12.     CFRunLoopTimerCallBack _callout; //timer的回调   
  13.     CFRunLoopTimerContext _context;   //上下文对象
  14. };
复制代码
对于NSTimer
scheduledTimerWithTimeInterval和RunLoop的关系:
  1. [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
复制代码
体系会将NSTimer主动加入NSDefaultRunLoopMode模式中,所以它就等同于下面代码:
  1. NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
  2. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
复制代码
NSTimer在滑动时停止工作的问题
在写定时无限轮播图时,需要添加定时器,但是在手动滑动或者按住屏幕时,定时器就会被打断,从而不会继承轮播。
我们来写个例子:
  1. self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(50, 80, 300, 200)];
  2.     self.scrollView.scrollEnabled = YES;
  3.     self.scrollView.pagingEnabled = YES;
  4.     self.scrollView.bounces = YES;
  5.     self.scrollView.alwaysBounceHorizontal = NO;
  6.     self.scrollView.alwaysBounceVertical = NO;
  7.     self.scrollView.showsHorizontalScrollIndicator = NO;
  8.     self.scrollView.contentSize = CGSizeMake(900, 200);
  9.     self.scrollView.backgroundColor = [UIColor yellowColor];
  10.     [self.view addSubview:self.scrollView];
  11.    
  12.     static int count = 0;
  13.     NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
  14.         NSLog(@"%d", ++count);
  15.     }];
复制代码
输出结果:

可以看出在第四秒和第五秒之间我们拖动,timer就停止了。
造成这种问题的缘故原由就是:


  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下
  • 当我们举行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其变乱,所以我们的NSTimer就不工作了
  • 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了
要想解决这个问题,我们直接让NSTimer在两种mode下都能工作就完了,
  1. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  2. [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
复制代码
修改之后就会一直正常运行了。
CFRunLoopObserverRef

CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当runloop的状态发生变化时,观察者就能通过回调接收到这个变化。
  1. typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
  2. struct __CFRunLoopObserver {
  3.     CFRuntimeBase _base;
  4.     pthread_mutex_t _lock;
  5.     CFRunLoopRef _runLoop;//监听的RunLoop
  6.     CFIndex _rlCount;//添加该Observer的RunLoop对象个数
  7.     CFOptionFlags _activities;                /* immutable */
  8.     CFIndex _order;//同时间最多只能监听一个
  9.     CFRunLoopObserverCallBack _callout;//监听的回调
  10.     CFRunLoopObserverContext _context;//上下文用于内存管理
  11. };
复制代码
RunLoop的六种状态

  1. //观测的时间点有一下几个
  2. typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
  3.     kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
  4.     kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
  5.     kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
  6.     kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
  7.     kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
  8.     kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
  9.     kCFRunLoopAllActivities = 0x0FFFFFFFU
  10. };
复制代码
这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊变乱,创建监听,监听RunLoop的状态变化:
  1. // 创建observer
  2. CFRunLoopObserverRef ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
  3.     switch (activity) {
  4.         case kCFRunLoopEntry:
  5.             NSLog(@"kCFRunLoopEntry");
  6.             break;
  7.         case kCFRunLoopBeforeTimers:
  8.             NSLog(@"kCFRunLoopBeforeTimers");
  9.             break;
  10.         case kCFRunLoopBeforeSources:
  11.             NSLog(@"kCFRunLoopBeforeSources");
  12.             break;
  13.         case kCFRunLoopBeforeWaiting:
  14.             NSLog(@"kCFRunLoopBeforeWaiting");
  15.             break;
  16.         case kCFRunLoopAfterWaiting:
  17.             NSLog(@"kCFRunLoopAfterWaiting");
  18.             break;
  19.         case kCFRunLoopExit:
  20.             NSLog(@"kCFRunLoopExit");
  21.             break;
  22.         default:
  23.             break;
  24.     }
  25. });
  26. // 添加observer到runloop中
  27. CFRunLoopAddObserver(CFRunLoopGetMain(), ob, kCFRunLoopCommonModes);
  28. CFRelease(ob);
复制代码
输出结果:

RunLoop是在不停的监听状态并做出反应。
RunLoop实现逻辑

RunLoop的内部逻辑如下:

__CFRunLoopRun源码实现

精简后的__CFRunLoopRun函数,保留了重要代码,看一下具体实现:
  1. //用DefaultMode启动
  2. void CFRunLoopRun(void) {
  3.     CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
  4. }
  5. //用指定的Mode启动,允许设置RunLoop的超时时间
  6. int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
  7.     return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
  8. }
  9. //RunLoop的实现
  10. int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
  11.         //首先根据modeName找到对应的mode
  12.         CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
  13.         //如果该mode中没有source/timer/observer,直接返回
  14.         if (__CFRunLoopModeIsEmpty(currentMode)) return;
  15.        
  16.         //1.通知Observers:RunLoop即将进入loop
  17.         __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
  18.         //调用函数__CFRunLoopRun 进入loop
  19.         __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
  20.         
  21.         Boolean sourceHandledThisLoop = NO;
  22.         int retVal = 0;
  23.         do {
  24.                         //2.通知Observers:RunLoop即将触发Timer回调
  25.                         __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
  26.                         //3.通知Observers:RunLoop即将触发Source0(非port)回调
  27.                         __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
  28.                         ///执行被加入的block
  29.                         __CFRunLoopDoBlocks(runloop, currentMode);
  30.                         //4.RunLoop触发Source0(非port)回调,处理Source0
  31.                         sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
  32.                         //执行被加入的Block
  33.                         __CFRunLoopDoBlocks(runloop, currentMode);
  34.                         //5.如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息
  35.                         if (__Source0DidDispatchPortLastTime) {
  36.                 Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
  37.                 if (hasMsg) goto handle_msg;
  38.             }
  39.                         //6.通知Observers:RunLoop的线程即将进入休眠
  40.                         if (!sourceHandledThisLoop) {
  41.                 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
  42.             }
  43.                         //7.调用mach_msg等待接收mach_port的消息。线程将进入休眠,直到被下面某个事件唤醒:
  44.                         // 一个基于port的Source的事件
  45.                         // 一个Timer时间到了
  46.                         // RunLoop自身的超时时间到了
  47.                         // 被其他什么调用者手动唤醒
  48.                         __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
  49.                 mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
  50.             }
  51.                         //8.通知Observers:RunLoop的线程刚刚被唤醒
  52.                         __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
  53.                         //收到消息,处理消息
  54.                         handle_msg:
  55.                         //9.1 如果一个Timer时间到了,触发这个timer的回调
  56.                         if (msg_is_timer) {
  57.                 __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
  58.             }
  59.                         //9.2 如果有dispatch到main_queue的block,执行block
  60.                         else if (msg_is_dispatch) {
  61.                 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
  62.             }
  63.             //9.3 如果一个Source1(基于port)发出事件了,处理这个事件
  64.             else {
  65.                 CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
  66.                 sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
  67.                 if (sourceHandledThisLoop) {
  68.                     mach_msg(reply, MACH_SEND_MSG, reply);
  69.                 }
  70.             }
  71.             //执行加入到loop的block
  72.             __CFRunLoopDoBlocks(runloop, currentMode);
  73.                         //设置do-while之后的返回值
  74.                         if (sourceHandledThisLoop && stopAfterHandle) {
  75.                 // 进入loop时参数说处理完事件就返回
  76.                 retVal = kCFRunLoopRunHandledSource;
  77.             } else if (timeout) {
  78.                 // 超出传入参数标记的超时时间了
  79.                 retVal = kCFRunLoopRunTimedOut;
  80.             } else if (__CFRunLoopIsStopped(runloop)) {
  81.                 // 被外部调用者强制停止了
  82.                 retVal = kCFRunLoopRunStopped;
  83.             } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
  84.                 // source/timer/observer一个都没有了
  85.                 retVal = kCFRunLoopRunFinished;
  86.             }
  87.                         // 如果没超时,mode里没空,loop也没被停止,那继续loop。
  88.                  } while (retVal == 0);
  89.     }
  90.         //10. 通知Observers:RunLoop即将退出
  91.         __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
  92. }
复制代码
现实上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回。
Runloop与现线程的关系


  • 一条线程对应一个RunLoop对象, 每条线程都有唯一一个与之对应的RunLoop对象。
  • RunLoop并不保证线程安全. 我们只能在当前线程内部操作当前线程的RunLoop对象, 而不能在当前线程内部去操作其他线程的RunLoop对象方法。
  • RunLoop对象在第一次获取RunLoop时创建, 烧毁则是在线程结束的时候。
  • 主线程的RunLoop对象体系主动资助我们创建好了(UIApplicationMain函数), 子线程的RunLoop对象需要我们主动创建和维护。
RunLoop启动方法



  • 1.run,无条件

    • 无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环自己。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。

  • 2.runUntilDate, 设置时间限定

    • 设置了超时时间,凌驾这个时间runloop结束,优于第一种

  • 3.runMode:beforeDate:,在特定模式下

    • 相对比力好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会主动退出,上述两种方式都是循环调用的
    • 现实上run方法的实现就是无限调用runMode:beforeDate:方法
    • runUntilDate:也会重复调用runMode:beforeDate:方法,区别在于它超时就不会再调用

RunLoop关闭方法



  • 1.将运行循环设置为使用超时值运行。
  • 2.手动停止。
这里需要注意,固然删除runloop的输入源和定时器大概会导致运行循环的退出,但这并不是个可靠的方法,体系大概会添加输入源到runloop中,但在我们的代码中大概并不知道这些输入源,因此无法删除它们,导致无法退出runloop。
我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超机遇制,这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用,而不会结束后续runloop的调用。
ImageView延长显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么大概出现卡顿的情况。
我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示
  1. [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
复制代码
用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。
由于限定了方法setImage只能在NSDefaultRunLoopMode模式下使用而滚动tableview的时候,步伐运行在tracking模式下面,所以方法setImage不会执行。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

干翻全岛蛙蛙

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表