OC之NSThread

2018-11-30  本文已影响0人  苏沫离

NSThread 面向对象,是对 pthread 的 OC 封装,解决了 C 语言使用不方便的问题,,但仍然需要程序员手动管理线程生命周期、处理线程同步等问题。是一个轻量级的多线程编程方法(相对GCDNSOperation)。

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后,将一段数据传至主线程,由于参数 waitUntilDoneYES,系统在此处阻塞当前线程直至 -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、RunLoopThread

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;
}

整体的流程可以概括为以下几步:

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分析)

上一篇下一篇

猜你喜欢

热点阅读