前言

一、主线程和子线程的区别

二、IOS - 子线程操作UI

三、IOS - NSThread

四、IOS - performSelector

五、常用的多线程方案


了解了进程和线程的区别、线程的原理、线程优缺点等知识点之后。具体总结一下IOS的多线程使用。


一、主线程和子线程的区别

先来看一个demo:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建一个子线程,执行test任务
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    [thread start];
}

- (void)test {
    //在子线程中,添加一个timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:true];
    //添加到runloop中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //开启runloop,保持循环
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
}

- (void)run {
    NSLog(@"1");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //点击屏幕,让主线程退出
    [NSThread exit];
}

开启一条子线程,在子线程中开启计时。然后我点击屏幕让主线程退出。此时主线程会被杀掉吗?如果主线程杀掉,子线程会仍然保持运行吗?
答案是:主线程会被杀掉,子线程仍然会保持运行,而且app不会崩溃,但是点击屏幕之后,所有UI都无响应了。
通过这个demo可以总结:

对于操作系统来说,app的主线程就是一个子线程,所以主线程和子线程在本质上没有任何区别。

但是IOS系统对待主线程和子线程是有区别的,它的区别在于:所有的UI渲染都必须在主线程上执行。

之所以放在主线程,是因为考虑到性能问题:

如果在渲染UI的时候,用多个线程去操作,会产生混乱问题。要么只用一条线程去加载,要么就通过多线程加锁来解决线程安全问题。

通过多线程加锁方案,首先开启多个线程会耗费cpu的资源以及占用内存,同时加锁也会耗费性能。基于这种情况考虑,苹果选择了用一条线程来加载渲染UI。


二、IOS - 子线程操作UI

在了解子线程和主线程的区别之后,抛出一个问题,如果在子线程操作UI,比如在子线程更新UILabel的text,会有效果吗?

首先明确一点:子线程里面是可以更新UI的。

比如我们在设置UI文字、背景颜色等,简单的来说是通过Core Animation 将处理过后的Layer递归打包发送到RenderServer,最终通过GPU渲染。在这个过程中,UI操作涉及到访问各种View对象的属性和渲染。如果我们在子线程去操作,由于UIKit不是线程安全的,所以会造成最少两个方面的问题:
(1)会存在读写不一致的问题。
(2)在渲染方面,由于图像的渲染需要以60帧的刷新率在屏幕上同步更新,在非主线程异步的情况下,无法确定这个处理过程能够实现同步更新。
所以简单的或者极少数的UI操作,比如设置文字,颜色等,在子线程中可能会及时更新。但是非常不建议在子线程中去做UI相关的操作。


三、IOS - NSThread

1、 NSThread基本用法

(1)创建一个thread,然后执行start方法

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[thread start];

(2)通过detach来默认启动一条线程

+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) NS_SWIFT_DISABLE_ASYNC;

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

(3)通过alloc出NSThread创建线程的时候,需要手动执行start方法,紧接着会执行main方法。
而通过detachNewThread来启动线程,会默认创建NSThread并执行start方法。
下两个符号断点:-[NSThread start]和-[NSThread main],通过汇编分析:

在start方法中,会通过pthread_create来创建一条线程。

然后来到main方法中,执行target的selector方法。

结论:NSThread 和 pthread是一一对应的,NSThread是pthread的一层抽象封装。

当然NSThread也可以设置线程的名称、优先级以及判断线程是否是主线程等方法。

2、cancel和exit方法

例如下面代码:

@property (nonatomic, strong) NSThread *curThread;
.....
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    self.curThread = thread;
    [self.curThread start];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,0.5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self.curThread cancel];
    });
}

- (void)test {
    NSLog(@"任务开始");
    while (![NSThread currentThread].isCancelled) {
        NSLog(@"任务执行");
    }
    NSLog(@"任务结束");
}

当开启一条线程,然后延时0.5秒之后执行cancel方法时,通过判断[NSThread currentThread].isCancelled来停止循环,此时任务结束。
为了验证线程是否被销毁:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
     [self performSelector:@selector(test) onThread:self.curThread withObject:nil waitUntilDone:false];
});

我延时1.0秒,继续在该线程执行test任务,结果并没有执行。这就证明,当线程任务执行完之后,线程就会销毁了。
为了不让线程在执行完任务之后销毁,我们将此线程的runloop开启,并且加入一个port,不让runloop退出:

- (void)test {
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    NSLog(@"任务开始");
    while (![NSThread currentThread].isCancelled) {
        NSLog(@"任务执行");
    }
    NSLog(@"任务结束");
    [[NSRunLoop currentRunLoop] run];
}

此时线程会是一个常驻线程,也就不会退出了。
那么如何把一个常驻线程退出呢?需要通过 [NSThread exit];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,2.0*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(test1) onThread:self.curThread withObject:nil waitUntilDone:false];
    });
......
- (void)test1 {
    [NSThread exit];
}

我继续延时2.0秒,执行该线程的test1任务,在任务中调用 [NSThread exit]。此时线程会被销毁,runloop也退出了。

或者通过移除Runloop中刚才添加的port,让Runloop先退出,然后线程会自动销毁。

当然也可以通过变量控制,这里会在探索Runloop的时候具体分析。

四、IOS - performSelector

1、NSObject中的performSelector

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
//底层实现:
- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}

这3个performSelector方法其底层都是走的objc_msgSend,和我们直接调用一个方法没有区别。

2、NSThreadPerformAdditions分类

@interface NSObject (NSThreadPerformAdditions)

- (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;
	// equivalent to the first method with kCFRunLoopCommonModes
// waitUntilDone设置为YES,会阻塞主线程,优先在子线程中执行方法,方法执行完后主线程再继续后面的打印
// waitUntilDone设置为NO,子线程的方法和主线程的打印同时进行
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
	// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

注意

(1)其中前4个方法,是在主线程或者开启子线程去执行任务,而performSelectorInBackground就是创建一个NSThread然后去start。这里就不具体的分析了。

(2)后3个方法(主线程/子线程->子线程),必须保证要执行事件的子线程开启RunLoop。


3、NSDelayedPerforming 分类

@interface NSObject (NSDelayedPerforming)

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end

这个分类同时提供了延时和取消的方法,但是有一点需要注意:

它是基于runloop中的timer的,如果在子线程中使用的话,需要手动开启runloop,否则不会执行。


4、NSRunloop的扩展

@interface NSRunLoop (NSOrderedPerform)

- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;

@end

这个方法很少在项目中用到,但是功能我感觉是真的强大,它可以根据order来控制执行的顺序。因为它是基于Runloop的observer回调,所以在子线程中需要开启Runloop。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    _port1 = [NSPort port];
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    thread.name = @"custom thread";
    [thread start];
    /*
    dispatch_queue_t q = dispatch_queue_create("xxxx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(q, ^{
        [self test];
    });
     */
}

- (void)test {
    
    [[NSRunLoop currentRunLoop] addPort:_port1 forMode:NSRunLoopCommonModes];
    [[NSRunLoop currentRunLoop] performSelector:@selector(test1) target:self argument:nil order:1 modes:@[NSRunLoopCommonModes]];
    [[NSRunLoop currentRunLoop] performSelector:@selector(test4) target:self argument:nil order:4 modes:@[NSRunLoopCommonModes]];
    [[NSRunLoop currentRunLoop] performSelector:@selector(test3) target:self argument:nil order:3 modes:@[NSRunLoopCommonModes]];
    [[NSRunLoop currentRunLoop] performSelector:@selector(test2) target:self argument:nil order:2 modes:@[NSRunLoopCommonModes]];
    /*
    //或者:让runloop只跑一次
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantPast]];
    */
    [self performSelector:@selector(cancel) withObject:nil afterDelay:1];
    [[NSRunLoop currentRunLoop] run];
}

- (void)test1 {
    NSLog(@"1");
}

- (void)test2 {
    NSLog(@"2");
}

- (void)test3 {
    NSLog(@"3");
}
- (void)test4 {
    NSLog(@"4");
}

- (void)test6 {
    NSLog(@"===");
}

- (void)cancel {
    
    [[NSRunLoop currentRunLoop] removePort:_port1 forMode:NSRunLoopCommonModes];
    /*
     如果是用gcd开启的线程,这里exit会崩溃。(EXC_BAD_INSTRUCTION )
     [NSThread exit];
     */
}

打印顺序为 1 2 3 4。同样的如果在子线程中执行,需要开启runloop,并保持事件循环。注意必要时需要退出线程(通过移除port,退出runloop)。


五、常用的多线程方案

IOS最常用的多线程方案就是GCD和NSOperation&NSOperationQueue了。
这个两个方案后面会具体进行分析。