美文网首页
OC之NSThread

OC之NSThread

作者: 苏沫离 | 来源:发表于2018-11-30 20:19 被阅读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;
    }
    

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

    • 通过_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分析)

    相关文章

      网友评论

          本文标题:OC之NSThread

          本文链接:https://www.haomeiwen.com/subject/gcszsftx.html