一,RunLoop简介
RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行,RunLoop在没有事件处理的时候,会使线程进入睡眠模式,从而节省CPU资源,提高程序性能。
简单来说,runloop可以让线程在需要做事情的时候忙起来,不需要的时候让线程休眠,使程序不会结束。
二,RunLoop基本作用
1,保持程序的持续运行 :
程序一开启就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop。RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
2,处理App中的各种事件:
3,节省CPU资源,提高程序性能
三,主线程的RunLoop原理
启动一个iOS程序时,系统会调用创建项目自动生成的main.m的文件
objectivec
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
其中UIApplicationMain
函数内部会帮我们开启主线程的RunLoop
,内部拥有一个无限循环的代码,一直在do_while
循环中执行,程序也就会一直保持执行状态,不会马上退出
objectivec
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running); // 判断是否需要退出
return 0;
}
RunLoop的模型图
RunLoop
就是线程中的一个循环,RunLoop
会在循环中不断检测,通过Input sourse
(输入源)和Timer sources
(定时器)两种来源更待接受事件;然后接受到事件通知线程进行处理,并在没有事件的时候让线程进行休息。
三,RunLoop对象
RunLoop实际上是一个对象,是基于CFFoundation框架的CFRunLoopRef类型分装的对象。
这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。
RunLoop对象的获取
objectivec
// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
objectivec
// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象
CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全
看一下CoreFoundation框架中这两个函数的具体实现:
objectivec
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 needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
它们都调用了_CFRunLoopGet0这个方法。
objectivec
//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的"主线程"的同义词
//获取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);
//第一次进入时,创建一个临时字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//根据传入的主线程,获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉
CFRelease(dict);
}
//释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建
//从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
//如果找不到对应的Runloop
if (!loop) {
//创建一个该线程的Runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
//再次在__CFRunLoops中查找该线程的Runloop
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//如果在字典中还是找不到该线程的Runloop
if (!loop) {
//把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程t
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
//并且让loop指向刚才创建的Runloop
loop = newLoop;
}
// don't 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);
}
}
//返回该线程对应的Runloop
return loop;
}
我们可以得出RunLoop和线程的关系
- 每个线程都有唯一一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建的时没有RunLoop对象,在第一次获取RunLoop时创建,销毁则是在线程结束的时候
- 主线程的RunLoop对象系统自动创建,子线程的RunLoop对象需要我们主动创建和维护
CFRunLoopRef源码部分
objectivec
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; // runloop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; // 存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems; // 存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; // 当前运行的mode
CFMutableSetRef _modes; // 装着一堆CFRunLoopModeRef类型
struct _block_item *_blocks_head; // do blocks时用到
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
CFRunLoopModeRef
代表RunLoop
的运行模式,一个RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source0
/Source1
/Timer
/Observer
,而RunLoop
启动时只能选择其中一个Mode作为currentMode
。
- Source0:触摸事件,performSelectors
- Sourse1:基于Port的线程间的通信
- Timers:定时器,NSTimer
- Observer;监听器,用于监听RunLoop的状态
ConmonModes
它记录了所有被标记了common的mode
- 一个Mode可以将自己标记为Common属性,通过将其ModelName添加到RunLoop的commonModes中
- 每当RunLoop的内容发生变化时,RunLoop都会将_commonItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。
objectivec
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
//获取所有的_commonModeItems
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//获取所有的_commonModes
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
//将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
_commonModes 中存储的是所有被标记为 common 的 mode 的集合,这些 mode 具有跨 run loop 共享的特性,能够让特定的事件源、定时器和观察者在多个 run loop 中共同作用。
四,RunLoop的相关类
- CFRunLoopRef:RunLoop对象
- CFRunLoopModeRef:RunLoop的运行模式
- CFRunLoopSourceRef:RunLoop运行模式的输入源/事件源
- CFRunLoopTimerRef:RunLoop运行模式的定时源
- CFRunLoopObserverRef:RunLoop运行模式的观察者,监听RunLoop的状态改变
一个RunLoop对象(CFRunLoopRef)
中包含若干个运行模式(CFRunLoopModeRef)
。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)
、定时源(CFRunLoopTimerRef)
、观察者(CFRunLoopObserverRef)
。
- 每次
RunLoop
启动时,只能指定其中一个运行模式(CFRunLoopModeRef
),这个运行模式(CFRunLoopModeRef)
被称作当前运行模式(CurrentMode)
。 - 如果需要切换运行模式
(CFRunLoopModeRef)
,只能退出当前Loop
,再重新指定一个运行模式(CFRunLoopModeRef)
进入。 - 这样做主要是为了分隔开不同组的输入源
(CFRunLoopSourceRef)
、定时源(CFRunLoopTimerRef)
、观察者(CFRunLoopObserverRef)
,让其互不影响 。
4.1 CFRunLoopModeRef类
objectivec
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; // Sources0
CFMutableSetRef _sources1; // Sources1
CFMutableArrayRef _observers; // 观察者
CFMutableArrayRef _timers; // 定时器
------------------------------------------
CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
系统默认注册的5个Mode:
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultModeGSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,通常用不到kCFRunLoopCommonModes
: 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和UITrackingRunLoopMode
4.2 CFRunLoopSourceRef类
CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0
和source1
两个版本,它的结构如下:
objectivec
struct __CFRunLoopSource
CFRuntimeBase _base;
uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
source0
- source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态。
- source0是非基于Port的。只包含了一个回调(函数指针),它并不能触发出发事件。
- 使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop
source1
- Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop。
4.3 CFRunLoopTimerRef
CFRunLoopTimer
是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop
时,runloop
会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。并且CFRunLoopTimer
和NSTimer
是toll-free bridged
(对象桥接),可以相互转换。
objectivec
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的关系
objectivec
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以他就等同于下面的代码
objectivec
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSTimer在滑动时停止工作:
我们在写NSTimer时,如果我们滑动UIScrollView时,定时器的事件就不执行了,如果我们停止滑动时,定时器就会重新工作。
原因:
- 当我们不做任何操作的时候,
RunLoop
处于NSDefaultRunLoopMode
下。 - 而当我们拖动
ScrollView
的时候,RunLoop
就结束NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加NSTimer
,所以我们的NSTimer
就不工作了。 - 但当我们松开鼠标的时候
,RunLoop
就结束UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer
就又开始正常工作了
如何将NSTimer在两种模式下运行?
就是将Timer加入到顶层RunLoop的commonModeItems中,commonModeItems被RunLoop自动更新到所有具有Common属性的Mode中去,Common Modes并不是一个正真的模式,它只是一个标记,NSDefaultRunLoopMode
和 UITrackingRunLoopMode
被标记其中。我们可以将NSTimer添加到当前RUnLoop的kCFRunLoopCommonModes
。
objectivec
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
其实现的效果和下面的相同
objectivec
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
4.4 CFRunLoopObserverRef
CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当runloop的状态发生变化时,观察者就能通过回调接收到这个变化。
objectivec
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;//监听的RunLoop
CFIndex _rlCount;//添加该Observer的RunLoop对象个数
CFOptionFlags _activities; /* immutable */
CFIndex _order;//同时间最多只能监听一个
CFRunLoopObserverCallBack _callout;//监听的回调
CFRunLoopObserverContext _context;//上下文用于内存管理
};
RunLoop的六种状态
objectivec
//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
RunLoop的底层原理
流程图
__CFRunLoopRun源码实现
objectivec
/// RunLoop的实现, 大概在文件的2622行
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
/// __CFRunLoopRun中具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// 11. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// __CFRunLoopRun的实现, 进入loop, 大概在文件的2304行
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 4. 处理block
__CFRunLoopDoBlocks(rl, rlm);
// 5. 处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果处理Source0的结果是rrue
if (sourceHandledThisLoop) {
// 再次处理block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 如果有Source1, 就跳转到handle_msg
goto handle_msg;
}
// 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 7. 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程
// 一个基于 port 的Source 的事件。
// 一个 Timer 到时间了
// RunLoop 自身的超时时间到了
// 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 收到消息,处理消息。
handle_msg:;
if (/* 被timer唤醒 */) {
// 01. 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (/* 被gcd唤醒 */) {
// 02. 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Source1唤醒
// 处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
}
// 9. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 10. 设置返回值, 根据不同的结果, 处理不同操作
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (0 == retVal);
return retVal;
}
RunLoop其内部是一个do-while循环; 当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回
RunLoop的退出
- 主线程销毁时
- Mode中的
Timer
,Source
,Observer
为空 RunLoop
到了设置的停止时间
Run Loop的实现原理
从用户态切换到内核态,在内核态让线程进行休眠,有消息的时候就还行线程,回到用户态处理消息
RunLoop的核心就是调用mach_msg().
RunLoop的实际应用
图片下载
由于图片渲染到屏幕需要消耗较多的资源,为了提高1用户的体验,当用户滚动TableView的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示出来。
objectivec
[self.imageView performSelector:@selector(setImage:) withObject:image afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滚动tableView的时候,程序运行在tracking模式下面,所以方法setImage不会执行。
常驻线程
我们在开发应用程序的时候,如果后台操作特别频繁,经常会做一些耗时的操作(下载文件,后台播放音乐),如果频繁的创建销毁线程,非常消耗性能。我们最好能让这条线程永驻内存。添加一条长驻于内存的强引用的子线程,在该线程添加一个Sources,开启RunLoop.
objectivec
#import "ViewController.h"
@interface ViewController ()
@property (strong, nonatomic) NSThread* thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[self.thread start];
}
- (void) run1 {
NSLog(@"----run1----");
NSLog(@"%@", [NSThread currentThread]);
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"为开启RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) run2
{
NSLog(@"----run2------");
NSLog(@"%@", [NSThread currentThread]);
}
@end
输出结果
objectivec
2024-08-03 15:49:07.960805+0800 线程常驻[43968:1479175] ----run1----
2024-08-03 15:49:08.730213+0800 线程常驻[43968:1479175] ----run2------
2024-08-03 15:49:08.730499+0800 线程常驻[43968:1479175] <NSThread: 0x60000210d500>{number = 6, name = (null)}
2024-08-03 15:49:15.483094+0800 线程常驻[43968:1479175] ----run2------
2024-08-03 15:49:15.483280+0800 线程常驻[43968:1479175] <NSThread: 0x60000210d500>{number = 6, name = (null)}
我们发现RunLoop启动成功进入循环,并没有打印为开启RunLoop,点击屏幕时也是在子线程添加方法,这样就达成了线程常驻的效果。
定时器NSTimer
在实际开发中,一般不把timer放到主线程的RunLoop中,因为主线程在执行阻塞的任务时,timer计时会不准。
如何让计时准确?如果timer在主线程中阻塞了怎么办?
放入子线程中(即要开辟一个新的线程,但是成本是需要开辟一个新的线程)
写一种跟RunLoop没有关系的计时,即GCD。(不会阻塞,推荐使用这种)
objectivec
// 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);