【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
,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
GSEventReceiveRunLoopMode
: 接受系统事件的内部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种适合工作之余做的兼职副业!
你是否也曾经在为每个月的工资发愁?你是否想过做点副业来增加收入?现在很多上班族的工资,已经难以满足他们的生活需求了,很多人开始尝试通过副业来增加收入。那么上班族要如何寻找适合自己的副业呢?下面就给大家分享几…...

深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...