iOS开发之多线程笔记(三)之NSThread
NSThread
官方文档上的介绍翻译过来的大致意思就是:
- 概述
- 当你有一个方法需要在它自己的线程中运行的时候可以使用该类.
- 当你需要执行一个耗时冗长的任务而又不希望它阻塞应用程序的其他线程时,threads就显得额外的有效.特别是你能用它避免阻塞了用来处理用户交互的主线程.
3.线程还可以将一个大型的任务瓜分成多个小任务,这样可以充分利用和突出多核计算机的性能.
- NSThread类似NSOperation,能支持线程状态监测.
- 你可以取消或者继续一个正在执行的线程.
- 如何用代码来请求取消一个线程,有关更多信息,请参见“取消”的说明。
- 子类化
你可以继承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方法才能开始执行.
相关方法
- 获取当前代码块在哪个线程中执行
//oc
NSThread *currentThread = [NSThread currentThread];
//swift
let currentThread = Thread.current
- 获取主线程
//oc
NSThread *mainThread = [NSThread mainThread];
//swift
let mainThread = Thread.main
- 线程操作
/*
退出线程
*/
//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:效果一样,只是传入时间类型不同.
- 线程状态
/*
oc
//是否是主线程
[thread1 isMainThread];
//线程是否取消执行
[thread1 isCancelled];
//线程是否完成
[thread1 isFinished];
//线程是否正在调用中,执行中
[thread1 isExecuting];
*/
/*
swift
thread1.isCancelled;
thread1.isMainThread;
thread1.isFinished;
thread1.isExecuting;
*/
这里说下:
- 调用exit方法后,此时在外部访问isFinished, isFinished = YES.
- 调动cancel方法后,此时在外部访问isFinished, isFinished = YES. isCancelled = YES
cancel和exit区别
- cancel 这个方法会将正在执行的当前进程信息保存给接收者,然后再将进程取消,同时会通过方法isCancled反馈状态,如果成功取消,isCancled将会返回YES,否则返回NO;进程被取消后,会调用exit方法;
- 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,2个方法,是指在主线程中执行一段任务.
- 第3,4个方法,是指在指定的NSThread中执行一段任务.
- 第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
大致意思:
- models是个存储NSString类型的数组.
- models中的元素用来识别selector触发的时机.
- models至少包含一个元素,如果为空,则不会有任何作用.
- 从最后一句可以看出models里面的元素和Run Loops知识相关
因为Run Loop是个大模块,这里不过多赘述,这里models数组里面存储的其实就是RunLoopModel
typedef NSString * NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;
如上,它其实就是个string类型.
平时我们使用最多的是Default模式, Event tracking模式,Common模式.这里介绍这三种.
- 先看示例代码,点击按钮触发方法performSelectorTest,延时3秒后执行testMethod
- (void)performSelectorTest {
NSLog(@"设置方法!!!");
[self performSelector:@selector(testMethod)
withObject:nil
afterDelay:3
inModes:@[NSDefaultRunLoopMode]];
}
- (void)testMethod {
NSLog(@"开始执行!!!");
}
- 控制台打印如下:
2018-01-18 22:07:57.157753+0800 Thread[34060:209224] 设置方法!!!
2018-01-18 22:08:00.159394+0800 Thread[34060:209224] 开始执行!!!
- 时间相差3秒,一切正常.
-
ok,问题来了,此时我在控制器上拖一个tableView,看图:
1.gif - 控制台日志:
2018-01-18 22:12:45.305354+0800 Thread[34060:209224] 设置方法!!!
2018-01-18 22:12:52.947010+0800 Thread[34060:209224] 开始执行!!!
- 两次打印的时间间隔是7秒,并不是上面代码写的3秒.
- 当点击了按钮后我开始一直滑动tableView,时间超过3秒后下一条日志并没有打印出来,也就是说performSelector里面的方法并没有按照预期的开始调用.直到我停止了滑动才开始调用,第二条日志才打印出来.
- 而这就是RunLoopModel的神奇之处.
- 原因是在default mode下,当用户在拖动UITableView处于UITrackingRunLoopMode模式时,默认模式下的数据便无法处理。
总结
默认模式下一般情况下可以正常达到预期效果,但是当有类似滑动屏幕如按住UITableView拖动时等操作,该模式下的selector便无法做出响应
- 上面说的tableview拖动操作就是出于该种模式下
- 代码同上,这里传入UITrackingRunLoopMode
- (void)performSelectorTest {
NSLog(@"设置方法!!!");
[self performSelector:@selector(testMethod)
withObject:nil
afterDelay:3
inModes:@[UITrackingRunLoopMode]];
}
#pragma mark - Logic Helper
- (void)testMethod {
NSLog(@"开始执行!!!");
}
- 当点击完按钮后,我立刻去不停的拖动tableview,发现控制台打印结果和预期的就一样了.效果如图 2.gif
-
看似问题解决了,ok那么问题又来了.当我点击完按钮不去滑动tableview,而是静等3秒,发现第二条日志并没有打印出来.现象如图:
3.gif - 如图,我点击了按钮后,等待了远不止3秒,第二条日志迟迟不来.
- 其实它永远都不会被触发了,只有当我再次滑动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总结完毕.线程资源共享,线程数据安全等将另辟篇章总结.