更新UI放在主线程的原因
前言(1-9)
1、在子线程中是不能进行UI 更新的,而可以立刻更新的原因是:子线程代码执行完毕了,又自动进入到了主线程,这中间的时间非常的短,让我们误以为子线程可以更新UI。如果子线程一直在运行,则无法更新UI,因为无法进入到主线程.
2、程序一开始运行就进入了主线程
3、处理某些数据太过费时,影响用户交互,可以开辟子线程处理,处理完之后,然后通知主线程进行界面更新。
4.iOS中只有主线程 才能立即刷新UI。主线程中用于显示\刷新UI界面,处理UI事件(比如点击事件、滚动事件、拖拽事件等).耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验,所以解决办法是:异步开启一条子线程,让耗时操作在子线程中完成,这样又不会影响主线程的任务。当子线程中的任务完成之后,回到主线程刷新UI,显示UI即可。类比,1个人同时做两件事和2个人分别做一件事哪种效率高呢。
5.如果是通过侦听异步消息,触发回调函数,或者调用异步方法,请求刷新UI,都会产生线程阻塞和延迟的问题。
6.可以通过下面的方式解决线程阻塞:
if ([NSThread isMainThread]){// 主线程就执行{}
[self.downloadMapBtn setImage:[UIImage imageNamed:@"download_map.png"] forState:UIControlStateNormal];
[self.downloadMapBtn setNeedsDisplay];
}
else{// 不是主线程,就调用gcd的函数+主队列,实现回到主线程刷新UI
dispatch_sync(dispatch_get_main_queue(), ^{
//Update UI in UI thread here
[self.downloadMapBtn setImage:[UIImage imageNamed:@"download_map.png"] forState:UIControlStateNormal];
[self.downloadMapBtn setNeedsDisplay];
});
}
这样的方法来进行 消息派送给主线程,进行刷新。
真实案例解决方法链接
拓展:下面的截图我们可以总结:在block里面操作控制器的对象时,要将控制器的对象变为弱指针,否则当Pop当前的控制器时,当前控制器不会被销毁。
-
在子线程刷新UI可以看到在控制台中打印一些乱七八糟的东西
在子线程刷新UI.gif -
在主线程刷新UI可以看到控制台中不会出现这些乱七八糟的东西
在主线程刷新UI.gif
拓展:block里面使用外界的对象时,要将对象变为弱指针,否则该对象不会被释放
-
对象未变为弱指针
对象未变为弱指针.gif -
对象变为了弱指针
对象变为了弱指针.gif
7.所谓线程间通信,即如何从一个线程进入到另一个线程继续执行任务或者是传递参数(如从子线程回到主线程)
8.系统的网络请求框架中的dataTaskWithRequest:completionHandler,代码块中的内容是子线程,而AFN网络请求中的POST:parameters:progress:代码块中的内容是主线程,因为AFN的底层自动进行将子线程切换到主线程的操作.所以我们直接就可以在AFN回调的代码块直接执行self.tableView reloadData操作,不需要用到GCD的异步函数+主队列
9.GCD中的函数+队列的组合使用
01 异步函数+并发队列:开启多条线程,并发(无顺序)执行任务
02 异步函数+串行队列:开启一条线程,串行执行任务
03 同步函数+并发队列:不开线程,串行执行任务
04 同步函数+串行队列:不开线程,串行执行任务
05 异步函数+主队列:不开线程,在主线程中串行执行任务
06 同步函数+主队列:不开线程,串行执行任务(注意死锁发生)
对06的拓展(精华):同步函数在子线程的方法中肯定不会发生死锁 同步函数在主线程的方法中肯定会发生死锁
07 注意同步函数和异步函数在执行顺序上面的差异
巧记:异步函数+不是主队列,一定会开线程
10.阻塞线程: 所谓的阻塞,就是线程能够运行,但是某个条件阻止它的运行,当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间,直到线程重新进入就绪状态,它才有可能执行操作。就绪并代表是在运行啊,所谓的就绪,就是可运行也可不运行,只要调度器分配时间片给线程,线程就可以运行,因为我们都知道,调度器是如何分配线程,是不确定的。为什么任务会进入阻塞的状态,一般有以下几个原因:
1.通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行;
2.通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息线程才会进入就绪状态;
3.任务在等到某个输入或输出完成;
4.任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取这个锁;
11.线程阻塞和死锁区别:
线程阻塞:A等待B操作完成,在等待的过程中,A被阻塞,一直到B完成了,才消除A的阻塞
死锁:线程A等待线程B,线程B等待线程A,互相等待,就会陷入死锁
精华代码
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *btn;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// [NSThread detachNewThreadSelector:@selector(test1) toTarget:self withObject:nil];
// [NSThread detachNewThreadSelector:@selector(test2) toTarget:self withObject:nil];
[NSThread detachNewThreadSelector:@selector(test3) toTarget:self withObject:nil];
}
// 立刻更新UI.test1中的内容在子线程,瞬间能更新UI的原因:子线程的在这个方法的存活时间非常短,例如0.01秒,在0.01秒之后,子线程就会死亡,自动切换到主线程。所以实际上是在主线程更新UI
-(void)test1 {
NSLog(@"当前线程 %@",[NSThread currentThread]);
[self.btn setTitle:@"AAA" forState:0];
}
// 4秒后更新UI。即子线程结束才会更新UI,子线程存活的这4秒的时间按钮的标题不会发生变化。子线程生命结束自动会回到主线程,在主线程中刷新UI。
// 让子线程的生命延长4秒的目的就是验证到底是在哪个线程更新UI的。因为不延长4秒的话,子线程切换到主线程时间非常快,我们无法判断是在主线程还是在子线程更新UI的
-(void)test2{
NSLog(@"当前线程 %@",[NSThread currentThread]);
[self.btn setTitle:@"BBB" forState:0];
// 阻塞当前的子线程,阻塞4秒。目的:让子线程的生命延长4秒
[NSThread sleepForTimeInterval:4.0];
}
// 立刻更新UI。test3中的内容是子线程,并且让子线程的生命延长4秒,为什么会立刻更新UI的原因?答:显示UI的代码包装在了主线程的代码块中,只要是在主线程写上了UI代码,那么UI会被立刻刷新
-(void)test3{
NSLog(@"当前线程 %@",[NSThread currentThread]);
// GCD的异步函数+主队列:不开线程,在主线程中串行执行任务
dispatch_async(dispatch_get_main_queue(), ^{// 让当前的子线程回到主线程
NSLog(@"当前线程 %@",[NSThread currentThread]);
[self.btn setTitle:@"CCC" forState:0];
});
// 阻塞当前的子线程,阻塞4秒。目的:让子线程的生命延长4秒
[NSThread sleepForTimeInterval:4.0];
}
@end
精华代码总结
- 在子线程中,如果要对UI进行更新,只有两种做法:
- 做法1:等到该子线程运行结束,子线程结束之后系统自动回到主线程刷新UI
- 做法2:不用等到该子线程运行结束,我们直接在UI代码的外面写上GCD的异步函数+主队列,就可以实现立即主线程更新UI
运行结果:
test1:普通情况,立刻刷新UI,因为代码执行速度非常快,子线程死亡系统底层跳转至主线程刷新UI
101.114.giftest2:模拟子线程不死(虽然只有4秒),不能实现立即刷新UI,只能4秒之后才能刷新
101.115.giftest3:模拟子线程不死(4秒)+UI嵌套主线程,可以实现立即刷新UI
101.116.gif