
1、場(chǎng)景一
需要保證線程的長(zhǎng)時(shí)間存活
在iOS開發(fā)過程中,有時(shí)間我們不希望某些花費(fèi)很長(zhǎng)時(shí)間的操作長(zhǎng)時(shí)間占用并阻塞主線程,造成界面卡頓線程的存在,我們就需要把此類操作放在子線程來完成。但是當(dāng)子線程操作完成后,子線程就會(huì)銷毀。當(dāng)我們?cè)俅涡枰褂玫阶泳€程的時(shí)候就必須重新創(chuàng)建子線程,當(dāng)次操作頻繁時(shí)就會(huì)頻繁創(chuàng)建頻繁消費(fèi)線程,造成資源浪費(fèi)。如何保證線程的長(zhǎng)時(shí)間存活呢?
①創(chuàng)建NSThread子類
@interface CHThread : NSThread
@end
@implementation CHThread
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
②在viewController中測(cè)試
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1.測(cè)試線程的銷毀
[self threadTest];
}
// 線程啟動(dòng)后執(zhí)行touch事件使用已經(jīng)創(chuàng)建好的子線程
- (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;
}
/**
子線程啟動(dòng)后,啟動(dòng)runloop
*/
- (void)subThreadEntryPoint
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//如果注釋了下面這一行,子線程中的任務(wù)并不能正常執(zhí)行
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
[runLoop run];
}
}
/**
子線程任務(wù)
*/
- (void)subThreadOpetion
{
NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
}
@end
有幾點(diǎn)需要注意:
1.獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];
2.即使RunLoop開始運(yùn)行,如果RunLoop 中的 modes 為空,或者要執(zhí)行的mode里沒有item,那么RunLoop會(huì)直接在當(dāng)前l(fā)oop中返回,并進(jìn)入睡眠狀態(tài)。
3.自己創(chuàng)建的Thread中的任務(wù)是在kCFRunLoopDefaultMode這個(gè)mode中執(zhí)行的。
4.在子線程創(chuàng)建好后,最好所有的任務(wù)都放在AutoreleasePool中。
在AFNetworking庫中有此應(yīng)用場(chǎng)景的經(jīng)典使用。
+ (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都是通過調(diào)用 [NSObject performSelector:onThread:..] 將這個(gè)任務(wù)扔到了后臺(tái)線程的 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、場(chǎng)景二:
- ①我們經(jīng)常會(huì)在應(yīng)用中看到tableView 的header 上是一個(gè)橫向ScrollView,一般我們使用NSTimer,每隔幾秒切換一張圖片??墒钱?dāng)我們滑動(dòng)tableView的時(shí)候,頂部的scollView并不會(huì)切換圖片,這可怎么辦呢?
- ②.界面上除了有tableView,還有顯示倒計(jì)時(shí)的Label,當(dāng)我們?cè)诨瑒?dòng)tableView時(shí),倒計(jì)時(shí)就停止了,這又該怎么辦呢?
先看看常用寫法:
// 第一種寫法
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];
上面的兩種寫法其實(shí)是等價(jià)的。第二種寫法,默認(rèn)也是將timer添加到NSDefaultRunLoopMode下的。
然后,我們?cè)诨瑒?dòng)tableView的時(shí)候timerUpdate方法,并不會(huì)調(diào)用。
原因是啥呢?
原因是當(dāng)我們滑動(dòng)scrollView時(shí),主線程的RunLoop 會(huì)切換到UITrackingRunLoopMode這個(gè)Mode,執(zhí)行的也是UITrackingRunLoopMode下的任務(wù)(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的,所以timer任務(wù)并不會(huì)執(zhí)行,只有當(dāng)UITrackingRunLoopMode的任務(wù)執(zhí)行完畢,runloop切換到NSDefaultRunLoopMode后,才會(huì)繼續(xù)執(zhí)行timer。
要如何解決這一問題呢?
解決方法很簡(jiǎn)單。
解決方案一:我們只需要在添加timer 時(shí),將mode 設(shè)置為NSRunLoopCommonModes即可。
// 第一種寫法修改
NSTimer *timer = [NSTimer timerWithInterval:1.0 target: self selector:@selector(timeUpdate) userInfo: nil repeates: YES];
[[NSRunLoop currentRunLoop] addTimer: timer forModel: NSRunLoopCommonModes];
[timer fire];
// 第二種寫法,因?yàn)槭枪潭ㄌ砑拥絛efaultMode中,就不要用了
解決方案二:將Timer添加在子線程中,但需要注意當(dāng)timer添加到當(dāng)前runloop中后要啟動(dòng)runloop,否則timer只會(huì)執(zhí)行一次。
/首先是創(chuàng)建一個(gè)子線程
- (void)createThread
{
NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
[subThread start];
self.subThread = subThread;
}
// 創(chuàng)建timer,并添加到runloop的mode中
- (void)timerTest
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"啟動(dòng)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];
}
// 倒計(jì)時(shí)更新label
- (void)timerUpdate
{
NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
dispatch_async(dispatch_get_main_queue(), ^{
self.count ++;
NSString *timerText = [NSString stringWithFormat:@"計(jì)時(shí)器:%ld",self.count];
self.timerLabel.text = timerText;
});
}
總結(jié):
1、如果是在主線程中運(yùn)行timer,想要timer在某界面有視圖滾動(dòng)時(shí),依然能正常運(yùn)轉(zhuǎn),那么將timer添加到RunLoop中時(shí),就需要設(shè)置mode 為NSRunLoopCommonModes。
2、如果是在子線程中運(yùn)行timer,那么將timer添加到RunLoop中后,Mode設(shè)置為NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保證RunLoop在運(yùn)行,且其中有任務(wù)。