iOS NSThread

线程实例


创建一个线程

系统提供了三种创建线程的方法

- (instancetype)init;
// 通过指定对象和方法选择器的方式,argument是传递的参数
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
// 注意,这种方式是iOS10之后才增加的,将任务放在了block种执行
- (instancetype)initWithBlock:(void (^)(void))block;

可设置的一些信息

@property (nullable, copy) NSString *name;  // 线程名称
@property NSUInteger stackSize; // 栈的大小,4k的倍数,在线程start之前设置
@property double threadPriority; // 线程的优先级,【0-1】,值越大,优先级别越高,该属性不久的将来会废弃,可使用下面的属性
@property NSQualityOfService qualityOfService;  // 线程优先级,iOS8.0之后新增,有一下几种

NSQualityOfService优先级分类

NSQualityOfServiceUserInteractive // 与用户交互的任务,这些任务通常跟UI级别的刷新相关,比如动画,这些任务需要在一瞬间完成
NSQualityOfServiceUserInitiated // 由用户发起的并且需要立即得到结果的任务,比如滑动scroll view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成
NSQualityOfServiceUtility // 一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceBackground // 这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时
NSQualityOfServiceDefault // 优先级介于user-initiated 和 utility,当没有 QoS信息时默认使用,开发者不应该使用这个值来设置自己的任务

线程管理

- (void)cancel; // 取消线程
- (void)start; // 启动线程

线程的状态

@property (readonly, getter=isExecuting) BOOL executing; // 线程是否正在执行
@property (readonly, getter=isFinished) BOOL finished; // 线程是否完成
@property (readonly, getter=isCancelled) BOOL cancelled; // 线程是否已经取消

实例

  • 手动创建线程
// 创建线程
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"执行线程任务。。。");
}];
// 设置一些属性
thread.name = @"线程1";
// 开启线程
[thread start];
NSLog(@"线程信息:%@",thread);
// 取消线程
[thread cancel];
NSLog(@"线程是否被取消:%i",thread.isCancelled);

输出结果:

输出结果

  • 传递一些参数

    如果需要在线程任务中传递一些参数,我们需要使用另外一种创建方式

// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@{@"name":@"LOITA0164",@"age":@"25"}];
// 开启线程
[thread start];

-(void)run:(id)object{
    NSLog(@"传递过来的参数:%@",object);
}

结果:

结果

总结:

当我们创建NSThread实例后,我们为其设置一些属性,例如线程名称、线程优先级、栈大小等信息,与其相对的,我们需要手动管理线程的开始和取消等操作



创建线程

NSThread类不仅为我们提供了创建实例的方法,另外为我们提供了快捷使用线程的类方法,当你使用这类方法时,你的关注点不再是线程的创建和开启,而仅仅是想让线程去执行某些任务,另外,类的方法会自动开始执行线程任务。

注:使用类方法时,针对的都是当前的线程

同样的,有两种快捷使用方式,一种是通过block方式执行线程任务,另一种是通过对象和方法选择器实现线程任务。

// block方式执行任务
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// 方法选择器执行任务
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

示例

[NSThread detachNewThreadWithBlock:^{
    NSLog(@"执行新线程任务");
}];

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@[@"apple",@"pear"]];
-(void)run:(id)object{
    NSLog(@"传递过来的参数:%@",object);
}

结果:

结果

一些类方法

NSThread提供了一些类方法,管理线程的调度,获取当前的线程,线程的信息等

@property (class, readonly, strong) NSThread *mainThread;//获取到主线程
@property (class, readonly, strong) NSThread *currentThread;//获取当前线程
@property (class, readonly) BOOL isMainThread;//当前是否是主线程
+ (BOOL)isMultiThreaded;//当前线程是否是多线程
+ (void)sleepUntilDate:(NSDate *)date;//当前线程休眠到某时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;//当前线程休眠时长
+ (void)exit;//退出当前线程
+ (double)threadPriority;//当前线程的优先级
+ (BOOL)setThreadPriority:(double)p;//设置当前线程的优先级

示例

我们修改一下任务run的实现

-(void)run:(id)object{
    // 获取主线程
    NSThread *mainThread = [NSThread mainThread];
    mainThread.name = @"主线程";
    NSLog(@"主线程:%@",mainThread);
    // 获取当前的线程,可以通过这个线程实例使用线程方法
    NSThread *currentThread = [NSThread currentThread];
    currentThread.name = @"子线程";
    NSLog(@"当前线程:%@",currentThread);
    // 线程的优先级
    [NSThread setThreadPriority:0.8];
    NSLog(@"%f-%f",[NSThread threadPriority],currentThread.threadPriority);
    // 线程信息
    NSLog(@"多线程?:%i",[NSThread isMultiThreaded]);
    // 是否是主线程
    NSLog(@"当前线程是否是主线程:%i-%i",[NSThread isMainThread],[currentThread isMainThread]);
    // 线程休眠(阻塞)
    // 休眠时长
    [NSThread sleepForTimeInterval:1.0];
    // 休眠到某个时间点
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
    // 终止线程,一旦执行exit,线程之后的任务不再被执行
    [NSThread exit];
}

结果:

结果

结论:在某一线程任务中(包括主线程)使用NSThread的类方法时,都是针对某一线程的,如设置优先级、阻塞、终止等


线程通信

截止到现在,我们已经知晓了如何创建线程,管理线程,传递参数等等,但是读者仔细观察一下NSThread的方法,其中并没有为线程赋予任务的方法,这意味着我们通过[[NSThread alloc] init][NSThread new]的线程无法指定其任务,那么如果遇到一个需求:耗时的操作开辟子线程去完成,完成后进行刷新UI,这样的创建线程能否完成呢?我们知道UI需要在主线程中去完成的,那么为了完成上述需求,我们势必要在子线程中取到主线程,让其完成UI的更新,但是,正如之前提到的,NSThread并没有为线程赋予任务的方法,并且当前线程是只读属性,无法更换为主线程,因此,这种需求显然是无法实现的。(笔者试过取消当前线程,调用更新UI;还是创建block作为参数,回调结果更新UI,最终发现当前线程都是子线程,这说明消息传递默认都是在当前线程上完成的)

那么,NSThread无法完成了上述的需求了吗?答案是否定的,在NSThread的最后,apple扩展了NSObject对象,这些方法,是从实例对象角度,选择线程执行任务的,之前的NSThread都是从线程角度完成任务。

// 在主线程完成任务
// waitUntilDone:表示是否阻塞当前线程等待新任务结束(结束后会继续执行后面任务)
- (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;//相当于第一种方法使用kCFRunLoopCommonModes模式
// 选择某一线程完成任务
// waitUntilDone:表示是否阻塞当前线程等待新任务结束(结束后会继续执行后面任务)
- (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 NS_AVAILABLE(10_5, 2_0);//相当于第一种方法使用kCFRunLoopCommonModes模式
// 在后台完成任务
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

示例

我们来完成上述需求

@interface ViewController (){
    NSInteger _count; // 定义一个计数器
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 开启一个线程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

-(void)run{
    NSLog(@"%s,当前线程:%@",__func__,[NSThread currentThread]);
    _count = 1;
    NSLog(@"UI更新前的count:%ld",_count);
    // 在主线程上更新UI,这里只做计算器加1
    // 注:这里的waitUntilDone是YES,表示等待新任务结束后再继续执行
    [self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:YES];
    NSLog(@"UI更新后的count:%ld",_count);
}
-(void)updateUI:(NSNumber*)count{
    NSLog(@"%s,当前线程:%@",__func__,[NSThread currentThread]);
    _count++;
}

结果

结果

我们发现,updateUI中的线程是主线程,而不是子线程,另外我们发现,count变成了2,说明run中的任务是顺序执行的。

为了对比waitUntilDone的效果,我们将其改为NO
结果

我们发现,count依旧输出了两次,但是数量都是1,这说明run中的任务并没有顺序执行,因此waitUntilDone为YES还是NO需要根据需求而定。

另外需要注意的是,此时updateUI的主线程任务是加到主线程任务最后才被执行的,可能是因为先来先服务的原则吧。

为了对比NSThreadPerformAdditions扩展调用方法和普通的访问的不同,我们将

[self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:NO];

改为

[self updateUI:@(_count)];

结果:

结果

我们发现结果似乎和第一次一样,但是,需要注意的是,<NSThread: 0x1700798c0>{number = 3, name = (null)},这里,这说明线程依旧是原来的子线程(number为1时,才是主线程,因为number是递增的,主线程最先被创建出来,其值为1)

那么,这种调用方式似乎等同于下面的方法

[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

结果:

结果

结果一致,[object method]这种方式都是在当前线程上执行的。

类比拓展:延迟执行

[self performSelector:@selector(updateUI:) withObject:@(_count) afterDelay:1.0];

等同于

[NSThread sleepForTimeInterval:1.0];
[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

总结

1、我们可以创建子线程完成一些其他任务
2、NSThread提供了一些类方法,让我们可以便捷的创建使用子线程
3、NSThread的对象都是在创建时执行了线程任务,并未提供给线程赋予执行新任务的方式(因此及时你在子线程中拿到了主线程对象,你也无法执行主线程完成一些任务)
4、NSObject扩展了一些方法,用来指定在某些线程上完成任务
5、NSThread结合4完成:子线程执行耗时操作,主线程更新UI的任务需求
6、[object method]这种调用方式是以当前线程来执行的

个人观点:
不知道NSThread为何这样设计,我们无法直接通过NSThread获取主线程让其完成一些任务,而是通过NSObject去指定某种线程去完成任务;前者是NSThread层面,后者则是从对象层面,窃以后这种方式有点奇葩。


其他

关于线程安全问题,请查阅其他详细资料。

猜你喜欢

转载自blog.csdn.net/LOLITA0164/article/details/80772508
今日推荐