iOS开发之多线程笔记(三)之NSThread

NSThread

官方文档上的介绍翻译过来的大致意思就是:

  • 概述
  1. 当你有一个方法需要在它自己的线程中运行的时候可以使用该类.
  2. 当你需要执行一个耗时冗长的任务而又不希望它阻塞应用程序的其他线程时,threads就显得额外的有效.特别是你能用它避免阻塞了用来处理用户交互的主线程.
    3.线程还可以将一个大型的任务瓜分成多个小任务,这样可以充分利用和突出多核计算机的性能.
  • NSThread类似NSOperation,能支持线程状态监测.
  1. 你可以取消或者继续一个正在执行的线程.
  2. 如何用代码来请求取消一个线程,有关更多信息,请参见“取消”的说明。
  • 子类化
    你可以继承NSThread并重写main方法来实现主入口,如果你重写main方法,你不需要调用super方法.

构造方法(OC&&Swift)

  • 静态构造方法:
/**
     静态构造方法1

     @param testMethod 需要在线程中执行的方法
     @param target 方法的接受者
     @param object 需要传递的参数
     @return void
*/
    [NSThread detachNewThreadSelector:@selector(testMethod) toTarget:self withObject:nil];

/**
静态构造方法2 (此方法iOS10之后才有)
@param block 需要在线程中执行的代码块
@return void
*/
[NSThread detachNewThreadWithBlock:^{
        
 }];

ps: 以上2种构造方法调用,线程会立刻开始执行
优点:使用方便.代码简洁.
缺点:不返回实例对象,线程不可控.
  • 返回实例对象的构造方法
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
       
}];

NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(testMethod) object:nil];

ps: 参数同静态方法,这里不再赘述.同样带有block代码块的方法iOS10之后才有.

这种形式创建的thead不会自己开始执行.需要调用start方法才能够开始执行

[thread start];
  • swift创建方法
        let thread1 = Thread(target: self, selector: #selector(testMethod), object:nil)
        let thread2 = Thread(block: {
           
        })
        thread1.start()
        thread2.start()
ps: 
1. swift中都是有返回实例对象的.
2. 一种传方法接受者,方法和传递参数,另一种是传入一个闭包.
3. 都不会自动执行,需要调用start方法才能开始执行.

相关方法

  1. 获取当前代码块在哪个线程中执行
//oc
NSThread *currentThread = [NSThread currentThread];
//swift
let currentThread = Thread.current
  1. 获取主线程
//oc
NSThread *mainThread =  [NSThread mainThread];
//swift
let mainThread =  Thread.main
  1. 线程操作
/*
退出线程
*/
//oc
[NSThread exit];
//swift
Thread.exit()

/*
取消线程
*/
//oc
NSThread *thread = ...;
[thread cancel];
//swift
let thread = ...
thread1.cancel()

/*
线程中延时处理
*/
//oc

[NSThread sleepUntilDate:(nonnull NSDate *)];
[NSThread sleepForTimeInterval:(NSTimeInterval)]
//swift
Thread.sleep(until: Date)
Thread.sleep(forTimeInterval: TimeInterval)
ps:效果一样,只是传入时间类型不同.
  1. 线程状态
/*
oc
//是否是主线程
[thread1 isMainThread];
//线程是否取消执行
[thread1 isCancelled];
//线程是否完成
[thread1 isFinished];
//线程是否正在调用中,执行中
[thread1 isExecuting];
*/

/*
swift

thread1.isCancelled;
thread1.isMainThread;
thread1.isFinished;
thread1.isExecuting;
*/

这里说下:

  1. 调用exit方法后,此时在外部访问isFinished, isFinished = YES.
  2. 调动cancel方法后,此时在外部访问isFinished, isFinished = YES. isCancelled = YES

cancel和exit区别

  1. cancel 这个方法会将正在执行的当前进程信息保存给接收者,然后再将进程取消,同时会通过方法isCancled反馈状态,如果成功取消,isCancled将会返回YES,否则返回NO;进程被取消后,会调用exit方法;
  2. exit 直接退出.
  • 如果字面意思不好理解,这里举个例子你就明白了.(这里以swift代码为例)

循环0-9,当执行到5时,取消掉线程

    //MARK: NSThread
    @objc private func NSThread_test() -> Void {
        let thread2 = Thread(block: {
            for i in 0..<10 {
                print("\(i)")
                if i == 5 {
                    Thread.current.cancel()
                }
            }
        })
        thread2.start()
    }

打印日志如下

0
1
2
3
4
5
6
7
8
9

循环0-9,当执行到5时,退出线程

    //MARK: NSThread
    @objc private func NSThread_test() -> Void {
        let thread2 = Thread(block: {
            for i in 0..<10 {
                print("\(i)")
                if i == 5 {
//                    Thread.current.cancel()
                    Thread.exit()
                }
            }
        })
        thread2.start()
    }

打印日志如下:

0
1
2
3
4
5

所以看完日志,不难发现一个问题,cancel并不能exit线程,只是标记为canceled,但线程并没有死掉。你在子线程中执行了一个循环,则cancel后,循环还在继续.

  • 但是上面第一种情况,我既想让他cancel掉有不继续执行循环了怎么办
  • 此时你需要在循环的条件判断中加入isCancelled 来判断子线程是否已经被cancel来决定是否继续循环。代码如下:
//MARK: NSThread
    @objc private func NSThread_test() -> Void {
        let thread2 = Thread(block: {
            for i in 0..<10 {
//此处加上判断,如果线程已经标记为取消状态了,那么就跳出循环
                guard !Thread.current.isCancelled else {
                    break;
                }
                print("\(i)")
                if i == 5 {
                    Thread.current.cancel()
                }
            }
        })
        thread2.start()
    }

打印日志如下:

0
1
2
3
4
5

还有:当线程已经标记为isFinished=YES,如果此时再调用start会crash

相关属性

1. 设置线程名称,便于区分线程
//oc
[thread1 setName:@"blockThread"];
//swift
thread2.name = "threadName"

2. 设置线程优先级 (0-1),值越大优先级越高,优先执行概率越大
//oc
[thread1 setThreadPriority:0.5];
//swift
thread2.threadPriority = 0.8

3. 栈区大小
默认情况下,无论是主线程还是子线程,栈区大小都是512KB
栈区大小可以设置,最小16KB,但是必须是4KB的整数倍

隐式开启线程(线程间通信)

  • 在NSThread.h文件中我们可以看到如下的一段NSObject扩展代码
@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

- (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. 第1,2个方法,是指在主线程中执行一段任务.
  2. 第3,4个方法,是指在指定的NSThread中执行一段任务.
  3. 第5个方法,是指在后台执行一段任务.(这里的后台不是指Home键App退出后台状态).这里的后台指的是一种优先级:后台优先级,用于完全不紧急的任务.

这里主要说下以上方法中的wait参数和modes参数

先来看wait设置成YES和NO分别有什么区别:

  • 模拟下这样一个场景,在子线程循环0-9,当执行到5时,在主线执行一段代码.这里以oc示例:
  • wait = YES 时
 NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        for(int i = 0; i < 10; i++) {
            NSLog(@"%d", i);
            sleep(1);
            if(i == 5) {
//这里waitUntilDone我们设置成YES
                [self performSelectorOnMainThread:@selector(testMethod) withObject:nil waitUntilDone:YES];
            }
        }
    }];
[thread1 start];

- (void)testMethod {
    sleep(2);
    NSLog(@"----");
}

打印日志如下:

2018-01-16 17:47:10.433601+0800 Thread[8490:1502849] 0
2018-01-16 17:47:11.434261+0800 Thread[8490:1502849] 1
2018-01-16 17:47:12.435614+0800 Thread[8490:1502849] 2
2018-01-16 17:47:13.437184+0800 Thread[8490:1502849] 3
2018-01-16 17:47:14.438567+0800 Thread[8490:1502849] 4
2018-01-16 17:47:15.439235+0800 Thread[8490:1502849] 5
2018-01-16 17:47:18.442095+0800 Thread[8490:1502651] ----
2018-01-16 17:47:18.442516+0800 Thread[8490:1502849] 6
2018-01-16 17:47:19.443534+0800 Thread[8490:1502849] 7
2018-01-16 17:47:20.444993+0800 Thread[8490:1502849] 8
2018-01-16 17:47:21.446189+0800 Thread[8490:1502849] 9

可以看到:子线程循环到5后就进入了阻塞状态,转而去执行主线程中的任务,从打印"5"和打印"----"中间间隔了三秒时间差就可以看出来.当主线程任务执行完毕后才恢复循环.

  • wait = NO 时
 NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        for(int i = 0; i < 10; i++) {
            NSLog(@"%d", i);
            sleep(1);
            if(i == 5) {
//这里waitUntilDone我们设置成YES
                [self performSelectorOnMainThread:@selector(testMethod) withObject:nil waitUntilDone:NO];
            }
        }
    }];
[thread1 start];

- (void)testMethod {
    sleep(2);
    NSLog(@"----");
}

打印日志如下:

2018-01-16 17:53:24.618734+0800 Thread[10529:1521148] 0
2018-01-16 17:53:25.619506+0800 Thread[10529:1521148] 1
2018-01-16 17:53:26.621291+0800 Thread[10529:1521148] 2
2018-01-16 17:53:27.623589+0800 Thread[10529:1521148] 3
2018-01-16 17:53:28.624586+0800 Thread[10529:1521148] 4
2018-01-16 17:53:29.625488+0800 Thread[10529:1521148] 5
2018-01-16 17:53:30.626444+0800 Thread[10529:1521148] 6
2018-01-16 17:53:31.627608+0800 Thread[10529:1521148] 7
2018-01-16 17:53:32.627668+0800 Thread[10529:1520884] ----
2018-01-16 17:53:32.628229+0800 Thread[10529:1521148] 8
2018-01-16 17:53:33.629855+0800 Thread[10529:1521148] 9

可以看到,子线程在执行到5后,循环仍在继续,而从打印"5"到打印"----"的时间差来看也是三秒.说明主线程中的任务的确是在打印5的时候就开始同时执行了.
总结:
1. 如果wait传YES,则为同步执行,会阻塞当前线程.
2. 如果wait传NO,则为异步执行,不会阻塞当前线程.

在来看下modes这个参数如何使用

  • 我们先来看下官方文档怎么说
models.png

大致意思:

  1. models是个存储NSString类型的数组.
  2. models中的元素用来识别selector触发的时机.
  3. models至少包含一个元素,如果为空,则不会有任何作用.
  4. 从最后一句可以看出models里面的元素和Run Loops知识相关
    因为Run Loop是个大模块,这里不过多赘述,这里models数组里面存储的其实就是RunLoopModel
typedef NSString * NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;

如上,它其实就是个string类型.

平时我们使用最多的是Default模式, Event tracking模式,Common模式.这里介绍这三种.

  1. 先看示例代码,点击按钮触发方法performSelectorTest,延时3秒后执行testMethod
- (void)performSelectorTest {
    NSLog(@"设置方法!!!");
    [self performSelector:@selector(testMethod)
               withObject:nil
               afterDelay:3
                  inModes:@[NSDefaultRunLoopMode]];
}
- (void)testMethod {
    NSLog(@"开始执行!!!");
}
  1. 控制台打印如下:
2018-01-18 22:07:57.157753+0800 Thread[34060:209224] 设置方法!!!
2018-01-18 22:08:00.159394+0800 Thread[34060:209224] 开始执行!!!
  1. 时间相差3秒,一切正常.
  2. ok,问题来了,此时我在控制器上拖一个tableView,看图:


    1.gif
  3. 控制台日志:
2018-01-18 22:12:45.305354+0800 Thread[34060:209224] 设置方法!!!
2018-01-18 22:12:52.947010+0800 Thread[34060:209224] 开始执行!!!
  1. 两次打印的时间间隔是7秒,并不是上面代码写的3秒.
  2. 当点击了按钮后我开始一直滑动tableView,时间超过3秒后下一条日志并没有打印出来,也就是说performSelector里面的方法并没有按照预期的开始调用.直到我停止了滑动才开始调用,第二条日志才打印出来.
  3. 而这就是RunLoopModel的神奇之处.
  4. 原因是在default mode下,当用户在拖动UITableView处于UITrackingRunLoopMode模式时,默认模式下的数据便无法处理。
    总结
    默认模式下一般情况下可以正常达到预期效果,但是当有类似滑动屏幕如按住UITableView拖动时等操作,该模式下的selector便无法做出响应
  1. 上面说的tableview拖动操作就是出于该种模式下
  2. 代码同上,这里传入UITrackingRunLoopMode
- (void)performSelectorTest {
    NSLog(@"设置方法!!!");
    [self performSelector:@selector(testMethod)
               withObject:nil
               afterDelay:3
                  inModes:@[UITrackingRunLoopMode]];
}

#pragma mark - Logic Helper
- (void)testMethod {
    NSLog(@"开始执行!!!");
}
  1. 当点击完按钮后,我立刻去不停的拖动tableview,发现控制台打印结果和预期的就一样了.效果如图
    2.gif
  2. 看似问题解决了,ok那么问题又来了.当我点击完按钮不去滑动tableview,而是静等3秒,发现第二条日志并没有打印出来.现象如图:


    3.gif
  3. 如图,我点击了按钮后,等待了远不止3秒,第二条日志迟迟不来.
  4. 其实它永远都不会被触发了,只有当我再次滑动tableview进入UITrackingRunLoopMode模式它才会再次触发.
    总结: 在拖动loop或其他user interface tracking loops时处于此种模式下,performSelector里的方法才会被触发

所以如果既想在默认模式下触发,又想在拖动时能够触发,由于models参数是一个数组类型,所以我们可以将这2种模式都设置进去就好了.当然还有一种模式可以达到同样的效果NSRunLoopCommonModes

  • NSRunLoopCommonModes
    这是一个伪模式,其为一组run loop mode的集合,将输入源加入此模式意味着在Common Modes中包含的所有模式下都可以处理
- (void)performSelectorTest {
    NSLog(@"设置方法!!!");
    [self performSelector:@selector(testMethod)
               withObject:nil
               afterDelay:3
                  inModes:@[NSRunLoopCommonModes]];
}

#pragma mark - Logic Helper
- (void)testMethod {
    NSLog(@"开始执行!!!");
}

这样无论是正常模式还是有类似拖动tableview等操作,selector都能正常触发并调用.
这里顺便附上swif代码

@objc private func testMethod() -> Void {
        NSLog("开始调用!!!")
    }
    
    @objc private func performTest() -> Void {
        NSLog("设置方法!!!")
        ///< 默认模式
        self.perform(#selector(testMethod), with: nil, afterDelay: 3, inModes: [RunLoopMode.defaultRunLoopMode])
        ///<UITrackingRunLoopMode
        self.perform(#selector(testMethod), with: nil, afterDelay: 3, inModes: [RunLoopMode.UITrackingRunLoopMode])
        ///<common
        self.perform(#selector(testMethod), with: nil, afterDelay: 3, inModes: [RunLoopMode.commonModes])
    }

至此:Thread总结完毕.线程资源共享,线程数据安全等将另辟篇章总结.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352