网站建设 河南,wordpress dux 5.3,淮阴区住房和城乡建设局网站,百度推广关键词和创意Runloop概述
一般来说#xff0c;一个线程一次只能执行一个任务#xff0c;执行完成后线程就会退出。就比如之前学OC时使用的命令行程序#xff0c;执行完程序就结束了。 而runloop目的就是使线程在执行完一次代码之后不会结束程序#xff0c;而是使该线程处于一种休眠的状…Runloop概述
一般来说一个线程一次只能执行一个任务执行完成后线程就会退出。就比如之前学OC时使用的命令行程序执行完程序就结束了。 而runloop目的就是使线程在执行完一次代码之后不会结束程序而是使该线程处于一种休眠的状态等待有事件需要处理的时候再醒来处理。
简单的来说runloop可以让线程在需要做事的时候忙起来不需要的时候让线程休眠使程序不会结束。
Runloop基本作用
保持程序的持续运行处理app中各种事件节省CPU资源提高程序性能该做事时做事该休眠时休眠。并且休眠时不占用CPU
Runloop伪代码
int main(int argc, char *argv[]) {atuoreleasepool {int retVal 0;do {// 睡眠中等待消息int message sleep_and_wait();// 处理消息retVal process_message(message);} while (0 retVal);return 0;}
}Runloop会一直在do-while循环中执行这也就是我们写的程序不会在执行完一次代码之后就退出的原因了。
Runloop模型图
看一下苹果官方给出的RunLoop模型图
Runloop对象
runloop实际上是一个对象这个对象管理了其需要处理的事件和消息并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后就会处于这个函数内部的循环中直到循环结束函数返回
Runloop对象的获取
Runloop对象主要有两种获取方式
// Foundation
NSRunLoop *runloop [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象NSRunloop类是Fundation框架中Runloop的对象并且NSRunLoop是基于CFRunLoopRef的封装提供了面向对象的API但是这些API不是线程安全的。
// Core Foundation
CFRunLoopRef runloop CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop CFRunLoopGetMain(); // 获得主线程的RunLoop对象CFRunLoopRef类是CoreFoundation框架中Runloop的对象并且其提供了纯C语言函数的API所有这些API都是线程安全 看一下CoreFoundation框架中这两个函数的具体实现
CFRunLoopRef CFRunLoopGetCurrent(void) {CHECK_FOR_FORK();CFRunLoopRef rl (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);if (rl) return rl;return _CFRunLoopGet0(pthread_self());
}CFRunLoopRef CFRunLoopGetMain(void) {CHECK_FOR_FORK();static CFRunLoopRef __main NULL; // no retain neededif (!__main) __main _CFRunLoopGet0(pthread_main_thread_np()); // no CAS neededreturn __main;
}发现其都调用了_CFRunLoopGet0这个方法顺便就来看看
_CFRunLoopGet0方法
//全局的Dictionarykey是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock CFSpinLockInit;// should only be called by Foundation
// t0 is a synonym for main thread that always works
//t0是始终有效的“主线程”的同义词//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {if (pthread_equal(t, kNilPthreadT)) {//pthread为空时获取主线程t pthread_main_thread_np();}__CFSpinLock(loopsLock);//如果这个__CFRunLoops字典不存在即程序刚开始运行if (!__CFRunLoops) {__CFSpinUnlock(loopsLock);//第一次进入时创建一个临时字典dictCFMutableDictionaryRef dict CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, kCFTypeDictionaryValueCallBacks);//根据传入的主线程获取主线程对应的RunLoopCFRunLoopRef mainLoop __CFRunLoopCreate(pthread_main_thread_np());//保存主线程的Runloop将主线程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//此处NULL和__CFRunLoops指针都指向NULL匹配所以将dict写到__CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)__CFRunLoops)) {//释放dict因为我们已经将dict的内存保存了该临时变量也就没用了要及时的释放掉CFRelease(dict);}//释放mainRunLoop刚才用于获取主线程的Runloop已经保存了就可以释放了CFRelease(mainLoop);__CFSpinLock(loopsLock);}//以上说明第一次进来的时候不管是getMainRunLoop还是get子线程的runLoop主线程的runLoop总是会被创建//从全局字典里获取对应的RunLoopCFRunLoopRef loop (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFSpinUnlock(loopsLock);//如果找不到对应的Runloopif (!loop) {//创建一个该线程的RunloopCFRunLoopRef newLoop __CFRunLoopCreate(t);__CFSpinLock(loopsLock);//再次在__CFRunLoops中查找该线程的Runlooploop (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//如果在字典中还是找不到该线程的Runloopif (!loop) {//把刚创建的该线程的newLoop存入字典__CFRunLoopskey是线程tCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且让loop指向刚才创建的Runlooploop newLoop;}// dont release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it__CFSpinUnlock(loopsLock);//loop已经指向这个newLoop了他也就可以释放了CFRelease(newLoop);}//如果传入线程就是当前线程if (pthread_equal(t, pthread_self())) {_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);if (0 _CFGetTSD(__CFTSDKeyRunLoopCntr)) {//注册一个回调当线程销毁时销毁对应的RunLoop_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);}}//返回该线程对应的Runloopreturn loop;
}从学姐的学姐那里偷的通过这段源码我们可以看到
每条线程都有唯一的一个与之对应的RunLoop对象RunLoop保存在一个全局的Dictionary里线程作为keyRunLoop作为value线程刚创建时并没有RunLoop对象RunLoop会在第一次获取线程的RunLoop时创建RunLoop会在线程结束时销毁主线程的RunLoop已经自动获取创建子线程默认没有开启RunLoop主线程的runloop在程序运行启动时就会启动在main.m函数中通过UIApplicationMain开启主线程的 runloop
Runloop与线程的关系 CFRunLoopRef源码部分
看一下CFRunLoopRef的源码
struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock; /* locked for accessing mode list */__CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloopBoolean _unused;volatile _per_run_data *_perRunData; // reset for runs of the run looppthread_t _pthread; // runloop对应的线程uint32_t _winthread;CFMutableSetRef _commonModes; // 存储的是字符串记录所有标记为common的modeCFMutableSetRef _commonModeItems; // 存储所有commonMode的itemsource、timer、observerCFRunLoopModeRef _currentMode; // 当前运行的modeCFMutableSetRef _modes; // 装着一堆CFRunLoopModeRef类型struct _block_item *_blocks_head; // do blocks时用到struct _block_item *_blocks_tail;CFAbsoluteTime _runTime;CFAbsoluteTime _sleepTime;CFTypeRef _counterpart;
};Runloop中除了记录了一些属性外重点还是以下几个
pthread_t _pthread; // runloop对应的线程
CFMutableSetRef _commonModes; // 存储的是字符串记录所有标记为common的mode
CFMutableSetRef _commonModeItems; // 存储所有common标记的mode的itemsource、timer、observer
CFRunLoopModeRef _currentMode; // 当前运行的mode
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发消息就完了
RunLoop的结构 Runloop相关的类
与Runloop相关的类主要有以下几个
CFRunLoopRef代表了Runloop的对象RunloopCFRunLoopModeRefRunloop的运行模式ModeCFRunLoopSourceRefRunloop模型图中的输入源/事件源SourceCFRunLoopTimerRefRunloop模型图中的定时源TimerCFRunLoopObserverRef观察者能够监听Runloop的状态变化
RunLoop机制
这些相关类就跟套娃似的一个RunLoop包含若干个Mode每个Mode又包含若干个Source/Timer/Observer这句话其实就是5个相关类的关系
一个RunLoop对象CFRunLoopRef中包含若干个运行模式CFRunLoopModeRef。而每一个运行模式下又包含若干个输入源CFRunLoopSourceRef、定时源CFRunLoopTimerRef、观察者CFRunLoopObserverRef。每次RunLoop启动时只能指定其中一个运行模式CFRunLoopModeRef这个运行模式CFRunLoopModeRef被称作CurrentMode。如果需要切换运行模式CFRunLoopModeRef只能退出Loop再重新指定一个运行模式CFRunLoopModeRef进入。这样做主要是为了分隔开不同组的输入源CFRunLoopSourceRef、定时源CFRunLoopTimerRef、观察者CFRunLoopObserverRef让其互不影响
CFRunLoopModeRef类
typedef struct __CFRunLoopMode *CFRunLoopModeRef;struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock; /* must have the run loop locked before locking this */CFStringRef _name; //mode名称运行模式是通过名称来识别的Boolean _stopped; //mode是否被终止char _padding[3];//整个结构体最核心的部分
------------------------------------------CFMutableSetRef _sources0; // Sources0CFMutableSetRef _sources1; // Sources1CFMutableArrayRef _observers; // 观察者CFMutableArrayRef _timers; // 定时器
------------------------------------------CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_tvalue是CFRunLoopSourceRef__CFPortSet _portSet;//保存所有需要监听的port比如_wakeUpPort_timerPort都保存在这个数组中CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERSdispatch_source_t _timerSource;dispatch_queue_t _queue;Boolean _timerFired; // set to true by the source when a timer has firedBoolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOOmach_port_t _timerPort;Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWSDWORD _msgQMask;void (*_msgPump)(void);
#endifuint64_t _timerSoftDeadline; /* TSR */uint64_t _timerHardDeadline; /* TSR */
};五种运行模式
系统默认注册的五个mode
kCFRunLoopDefaultModeApp的默认Mode通常主线程是在这个Mode下运行UITrackingRunLoopMode界面跟踪Mode用于ScrollView追踪触摸滑动保证界面滑动时不受其他 Mode 影响UIInitializationRunLoopMode在刚启动 App 时第进入的第一个 Mode启动完成后就不再使用会切换到kCFRunLoopDefaultModeGSEventReceiveRunLoopMode接受系统事件的内部 Mode通常用不到kCFRunLoopCommonModes并不是一种模式 只是一个标记当mode标记为common时将mode添加到runloop中的_commonModes中。runloop中的_commonModes实际上是一个Mode的集合可使用CFRunLoopAddCommonMode()将Mode放到_commonModes中。每当RunLoop的内容发生变化时RunLoop都会将_commonModeItems里的同步到具有Common标记的所有的Mode里
CommonModes
在RunLoop对象中前面有一个有一个叫CommonModes的概念它记录了所有标记为common的mode
//简化版本
struct __CFRunLoop {pthread_t _pthread;CFMutableSetRef _commonModes;//存储的是字符串记录所有标记为common的modeCFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode;//当前运行的modeCFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象不同mode类型它的mode名字不同
};一个Mode可以将自己标记为Common属性通过将其ModeName添加到RunLoop的commonModes中。每当RunLoop的内容发生变化时RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。其底层原理如下
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {CHECK_FOR_FORK();if (__CFRunLoopIsDeallocating(rl)) return;__CFRunLoopLock(rl);if (!CFSetContainsValue(rl-_commonModes, modeName)) {//获取所有的_commonModeItemsCFSetRef set rl-_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl-_commonModeItems) : NULL;//获取所有的_commonModesCFSetAddValue(rl-_commonModes, modeName);if (NULL ! set) {CFTypeRef context[2] {rl, modeName};//将所有的_commonModeItems逐一添加到_commonModes里的每一个ModeCFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);CFRelease(set);}} else {}__CFRunLoopUnlock(rl);
}CFRunLoop对外暴露的管理Mode接口只有下面两个
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);什么是Mode ItemMode到底包含哪些类型的元素
RunLoop需要处理的消息包括time以及source消息他们都属于Mode itemRunLoop也可以被监听被监听的对象是observer对象也属于Mode item所有的mode item都可以被添加到Mode中Mode中可以包含多个mode item一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有则RunLoop会退出不进入循环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只能增加不能删除
CFRunLoopSourceRef类
根据官方描述CFRunLoopSourceRef是input sources的抽象。 CFRunLoopSource分为Source0和Source1两个版本。 它的结构如下
struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits; //用于标记Signaled状态source0只有在被标记为Signaled状态才会被处理pthread_mutex_t _lock;CFIndex _order; //执行顺序CFMutableBagRef _runLoops;//包含多个RunLoop//版本union {CFRunLoopSourceContext version0; /* immutable, except invalidation */CFRunLoopSourceContext1 version1; /* immutable, except invalidation */} _context;
};可一通过共用体union看出它有两个版本 Source0和Source1
Source0
Source0只包含了一个回调函数指针它并不能主动触发事件。使用时你需要先调用CFRunLoopSourceSignal(source)将这个Source标记为待处理然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop让其处理这个事件。
Source0是App内部事件由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行时必须要先把它标为signal状态以下是source0结构体
typedef struct {CFIndex version;void * info;const void *(*retain)(const void *info);void (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode (*hash)(const void *info);void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);void (*perform)(void *info);
} CFRunLoopSourceContext;Source1
Source1包含了mach_port和一个回调函数指针Source1可以监听系统端口通过内核和其他线程通信接收、分发系统事件他能主动唤醒RunLoop由操作系统内核进行管理
注意Source1在处理的时候会分发一些操作给Source0去处理。
source1结构体
typedef struct {CFIndex version;void * info;const void *(*retain)(const void *info);void (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode (*hash)(const void *info);
#if TARGET_OS_OSX || TARGET_OS_IPHONEmach_port_t (*getPort)(void *info);void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#elsevoid * (*getPort)(void *info);void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件可以主动唤醒休眠中的RunLoopiOS里进程间通信开发过程中我们一般不主动使用。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。
举例说明source0和source1 一个APP在前台静止着此时用户用手指点击了一下APP界面那么过程就是下面这样的 我们触摸屏幕先摸到硬件(屏幕)屏幕表面的事件会先包装成EventEvent先告诉source1mach_portsource1唤醒RunLoop然后将事件Event分发给source0然后由source0来处理
CFRunLoopTimerRef类
CFRunLoopTimer是基于时间的触发器其包含一个时间长度、一个回调函数指针。当其加入runloop时runloop会注册对应的时间点当时间点到时runloop会被唤醒以执行那个回调。 并且CFRunLoopTimer和NSTimer是toll-free bridged对象桥接可以相互转换。其结构如下
struct __CFRunLoopTimer {CFRuntimeBase _base;uint16_t _bits;pthread_mutex_t _lock;CFRunLoopRef _runLoop;CFMutableSetRef _rlModes;//包含timer的mode集合CFAbsoluteTime _nextFireDate;CFTimeInterval _interval; /* immutable */CFTimeInterval _tolerance; /* mutable */uint64_t _fireTSR; /* TSR units */CFIndex _order; /* immutable */CFRunLoopTimerCallBack _callout; //timer的回调 CFRunLoopTimerContext _context; //上下文对象
};对于NSTimer
scheduledTimerWithTimeInterval和RunLoop的关系
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:selector(run) userInfo:nil repeats:YES];系统会将NSTimer自动加入NSDefaultRunLoopMode模式中所以它就等同于下面代码
NSTimer *timer [NSTimer timerWithTimeInterval:2.0 target:self selector:selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];NSTimer在滑动时停止工作的问题
这是个使用NSTimer的经典问题当时写NSTimer的时候疯狂折磨我就是我们在定义一个定时器后并且界面存在一个滚动视图当我们拖动滚动视图的时候其NSTimer停止执行事件了等到拖拽完了之后它才会继续开始执行事件。 举例如下
self.scr [[UIScrollView alloc] init];
self.scr.frame CGRectMake(100, 200, 100, 100);
self.scr.contentSize CGSizeMake(300, 100);
self.scr.backgroundColor [UIColor orangeColor];
[self.view addSubview:self.scr];static int count 0;
// 带有 scheduledTimer 就会将定时器添加到默认模式下
NSTimer *timer [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(%d, count);
}];我们发现当我们在拖拽的时候定时器的事件不执行了等我们拖拽停止的时候它又开始运行了所以其中隔了几秒。
造成这种问题的原因就是
当我们不做任何操作的时候RunLoop处于NSDefaultRunLoopMode下当我们进行拖拽时RunLoop就结束NSDefaultRunLoopMode切换到了UITrackingRunLoopMode模式下这个模式下没有添加该NSTimer以及其事件所以我们的NSTimer就不工作了当我们松开鼠标时候RunLoop就结束UITrackingRunLoopMode模式又切换回NSDefaultRunLoopMode模式所以NSTimer就又开始正常工作了
想要解决这个问题也很简单我们直接让NSTimer在两种mode下都能工作就完了这就用到我们之前不太清楚其用法的NSRunLoopCommonModes了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];当然你也可以把NSTimer分别加入到NSDefaultRunLoopMode和UITrackingRunLoopMode这两种写法是相同的因为系统的mode是默认在_commonModes中的
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];修改之后我们不论再怎么拖拽其也会正常运行了。
CFRunLoopObserverRef类
CFRunLoopObserverRef是观察者可以观察Runloop的各种状态每个Observer都包含了一个回调函数指针当runloop的状态发生变化时观察者就能通过回调接收到这个变化。
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {CFRuntimeBase _base;pthread_mutex_t _lock;CFRunLoopRef _runLoop;//监听的RunLoopCFIndex _rlCount;//添加该Observer的RunLoop对象个数CFOptionFlags _activities; /* immutable */CFIndex _order;//同时间最多只能监听一个CFRunLoopObserverCallBack _callout;//监听的回调CFRunLoopObserverContext _context;//上下文用于内存管理
};Runloop的六种状态
//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry (1UL 0), // 即将进入RunLoopkCFRunLoopBeforeTimers (1UL 1), // 即将处理TimerkCFRunLoopBeforeSources (1UL 2), // 即将处理SourcekCFRunLoopBeforeWaiting (1UL 5), //即将进入休眠kCFRunLoopAfterWaiting (1UL 6),// 刚从休眠中唤醒kCFRunLoopExit (1UL 7),// 即将退出RunLoopkCFRunLoopAllActivities 0x0FFFFFFFU
};这六种状态都可以被observer观察到我们也可以利用这一方法写一些特殊事件创建监听监听RunLoop的状态变化
// 创建observer
CFRunLoopObserverRef ob CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry:NSLog(kCFRunLoopEntry);break;case kCFRunLoopBeforeTimers:NSLog(kCFRunLoopBeforeTimers);break;case kCFRunLoopBeforeSources:NSLog(kCFRunLoopBeforeSources);break;case kCFRunLoopBeforeWaiting:NSLog(kCFRunLoopBeforeWaiting);break;case kCFRunLoopAfterWaiting:NSLog(kCFRunLoopAfterWaiting);break;case kCFRunLoopExit:NSLog(kCFRunLoopExit);break;default:break;}
});
// 添加observer到runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), ob, kCFRunLoopCommonModes);
CFRelease(ob);程序启动之后RunLoop是在不停的监听状态并做出反应的。
Runloop的内部逻辑
RunLoop的内部逻辑如下
__CFRunLoopRun源码实现
精简后的__CFRunLoopRun函数保留了主要代码看一下具体实现
//用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找到对应的modeCFRunLoopModeRef currentMode __CFRunLoopFindMode(runloop, modeName, false);//如果该mode中没有source/timer/observer直接返回if (__CFRunLoopModeIsEmpty(currentMode)) return;//1.通知ObserversRunLoop即将进入loop__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);//调用函数__CFRunLoopRun 进入loop__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop NO;int retVal 0;do {//2.通知ObserversRunLoop即将触发Timer回调__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);//3.通知ObserversRunLoop即将触发Source0非port回调__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);///执行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);//4.RunLoop触发Source0非port回调处理Source0sourceHandledThisLoop __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;}//6.通知ObserversRunLoop的线程即将进入休眠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.通知ObserversRunLoop的线程刚刚被唤醒__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执行blockelse 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);//设置do-while之后的返回值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. 通知ObserversRunLoop即将退出__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}实际上RunLoop就是这样的一个函数其内部是一个do-while循环。当你调用CFRunLoopRun()时线程就会一直停留在这个循环里知道超时或者被手动调用该函数才会返回。
并且其并不只是这么简单还有很多细节处理判空什么的都是在相应的方法里的。
RunLoop回调流程
当App启动时系统会默认注册五个上面说过的5个mode当RunLoop进行回调时一般都是通过一个很长的函数调出去call out当在代码中加断点调试时通常能在调用栈上看到这些函数。这就是RunLoop的流程
{/// 1. 通知Observers即将进入RunLoop/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);do {/// 2. 通知 Observers: 即将触发 Timer 回调。__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);/// 4. 触发 Source0 (非基于port的) 回调。__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);/// 6. 通知Observers即将进入休眠/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);/// 7. sleep to wait msg.mach_msg() - mach_msg_trap();/// 8. 通知Observers线程被唤醒__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);/// 9. 如果是被Timer唤醒的回调Timer__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);/// 9. 如果是被dispatch唤醒的执行所有调用 dispatch_async 等方法放入main queue 的 block__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了处理这个事件__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);} while (...);/// 10. 通知Observers即将退出RunLoop/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}RunLoop休眠的实现原理
从用户态切换到内核态在内核态让线程进行休眠有消息时唤起线程回到用户态处理消息 RunLoop的杂七杂八
RunLoop在实际开发中的应用
控制线程生命周期线程保活解决NSTimer在滑动时停止工作的问题监控应用卡顿性能优化
RunLoop启动方法
run无条件 无条件地进入运行循环是最简单的选项但也是最不理想的选择。无条件地运行runloop将线程放入永久循环这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。 runUntilDate 设置时间限制 设置了超时时间超过这个时间runloop结束优于第一种 runMode:beforeDate:在特定模式下相对比较好的方式可以指定runloop以哪种模式运行但是它是单次调用的超时时间到达或者一个输入源被处理则runLoop就会自动退出上述两种方式都是循环调用的实际上run方法的实现就是无限调用runMode:beforeDate:方法runUntilDate:也会重复调用runMode:beforeDate:方法区别在于它超时就不会再调用
RunLoop关闭方法
将运行循环配置为使用超时值运行。手动停止。 这里需要注意虽然删除runloop的输入源和定时器可能会导致运行循环的退出但这并不是个可靠的方法系统可能会添加输入源到runloop中但在我们的代码中可能并不知道这些输入源因此无法删除它们导致无法退出runloop。
我们可以通过上述2、3方法来启动runloop设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制而并不是依赖超时机制这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用而不会结束后续runloop的调用。
ImageView延迟显示
当界面中含有UITableView而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候如果有一堆的图片需要显示那么可能出现卡顿的情况。
如何解决这个问题
我们应该推迟图片的实现也就是ImageView推迟显示图片。当我们滑动时不要加载图片 拖动结束在显示
[self.imageView performSelector:selector(setImage:) withObject:[UIImage imageNamed:imgName.png] afterDelay:3.0 inModes:[NSDefaultRunLoopMode]];常驻线程
开发应用程序的过程中如果后台操作十分频繁比如后台播放音乐、下载文件等等我们希望执行后台代码的这条线程永远常驻内存我们可以添加一条用于常驻内存的强引用子线程在该线程的RunLoop下添加一个Sources开启RunLoop
property (nonatomic, strong) NSThread *thread;- (void)viewDidLoad {[super viewDidLoad];self.thread [[NSThread alloc] initWithTarget:self selector:selector(runThread) object:nil];[self.thread start];
}
- (void)runThread {NSLog(----run-----%, [NSThread currentThread]);//如果不加这句会发现runloop创建出来就挂了因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器就会立马消亡。//下面的方法给runloop添加一个NSport就是添加一个事件源也可以添加一个定时器或者observer让runloop不会挂掉//方法1[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];// 方法2
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];// 方法3
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];// 方法1 ,23实现的效果相同让runloop无限期运行下去[[NSRunLoop currentRunLoop] run];// 测试是否开启了RunLoop如果开启RunLoop则来不了这里因为RunLoop开启了循环。NSLog(未开启RunLoop);
}
//我们同时在我们自己新建立的这个线程中写一下touchesBegan这个方法
- (void)touchesBegan:(NSSetUITouch * *)touches withEvent:(UIEvent *)event {// 利用performSelector在self.thread的线程中调用runTest方法执行任务[self performSelector:selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}- (void)runTest {NSLog(----runTest------%, [NSThread currentThread]);
}我们发现线程启动RunLoop成功了没有打印未开启RunLoop并且通过输出线程发现执行点击事件的也是我们创建的这个线程这样我们就达到常驻线程的目的了该线程self.thread一直在等待一个事件加入其中然后执行。
线程保活
平时创建子线程时线程上的任务执行完这个线程就会销毁掉。 有时我们会需要经常在一个子线程中执行任务频繁的创建和销毁线程就会造成很多的开销这时我们可以通过runloop来控制线程的生命周期。
在下面的代码中因为runMode:beforeDate:方法是单次调用我们需要给它加上一个循环否则调用一次runloop就结束了和不使用runloop的效果一样。
这个循环的条件默认设置成YES当调用stop方法中执行CFRunLoopStop()方法结束本次runMode:beforeDate:同时将循环中的条件设置为NO使循环停止runloop退出。
property (nonatomic, strong) NSThread *thread;
property (nonatomic, assign) BOOL stopped;- (void)viewDidLoad {[super viewDidLoad];self.view.backgroundColor [UIColor greenColor];UIButton *button [UIButton buttonWithType:UIButtonTypeRoundedRect];[self.view addSubview:button];[button addTarget:self action:selector(pressPrint) forControlEvents:UIControlEventTouchUpInside];[button setTitle:执行任务 forState:UIControlStateNormal];button.frame CGRectMake(100, 200, 100, 20);UIButton *stopButton [UIButton buttonWithType:UIButtonTypeRoundedRect];[self.view addSubview:stopButton];[stopButton addTarget:self action:selector(pressStop) forControlEvents:UIControlEventTouchUpInside];[stopButton setTitle:停止RunLoop forState:UIControlStateNormal];stopButton.frame CGRectMake(100, 400, 100, 20);self.stopped NO;//防止循环引用__weak typeof(self) weakSelf self;self.thread [[NSThread alloc] initWithBlock:^{NSLog(Thread---begin);//向当前runloop添加Modeitem添加timer、observer都可以。因为如果mode没有itemrunloop就会退出[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];while (!weakSelf.stopped) {[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];}NSLog(Thread---end);}];[self.thread start];
}
- (void)pressPrint {//子线程中调用print[self performSelector:selector(print) onThread:_thread withObject:nil waitUntilDone:NO];
}//子线程需要执行的任务
- (void)print {NSLog(%s, %, __func__, [NSThread currentThread]);
}- (void)pressStop {//子线程中调用stopif (_stopped NO ) {[self performSelector:selector(stop) onThread:_thread withObject:nil waitUntilDone:YES];}}//停止子线程的runloop
- (void)stop {//设置标记yesself.stopped YES;//停止runloopCFRunLoopStop(CFRunLoopGetCurrent());NSLog(%s, %, __func__, [NSThread currentThread]);//解除引用 停止runloop这个子线程就会deallocself.thread nil;
}- (void)dealloc {NSLog(%s, __func__);
}这样我们就实现了线程保活其中我们要注意线程的管理是系统管理的哪怕是在这个页面新建的线程线程是否销毁和页面的销毁没有任何关系这取决于系统 那么我们在某个页面销毁的时候就会存在页面新建的线程没有销毁这个问题。 解决这个问题最简单的办法就是在销毁这个页面的时候我们再重新调用一次stop方法并将我们这个线程指向置为nil。
定时器NSTimer
在实际开发中一般不把timer放到主线程的RunLoop中因为主线程在执行阻塞的任务时timer计时会不准。 如何让计时准确如果timer在主线程中阻塞了怎么办
放入子线程中(即要开辟一个新的线程但是成本是需要开辟一个新的线程)写一种跟RunLoop没有关系的计时即GCD。(不会阻塞推荐使用这种)
// GCD定时器(常用)
// 创建队列
dispatch_queue_t queue dispatch_get_global_queue(0, 0);
// 1.创建一个GCD定时器
/*第一个参数:表明创建的是一个定时器第四个参数:队列*/
dispatch_source_t timer dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用保证其不会被释放掉才会按时调用block块
// 局部变量让指针强引用
self.timer timer;
// 2.设置定时器的开始时间,间隔时间,精准度
/*第1个参数:要给哪个定时器设置第2个参数:开始时间第3个参数:间隔时间第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能GCD的单位是纳秒 所以要*NSEC_PER_SEC*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);// 3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{NSLog(---%--,[NSThread currentThread]);// 取消定时if (判断条件) {dispatch_source_cancel(timer);self.timer nil;}
});
// 4.启动
dispatch_resume(timer);