【iOS RunLoop】
文章目录
- 前言-什么是RunLoop?
- 默认情况下主线程的RunLoop原理
- 1. RunLoop对象
- RunLoop对象的获取
- CFRunLoopRef源码部分(引入线程相关)
- 2. RunLoop和线程
- 3. RunLoop相关的类
- RunLoop相关类的实现
- CFRunLoopModeRef
- 五种运行模式
- CommonModes
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- 定时器滑动不准确
- CFRunLoopObserverRef
- 什么是Mode Item?
- RunLoop内部逻辑
- RunLoop休眠的实现原理
- RunLoop小结
- 4. RunLoop实际应用
- RunLoop的启动方法
- RunLoop关闭
- imageView延迟显示
- 常驻线程
- 线程保活
- NSTimer不准
前言-什么是RunLoop?
什么是RunLoop? 跑圈?字面上理解确实是这样的。
Apple官方文档这样解释RunLoop
RunLoop是与线程息息相关的基本结构的一部分。RunLoop是一个调度任务和处理任务的事件循环。RunLoop的目的是为了在有工作的时候让线程忙起来,而在没有工作的时候让线程进入休眠状态。
之所以iOS的app能够持续的响应从而让程序保持运行状态,在于其存在一个事件循环(Event Loop)机制: 线程能够随时响应并处理事件的机制,这种机制要求线程不能退出从而高效的完成事件调度和处理。
在iOS这种事件循环机制就叫做RunLoop
RunLoop实际上是一个对象,对象在循环中处理程序运行过程出现的各种事件(比如触摸事件,UI刷新事件,定时器事件,Selector事件)从而保持程序的持续运行并且让程序在没有事件处理的时候进入休眠状态,从而节省CPU资源达到提升程序性能的目的。
默认情况下主线程的RunLoop原理
#import <UIKit/UIKit.h>
#import "AppDelegate.h"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);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);: 这是 iOS 应用程序的主运行循环,它负责处理用户事件、界面更新和应用程序的主要逻辑。UIApplicationMain 函数创建应用程序对象和主运行循环,并传递控制权给应用程序的委托类(AppDelegate)来处理应用程序的逻辑。
- 其中的
UIApplicationMain函数内部帮我们开启了主线程的RunLoop。 UIApplicationMain内部拥有一个无限循环的代码。
function loop() {initialize();do {var message = get_next_message();process_message(message);} while (message != quit);
}
程序会一直在do-while循环中执行.
Apple官方的RunLoop模型图

RunLoop就是线程中的一个循环,RunLoop在循环中不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受消息,然后对接收到的事件通知线程进行处理,并在没有事件的时候进行休息。
1. RunLoop对象
RunLoop是一个对象。
RunLoop对象的获取
RunLoop对象是基于CFFoundation框架的CFRunLoopRef类型封装的对象。
NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
[NSRunLoop currentRunLoop];//获得当前RunLoop对象
[NSRunLoop mainRunLoop];//获得主线程的RunLoop对象
CoreFoundation框架的 CFRunLoopRef对象
CFRunLoopRef是在CoreFoundation框架内的,其提供了纯C语言函数的API,所有这些API都是线程安全的。
CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
CFRunLoopGetMain();//获得主线程的RunLoop对象

那么对应的两种方式就是
- (void)getRunLoop {NSRunLoop *runloop = [ NSRunLoop currentRunLoop];NSRunLoop *manRlp = [NSRunLoop mainRunLoop];CFRunLoopRef cfRlp = CFRunLoopGetCurrent();CFRunLoopRef mainCfRlp = CFRunLoopGetMain();
}
看下CFRunLoopGetCurrent 和 CFRunLoopGetMain的具体实现
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这个函数,后面再进行讲解。
CFRunLoopRef源码部分(引入线程相关)
CFRunLoopRef的源码部分
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 looppthread_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;CFTypeRef _counterpart;
};
对于一些属性之外,重点需要关注三个成员变量
pthread_t _pthread;【RunLoop对应的线程】
CFRunLoopModeRef _currentMode;【当前运行的mode】
CFMutableSetRef _modes;【存储的是CFRunLoopModeRef】
看看RunLoop和线程的关系
2. RunLoop和线程
先看一下_CFRunLoopGet0这个函数是怎么实现的,和RunLoop和线程有什么关系。
//全局的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);if (!__CFRunLoops) {__CFSpinUnlock(&loopsLock);//第一次进入时,创建一个临时字典dictCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//根据传入的主线程获取主线程对应的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//保存主线程,将主线程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//释放dictCFRelease(dict);}//释放mainRunLoopCFRelease(mainLoop);__CFSpinLock(&loopsLock);}//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建//从全局字典里获取对应的RunLoopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFSpinUnlock(&loopsLock);if (!loop) {//如果取不到,就创建一个新的RunLoopCFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFSpinLock(&loopsLock);loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//创建好之后,以线程为key,runLoop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runLoopif (!loop) {//把newLoop存入字典__CFRunLoops,key是线程tCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);loop = newLoop;}// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it__CFSpinUnlock(&loopsLock);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);}}return loop;
}
这段源码告诉我们:
- 每条线程都有唯一的与之对应的
RunLoop对象。 RunLoop保存在一个全局的Dictionary里面,线程作为key,RunLoop作为Value- 线程刚创建的并没有
RunLoop对象。RunLoop会在第一次获取线程的RunLoop创建,在线程结束的时候销毁。 - 主线程的
RunLoop已经自动获取(创建),子线程默认没有开启RunLoop.
3. RunLoop相关的类
与RunLoop相关的类有5个。
CFRunLoopRef: 代表了RunLoop对象CFRunLoopModeRef: 代表了RunLoop的运行模式CFRunLoopSourceRef:RunLoop模型中提到的输入源。CFRunLoopTimerRef: 定时源CFRunLoopObserverRef: 观察者,监听RunLoop状态的改变。
- 一个
RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。

- 每次调用
RunLoop的主函数的时候, 只允许指定其中的一个运行模式(CFRunLoopModeRef),就是被称作CurrentMode; - 如果需要切换
Mode,只能通过退出Loop,然后需要重新指定一个Mode进入,这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响 - 如果一个
mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环。
RunLoop的结构和套娃一样,RunLoop里面装着Mode,Mode里面装着Souce / Observer / Timer
RunLoop相关类的实现
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。
这句话其实就是5个相关类的关系
CFRunLoopModeRef
代表了RunLoop的运行模式,但这里请理清概念,我们的RunLoop里可以装多个Mode,只是我们在指定运行的时候要指定一个Mode
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_t,value是CFRunLoopSourceRef__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中CFIndex _observerMask;
};
- 一个
CFRunLoopModeRef对象有name属性,若干source0,source1,timer,observer和port,可以看出来事件都是由mode在管理,而RunLoop负责管理Mode,
五种运行模式
系统默认注册的五个Mode
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响(与用户交互事件的Mode)UIInitializationRunLoopMode: 在刚启动App时第进入的第一个Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultModeGSEventReceiveRunLoopMode: 接受系统事件的内部Mode,通常用不到kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode(伪模式,并不是一种真正的模式)
其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式。
CommonModes
在RunLoop对象中,前面有一个CommonModes成员变量。
//简化版本
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);
}
总体来说,这个方法的主要作用是将一个指定的 commonModeItems 集合添加到运行循环的共同模式集合中,并将 commonModeItems 添加到共同模式集合中的每个 Mode 中,以确保共同模式的事件源在多个 Mode 下都能得到处理。它涉及到 CoreFoundation 框架中运行循环的底层操作,用于管理运行循环中的事件和模式。
CFRunLoopSourceRef
CFRunLoopSourceRef: RunLoop模型中提到的输入源,也就是事件产生的地方。
struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits;pthread_mutex_t _lock;CFIndex _order; //执行顺序CFMutableBagRef _runLoops;//包含多个RunLoop//版本union {CFRunLoopSourceContext version0; /* immutable, except invalidation */CFRunLoopSourceContext1 version1; /* immutable, except invalidation */} _context;
};
刚才上面提到souce存在 source0 和 source1两个版本,他们分别做了什么?
Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source), 将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件Source1包含了一个mach_port和一个回调(函数指针),可以用于内核和其他线程相互发送消息,这种Source能主动唤醒RunLoop线程。- ⚠️:对于button的点击事件属于Source0函数的执行内容。点击事件就是Source0进行处理的。
- Source1则是用来接受和分发事件,分发到Souce0进行处理。
CFRunLoopTimerRef
CFRunLoopTimerRef: 定时源- 基于时间的触发器。
CFRunLoopTimerRef是基于时间的触发器,它和NSTimer可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
当我们调用NSTimer的scheduledTimerWithTimeInterval的时候
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
系统会自动加入NSDefaltRunLoopMode.
等于如下代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
定时器滑动不准确
在知乎日报的时候,滑动tableView造成了上面自动轮播图的定时器失效问题。
常见的问题就是:当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况
原因就是:
- 当我们不做任何操作的时候,
RunLoop处于NSDefaultRunLoopMode下。 - 当我们进行拖拽时,
RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了。 - 当我们松开鼠标时候,
RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。
解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
在 iOS 中,当你滑动 UITableView 或其他滚动视图时,主线程上的 RunLoop 切换到 UITrackingRunLoopMode,这是一个特殊的运行循环模式,用于处理用户交互事件,例如滚动手势。在默认情况下,如果你在主线程上使用定时器,它会在默认的运行循环模式 NSDefaultRunLoopMode 下运行。由于 RunLoop 一次只能处理一个运行循环模式,当你滑动时,NSDefaultRunLoopMode 被切换到 UITrackingRunLoopMode,导致定时器事件暂停,直到滑动结束。
为了解决这个问题,可以使用 kCFRunLoopCommonModes。代表了一个 “common mode set”,它同时包括了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。通过在定时器的添加中使用 kCFRunLoopCommonModes,可以使定时器在默认模式和追踪模式下都得到触发,从而避免滑动导致定时器暂停的问题。
CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,每个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;//上下文用于内存管理
};//观测的时间点有一下几个
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
};
什么是Mode Item?
Mode到底包含哪些类型的元素?
前面提到过 CFMutableSetRef _commonModeItems:存储所有commonMode的item (source、timer、observer)
上面的 Source/Timer/Observer 被统称为 mode item
- 所有的
mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环.
RunLoop内部逻辑

RunLoop休眠的实现原理
从用户态切换到内核态, 在内核态让线程进行休眠,有消息的时候唤起线程,回到用户态处理消息
RunLoop小结
RunLoop内部实际是一个do while循环,当调用CFRunLoopRun()的时候,线程就会一直停留在这个循环里面,当超时或者被手动调用的时候该函数才会返回。
RunLoop的运行必定要指定一种mode,并且该mode必须注册任务事件。RunLoop是在默认mode下运行的,当然也可以指定一种mode运行,但是只能在一种mode下运行。RunLoop内部实际上是维护了一个do-while循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。RunLoop的核心就是一个mach_msg() ,RunLoop调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件
4. RunLoop实际应用
- 控制线程生命周期(线程保活)
- 解决NSTimer在滑动时停止工作的问题
- 监控应用卡顿
- 性能优化
RunLoop的启动方法
- (void)run; // 默认模式
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
run:没有任何条件就可以启动,虽然简单,但是实际的操控结果并不影响,RunLoop是一个do while循环,倘若无条件的运行RunLoop将线程永远的放入循环,这就使我们没有办法控制循环本身,只能靠杀死进程来停止RunLooprunUnitDate:设置时间限制。
- 设置了超时的时间,超过这个时间
RunLoop就结束了。
runMode:beforeDate:在特定模式下启动。
- 可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
⚠️:
- 第一种会一直运行下去,并且一直在
NSDefaultRunLoopMode模式下,重复调用runMode:beforeDate:方法。 - 第二种会在超时时间之前一直在
NSDefaultRunLoopMode模式下调用runMode:beforeDate:方法。 - 第三种则会在超时时间到达或者第一个
inputsource被处理前一直调用runMode:beforeDate:方法。
RunLoop关闭
- 将运行的循环配置设置为超时。
- 手动停止
这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。
当我们通过 runUnitDate 和 runMode: beforeDate:方法启动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]];
用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。
这是因为限定了方法setImage只能在NSDefaultRunLoopMode模式下使用。而滚动tableview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。
常驻线程
开发应用程序的过程中,如果后台操作十分频繁,比如后台播放音乐、下载文件等等,我们希望执行后台代码的这条线程永远常驻内存,我们可以添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop:
@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@endself.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];[self.thread start];
- (void)runThread {NSLog(@"开启子线程:%@", [NSThread currentThread]);
// 子线程的RunLoop创建出来需要手动添加事件输入源和定时器 因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。//下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] run];// 测试开始RunLoop// 未进入循环就会执行该代码NSLog(@"failed");
}// 同时在我们自己新建立的这个线程中写一下touchesBegan这个方法测试点击空白处会不会在子线程相应方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {[self performSelector:@selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)runTest {NSLog(@"子线程点击空白:%@", [NSThread currentThread]);
}

可以看到RunLoop成功启动进入循环,点击屏幕的时候也是在子线程调用方法,这样子子线程启动完成之后就达到了常驻线程的目的。
线程保活
场景:
平时创建子线程时,线程上的任务执行完这个线程就会销毁掉。
有时我们会需要经常在一个子线程中执行任务,频繁的创建和销毁线程就会造成很多的开销,这时我们可以通过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没有item,runloop就会退出[[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__);
}
NSTimer不准
timer在实际的开发中一般不在主线程的RunLoop里面存在,因为主线程在执行阻塞任务的时候timer的计时器也会导致不准确。
如果timer在主线程里面阻塞 如何解决timer不准确的问题。
- 放入子线程中,但是需要开辟线程和控制线程的生命周期,成本较大。
- 使用GCD的定时器计时,避免阻塞。
相关文章:
【iOS RunLoop】
文章目录 前言-什么是RunLoop?默认情况下主线程的RunLoop原理 1. RunLoop对象RunLoop对象的获取 CFRunLoopRef源码部分(引入线程相关) 2. RunLoop和线程3. RunLoop相关的类RunLoop相关类的实现CFRunLoopModeRef五种运行模式CommonModes CFRun…...
阿里云平台注册及基础使用
首先进入阿里云官网: 阿里云-计算,为了无法计算的价值 点击右上角“登录/注册”,如果没有阿里云账号则需要注册。 注册界面: 注册完成后需要开通物联网平台公共实例: 注册成功后的登录: 同样点击右上角的…...
Mr. Cappuccino的第58杯咖啡——MacOS配置Maven和Java环境
MacOS配置Maven和Java环境 查看Mac使用的是哪个shell下载并准备Maven下载Maven配置前准备 下载并安装JDK下载JDK安装JDK 配置Maven和Java环境添加配置加载配置 验证环境 查看Mac使用的是哪个shell echo $SHELL如果使用的是bash,则使用以下命令 open ~/.bash_profi…...
linux Ubuntu 更新镜像源、安装sudo、nvtop
1.更换镜像源 vi ~/.pip/pip.conf在打开的文件中输入: pip.conf [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple按下:wq保存并退出。 2.安装nvtop 如果输入指令apt install nvtop报错: E: Unable to locate package nvtop 需要更新一下apt&a…...
LUN映射出错导致写操作不互斥的服务器数据恢复案例
服务器数据恢复环境: 某公司的光纤SAN存储系统,6块硬盘组建一组RAID6,划分若干LUN,MAP到不同的SOLARIS操作系统服务器上。 服务器故障&分析: 由于业务增长需要新增应用,工作人员增加了一台IBM服务器&am…...
Android 仿京东头部滚动头像动态变化
UI出了一个新需求,仿京东头部滚动,头像需要动态变化,先来看下京东的是什么效果 我们知道什么效果以后,接下来就想想怎么实现吧,Android常规吸顶折叠布局是由CoordinatorLayoutAppBarLayoutCollapsingToolbarLayout组成…...
高频交易学习——上期SimNow开通
property 是 Python 中的一个装饰器(decorator),用于定义类的属性。它可以将方法转换为相应的特性(property),从而实现属性的访问和修改控制。 property 装饰器的作用是将一个方法变成一个只读属性&#x…...
电力巡检无人机助力迎峰度夏,保障夏季电力供应
夏季是电力需求量较高的时期,随着高温天气的来临,风扇、空调和冰箱等电器的使用量也大大增加,从而迎来夏季用电高峰期,电网用电负荷不断攀升。为了保障夏季电网供电稳定,供电公司会加强对电力设施设备的巡检࿰…...
UOS环境python3.7及pyqt5安装
解决方案尝试 先安装pyqt5依赖项: 1、更新python3.7 sudo add-apt-repository ppadeadsnakes/ppa sudo apt-get update sudo apt-get upgrade sudo apt-get autoremove sudo apt-get install python3.7 sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/…...
SEO优化:提升网站排名与流量的关键策略
导言: 在如今竞争激烈的互联网时代,网站的排名和流量对于企业的在线可见性和业务发展至关重要。搜索引擎优化(SEO)是一种关键的策略,旨在提高网站在搜索引擎结果页面上的排名,从而增加网站的曝光率和有针对…...
flutter-GridView使用
先看效果 代码实现 import package:app/common/util/k_log_util.dart; import package:app/gen/assets.gen.dart; import package:app/pages/widget/top_appbar.dart; import package:flutter/cupertino.dart; import package:flutter/material.dart; import package:flutter_…...
Unity Shader编辑器工具类ShaderUtil 常用函数和用法
Unity Shader编辑器工具类ShaderUtil 常用函数和用法 Unity的Shader编辑器工具类ShaderUtil提供了一系列函数,用于编译、导入和管理着色器。本文将介绍ShaderUtil类中的常用函数和用法。 编译和导入函数 CompileShader 函数签名:public static bool C…...
详解Spring中涉及的技术
注解 介绍: 注解(Annotation)很重要,未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注…...
阿里云ssl免费数字证书快过期 如何更换
1.登陆阿里云 找到ssl 查看快过期的证书 数字证书管理服务-ssl证书 2.创建免费的证书,对应过期证书的域名 3.下载新证书 pem key放在本地 此处记录本地的下载路径 /Users/dorsey/Downloads/10791167_lzzabc.cn_nginx/lzzabc.cn.pem /Users/dorsey/Downloads/1…...
利用OpenCV实现图像拼接
一、介绍 图像拼接. 二、分步实现 要实现图像拼接,简单来说有以下几步: 对每幅图进行特征点提取对对特征点进行匹配进行图像配准把图像拷贝到另一幅图像的特定位置对重叠边界进行特殊处理 PS:需要使用低版本的opencv,否则无法使…...
【java安全】无Commons-Collections的Shiro550反序列化利用
文章目录 【java安全】无Commons-Collections的Shiro550反序列化利用Shiro550利用的难点CommonsBeanutils1是否可以Shiro中?什么是serialVersionUID?W 无依赖的Shiro反序列化利用链POC 【java安全】无Commons-Collections的Shiro550反序列化利用 Shiro5…...
CSS 滚动条
一、滚动条样式属性 ::-webkit-scrollbar {width: 6px; /* 竖向滚动条宽度 */height: 6px; /* 横向滚动条高度 */ }::-webkit-scrollbar-thumb {border-radius: 10px; /* 滚动条样式 */-webkit-box-shadow: inset 0 0 3px red; /* 内阴影 */background-color: blue; /* 滚动条…...
Linux: security: openssh: sshd 出现defunct的一种情况
最近遇到了一个问题,就出现了一对遗留进程对,类似于下面这两个 root 77399 19100 77399 0 1 01:46 ? 00:00:00 sshd: \mzhan017 [priv] sshd 77400 77399 77400 0 1 01:46 ? 00:00:00 sshd: [defunct] 人生中的第一次遇到这种情况。一定要记录一下! 关于[priv]这个解释,…...
Self-regulating Prompts: Foundational Model Adaptation without Forgetting
本文也是大模型系列的文章,主要是与Prompt Learning有关。针对《Self-regulating Prompts: Foundational Model Adaptation without Forgetting》的翻译。 自我调节的提示:不遗忘的基础模型适应 摘要1 引言2 相关工作3 提出的方法3.1 前言3.2 提示学习的…...
平时工资不够用?推荐4种适合工作之余做的兼职副业!
你是否也曾经在为每个月的工资发愁?你是否想过做点副业来增加收入?现在很多上班族的工资,已经难以满足他们的生活需求了,很多人开始尝试通过副业来增加收入。那么上班族要如何寻找适合自己的副业呢?下面就给大家分享几…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
