1、场景一
需要保证线程的长时间存活
在iOS开发过程中,有时间我们不希望某些花费很长时间的操作长时间占用并阻塞主线程,造成界面卡顿线程的存在,我们就需要把此类操作放在子线程来完成。但是当子线程操作完成后,子线程就会销毁。当我们再次需要使用到子线程的时候就必须重新创建子线程,当次操作频繁时就会频繁创建频繁消费线程,造成资源浪费。如何保证线程的长时间存活呢?
①创建NSThread子类
@interface CHThread : NSThread
@end
@implementation CHThread
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
②在viewController中测试
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1.测试线程的销毁
[self threadTest];
}
// 线程启动后执行touch事件使用已经创建好的子线程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];
}
- (void)threadTest
{
CHThread *subThread = [[CHThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
[subThread setName:@"CHThread"];
[subThread start];
self.subThread = subThread;
}
/**
子线程启动后,启动runloop
*/
- (void)subThreadEntryPoint
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//如果注释了下面这一行,子线程中的任务并不能正常执行
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
NSLog(@"启动RunLoop前--%@",runLoop.currentMode);
[runLoop run];
}
}
/**
子线程任务
*/
- (void)subThreadOpetion
{
NSLog(@"启动RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"%@----子线程任务开始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子线程任务结束",[NSThread currentThread]);
}
@end
有几点需要注意:
1.获取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];
2.即使RunLoop开始运行,如果RunLoop 中的 modes 为空,或者要执行的mode里没有item,那么RunLoop会直接在当前loop中返回,并进入睡眠状态。
3.自己创建的Thread中的任务是在kCFRunLoopDefaultMode这个mode中执行的。
4.在子线程创建好后,最好所有的任务都放在AutoreleasePool中。
在AFNetworking库中有此应用场景的经典使用。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
AFNetworking都是通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
2、场景二:
- ①我们经常会在应用中看到tableView 的header 上是一个横向ScrollView,一般我们使用NSTimer,每隔几秒切换一张图片。可是当我们滑动tableView的时候,顶部的scollView并不会切换图片,这可怎么办呢?
- ②.界面上除了有tableView,还有显示倒计时的Label,当我们在滑动tableView时,倒计时就停止了,这又该怎么办呢?
先看看常用写法:
// 第一种写法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二种写法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[timer fire];
上面的两种写法其实是等价的。第二种写法,默认也是将timer添加到NSDefaultRunLoopMode下的。
然后,我们在滑动tableView的时候timerUpdate方法,并不会调用。
原因是啥呢?
原因是当我们滑动scrollView时,主线程的RunLoop 会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer。
要如何解决这一问题呢?
解决方法很简单。
解决方案一:我们只需要在添加timer 时,将mode 设置为NSRunLoopCommonModes即可。
// 第一种写法修改
NSTimer *timer = [NSTimer timerWithInterval:1.0 target: self selector:@selector(timeUpdate) userInfo: nil repeates: YES];
[[NSRunLoop currentRunLoop] addTimer: timer forModel: NSRunLoopCommonModes];
[timer fire];
// 第二种写法,因为是固定添加到defaultMode中,就不要用了
解决方案二:将Timer添加在子线程中,但需要注意当timer添加到当前runloop中后要启动runloop,否则timer只会执行一次。
/首先是创建一个子线程
- (void)createThread
{
NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
[subThread start];
self.subThread = subThread;
}
// 创建timer,并添加到runloop的mode中
- (void)timerTest
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"启动RunLoop前--%@",runLoop.currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
// 第一种写法,改正前
// NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [timer fire];
// 第二种写法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[timer fire];
[[NSRunLoop currentRunLoop] run];
}
// 倒计时更新label
- (void)timerUpdate
{
NSLog(@"当前线程:%@",[NSThread currentThread]);
NSLog(@"启动RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
dispatch_async(dispatch_get_main_queue(), ^{
self.count ++;
NSString *timerText = [NSString stringWithFormat:@"计时器:%ld",self.count];
self.timerLabel.text = timerText;
});
}
总结:
1、如果是在主线程中运行timer,想要timer在某界面有视图滚动时,依然能正常运转,那么将timer添加到RunLoop中时,就需要设置mode 为NSRunLoopCommonModes。
2、如果是在子线程中运行timer,那么将timer添加到RunLoop中后,Mode设置为NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保证RunLoop在运行,且其中有任务。