NSThread 面向对象,是对 pthread
的 OC 封装,解决了 C 语言使用不方便的问题,,但仍然需要程序员手动管理线程生命周期、处理线程同步等问题。是一个轻量级的多线程编程方法(相对GCD
和 NSOperation
)。
1、NSThread API
1.1、创建分线程
/* 创建一个 NSThread
* @note 实例方法创建 NSThread,可以获取实例对象,使用该对象调用 -start 开启分线程;
* @note 类方法创建的 NSThread,默认开启分线程。不可以获取实例对象,也就不用 -start ;
* @note 如果开辟的线程是程序中第一个分线程,系统发送通知 NSWillBecomeMultiThreadedNotification;
*/
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument ;
- (instancetype)initWithBlock:(void (^)(void))block ;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
/* 异步开辟一条线程,此时 executing=YES ,在新线程调用 -main 方法。
* @note 仅仅被实例方法创建的对象调用
*/
- (void)start;
1.2、线程休眠
/* 当前线程休眠到指定时间;
* @param date 截止时间
* @note 当线程休眠时,runloop 不会发生
*/
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
1.3、线程停止
/* 终止当前线程:
* @note 线程退出之前没有机会清除线程中分配的内存,不建议调用;
* @note 触发通知 NSThreadWillExitNotification;同步发送,观察者在线程退出之前都能收到通知。
*/
+ (void)exit;
/* 取消操作
* @note 仅仅将属性 cancelled 改为 YES,NSThread 会定期查询 cancelled 的状态
* 线程取消:CFRunLoopStop(CFRunLoopGetCurrent()); //停止当前线程的runLoop
*/
- (void)cancel;
1.4、线程主体
/* 线程的主体,封装的操作
* @note 子类化 NSThread,将待执行任务封装在 -main 方法中;
* 此时不需要调用 [super main],否则将实现初始化的 target 与 selector
*/
- (void)main;
1.5、 线程状态
//thread 是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;
//thread 是否完成了执行
@property (readonly, getter=isFinished) BOOL finished;
//thread 是否被取消。
@property (readonly, getter=isCancelled) BOOL cancelled;
1.6、 主线程相关
//当前线程是否是主线程
@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread ;
//获取主线程对象
@property (class, readonly, strong) NSThread *mainThread;
1.7、 查询线程环境
/* 判断应用程序是否是多线程的
* 如果使用 NSThread 将开启一个分线程,则该方法返回 YES;
* 如果在应用程序中使用非cocoa API(如 POSIX 或 Multiprocessing Services API)开启一个分线程,该方法将返回 NO
*/
+ (BOOL)isMultiThreaded;
//获取当前正在执行的线程的对象
@property (class, readonly, strong) NSThread *currentThread;
// 调用堆栈返回地址 的数组:每个元素都是包含NSUInteger值的NSNumber对象
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses ;
/* 调用堆栈符号的数组:描述在调用此方法时当前线程的调用堆栈回溯 ;
* 每个元素都是一个NSString对象,其值的格式由 backtrace_symbols() 函数确定。
*/
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;
//使用该字典来存储特定于线程的数据。该字典在任何对NSThread对象的操作中都不使用——它只是一个可以存储趣数据的地方。例如,Foundation 使用它存储线程的默认NSConnection和NSAssertionHandler实例。可以为字典自定义其键值。
@property (readonly, retain) NSMutableDictionary *threadDictionary;
//线程的名字
@property (nullable, copy) NSString *name;
//thread 的堆栈大小(这个值必须以字节为单位,并且是4KB的倍数)。要更改堆栈大小,必须在启动线程之前设置此属性。在线程启动后设置堆栈大小会更改属性大小(stackSize方法反映了这一点),但不会影响为线程预留的页面的实际数量。
@property NSUInteger stackSize;
1.8、优先级
/* 线程的优先级:当存在资源争用时,设置更高优先级获得更多的资源
* NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
* NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回的任务
* NSQualityOfServiceDefault:线程默认优先级
* NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回的任务
* NSQualityOfServiceBackground:后台优先级,用于完全不紧急的任务
*/
@property NSQualityOfService qualityOfService;
+ (double)threadPriority;
@property double threadPriority;
//设置线程的优先级
+ (BOOL)setThreadPriority:(double)p;
1.9、相关通知
//由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次
NSString * const NSWillBecomeMultiThreadedNotification;
//目前没有使用,可以忽略
NSString * const NSDidBecomeSingleThreadedNotification;
// 线程退出之前发送通知
NSString * const NSThreadWillExitNotification;
思考一下: 如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒?
2、线程间通信
/* 使用指定 modes 在指定 thread 执行 aSelector
* @param thread :指定的线程,用于执行 aSelector ;
* @param aSelector :在指定的线程中执行的任务;
* @param modes :执行模式,默认为 kCFRunLoopCommonModes;
* 如果为该参数指定nil或空数组,则不会执行 aSelector;
* @param wait :是否阻塞当前线程,直到 aSelector 执行完毕;
* 如果当前线程也是主线程,并且 wait 指定YES,则消息将立即被发送和处理;
* @note 该方法一旦被分发到主线程队列,就不能再被取消执行
*/
@interface NSObject (NSThreadPerformAdditions)
//回到主线程执行 aSelector
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
//在指定线程上执行 aSelector
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
@end
2.1、回到主线程
假如数据关联性特别强,需要在分线程操作一些耗时任务,然后回到主线程更新UI,接着根据更新UI的结果接着往下处理数据,我们应如何在线程间通信呢?我们来看以下程序:
- (void)newChileThread{
//先监听线程退出的通知,以便知道线程什么时候退出
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadExitNotice:) name:NSThreadWillExitNotification object:nil];
if (@available(iOS 10.0, *)){
[NSThread detachNewThreadWithBlock:^{
NSThread.currentThread.name = @"数据线程";
[NSThread sleepForTimeInterval:10];//处理耗时任务
NSLog(@"---------- 处理数据了 ----------- %@",[NSDate date]);
[self performSelectorOnMainThread:@selector(backMainThreadClick:) withObject:@{@"data":@"handle data"} waitUntilDone:YES];
NSLog(@"---------- 再次处理数据了 ----------- %@",[NSDate date]);
[NSThread sleepUntilDate:[[NSDate date] dateByAddingTimeInterval:10]];//z接着处理耗时任务
NSLog(@"---------- 处理数据完毕 ----------- %@",[NSDate date]);
}];
}
}
- (void)backMainThreadClick:(id)userInfo{
NSLog(@"刷新UI --- %@",userInfo);
[NSThread sleepForTimeInterval:3];//阻塞主线程 3s
}
我们开辟了一条新线程用来处理耗时任务,并且监听该线程的退出;中途由于关于界面的数据已经处理完毕,这时我们可以回到主线程刷新 UI,调用-performSelectorOnMainThread: withObject: waitUntilDone:
方法,参数waitUntilDone
设置为YES
,我们来看控制台的打印结果:
2018-07-03 19:17:48.759882+0800 ThreadDemo[967:43885] ---------- 处理数据了 ----------- 2018-07-03 11:17:48 +0000
2018-07-03 19:17:48.760508+0800 ThreadDemo[967:43826] 刷新UI --- {
data = "handle data";
}
2018-07-03 19:17:51.762262+0800 ThreadDemo[967:43885] ---------- 再次处理数据了 ----------- 2018-07-03 11:17:51 +0000
2018-07-03 19:18:01.763943+0800 ThreadDemo[967:43885] ---------- 处理数据完毕 ----------- 2018-07-03 11:18:01 +0000
2018-07-03 19:18:01.764444+0800 ThreadDemo[967:43885] threadExitNotice ------------ <NSThread: 0x600003c455c0>{number = 3, name = 数据线程}
我们可以看到:系统先在分线程处理了一个耗时任务,然后立即回到主线程刷新UI,此处的分线程一直等到主线程处理完UI事件,才接着在该分线程处理任务,直到任务处理完毕,退出该线程
思考一下: 假如分线程的任务可以独立执行处理,我们还需要在等到主线程的方法执行完再接着处理下面的任务嘛?这显然耽误了时间,浪费了效率。
还是上述程序,我们将参数waitUntilDone
传为NO
,
2018-07-03 19:19:38.867522+0800 ThreadDemo[1000:45692] ---------- 处理数据了 ----------- 2018-07-03 11:19:38 +0000
2018-07-03 19:19:38.868219+0800 ThreadDemo[1000:45638] 刷新UI --- {
data = "handle data";
}
2018-07-03 19:19:38.868225+0800 ThreadDemo[1000:45692] ---------- 再次处理数据了 ----------- 2018-07-03 11:19:38 +0000
2018-07-03 19:19:48.869657+0800 ThreadDemo[1000:45692] ---------- 处理数据完毕 ----------- 2018-07-03 11:19:48 +0000
2018-07-03 19:19:48.870864+0800 ThreadDemo[1000:45692] threadExitNotice ------------ <NSThread: 0x6000015de840>{number = 3, name = 数据线程}
我们可以看到:系统在回到主线程刷新UI的同时,接着处理分线程后面的任务。
2.2、 线程间通信
我们希望开辟一条分线程,用来处理数据,同时和主线程进行通信:代码如下所示:
- (void)newChileThread{
if (@available(iOS 10.0, *)){
[NSThread detachNewThreadWithBlock:^{
NSThread.currentThread.name = @"数据线程";
[self performSelector:@selector(newChileThreadTask:) withObject:@{@"data":@"A"}];
}];
}
}
- (void)newChileThreadTask_A:(id)userInfo{
NSLog(@"userInfo =========== %@",userInfo);
if ([userInfo[@"data"] isEqualToString:@"A"]){
[NSThread sleepForTimeInterval:10];//处理耗时任务
NSLog(@"处理数据A ======= %@",NSThread.currentThread);
//任务A处理完毕,回调主线程执行某些操作,并阻塞当前线程直至主线程的操作完成
[self performSelectorOnMainThread:@selector(backMainThreadClick:) withObject:@{@"result":@"1",@"thread":NSThread.currentThread} waitUntilDone:YES];
}
NSLog(@"处理完毕A ===========");
}
- (void)newChileThreadTask_B:(id)userInfo{
[NSThread sleepForTimeInterval:10];//处理耗时任务
NSLog(@"处理数据B ----------- %@",NSThread.currentThread);
}
- (void)backMainThreadClick:(id)userInfo{
NSLog(@"userInfo ---------- %@",userInfo);
NSThread *thread = userInfo[@"thread"];
[NSThread sleepForTimeInterval:3];//阻塞主线程 3s
if ([userInfo[@"result"] isEqualToString:@"1"]){
NSLog(@"currentThread ----- %@",NSThread.currentThread);
[self performSelector:@selector(newChileThreadTask_B:) onThread:thread withObject:@{@"data":@"B"} waitUntilDone:NO];
}
}
根据以上代码:我们在新开辟的分线程执行- performSelector: withObject:
,转到 -newChileThreadTask_A:
方法执行处理数据 A 的耗时任务,在此耗时10s后,将一段数据传至主线程,由于参数 waitUntilDone
为 YES
,系统在此处阻塞当前线程直至 -backMainThreadClick:
方法里执行完毕才会接着执行分线程下面的代码,我们期望接下来执行处理数据B的任务;
我们来看下打印结果:
10:50:35.719040+0800 userInfo =========== { data = A; }
10:50:45.727849+0800 处理数据A ======= <NSThread: 0x600002f81140>{number = 7, name = 数据线程}
10:50:45.728304+0800 userInfo ---------- { result = 1;thread = "<NSThread: 0x600002f81140>{number = 7, name = \U6570\U636e\U7ebf\U7a0b}";}
10:50:48.728859+0800 currentThread ----- <NSThread: 0x600002fe2d40>{number = 1, name = main}
10:50:48.729764+0800 处理完毕A ===========
疑问: -newChileThreadTask_B:
方法没有被执行,为什么?
3、RunLoop
与 Thread
Runloop 是一个用来调度工作和协调接受的事件的死循环。一个Runloop的目的是有任务的时候保持线程忙碌,没有任务的时候线程休眠。
RunLoop
寄生于线程:一个线程只能有唯一对应的RunLoop
,但这个根RunLoop
里可以嵌套子RunLoop
,主线程的RunLoop
自动创建,子线程的RunLoop默认不创建,在子线程中调用NSRunLoop.current获取RunLoop对象的时候,就会创建RunLoop
3.1、RunLoop 的创建
CFRunLoop.c
文件的部分源代如下:
static CFMutableDictionaryRef __CFRunLoops = NULL;
static pthread_t kNilPthreadT = (pthread_t)0;
// t==0 is a synonym for "main thread" that always works //当t==0时代表主线程
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
//进行加锁操作
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
// 第一次进入时,初始化全局dict,并先为主线程创建一个 RunLoop。并将mainLoop添加到dict中
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//通过线程直接从dict中获取loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//如果获取失败,通过线程创建一个loop,
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//再次确认没有loop,就添加到dict中。
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
//获取当前线程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
//获取主线程的RunLoop
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函数传入一条线程。
- 判断线程是否为主线程并且判断是否已经存在__CFRunLoops(全局CFMutableDictionaryRef)。
- 如果不存在,说明第一次进入,初始化全局dict,并先为主线程创建一个 RunLoop。并将mainLoop添加到dict中。
- 如果__CFRunLoops存在,会通过对应线程在全局的__CFRunLoops中查找对应的RunLoop。
- 如果对应RunLoop不存在,会创建一个新的RunLoop,并添加到__CFRunLoops中。
- 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
返回RunLoop。
我们只能通过CFRunLoopGetMain函数或者CFRunLoopGetCurrent函数来获取RunLoop,通过上面的源代码我们发现,无论是CFRunLoopGetMain函数还是CFRunLoopGetCurrent函数,都是通过对应的线程获取对应的RunLoop,线程和RunLoop是一一对应的,不会重复创建。在主线程,系统会帮我们创建RunLoop,来处理事件。而子线程RunLoop并不会默认开启。所有,子线程操作完成后,线程就被销毁了,如果我们想线程不被销毁,得主动获取一个RunLoop,并且在RunLoop中添加Timer/Source/Observer其中的一个。
3.2、使用 RunLoop 常驻线程
首先,我们添加一个通知监听线程退出的事件
//在适当的位置添加通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadExitNotice:) name:NSThreadWillExitNotification object:nil];
//通知响应的方法
- (void)threadExitNotice:(NSNotification *)notification
{
NSLog(@" threadExitNotice ------------ %@",notification.object);
}
这时,我们开启一个线程:
- (void)residentThread
{
if (@available(iOS 10.0, *))
{
//常驻线程
[NSThread detachNewThreadWithBlock:^{
NSThread.currentThread.name = @"常驻线程";
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
NSLog(@"---------- 此处代码不被执行 -----------");
}];
}
}
然后我们运行程序,可以发现,在新开启的分线程里,代码执行到[runLoop run];
就不再往后执行了,也就是说线程阻塞了。而且没有收到线程退出的通知。
参考文章:
iOS多线程篇:NSThread
小笨狼漫谈多线程:NSThread
多线程实现方案之一 : NSThread
RunLoop(从源码分析到Demo分析到mainLoop log分析)
网友评论