NSThread是官方提供的一套面向?qū)ο蟮妮p量級(jí)多線程開發(fā)技術(shù)。使用較為簡單,不需要過多地操作線程的行為配置,但是仍然需要開發(fā)者自己處理線程的生命周期。相比于C語言中的pthread相關(guān)接口,NSThread易用性更強(qiáng)。
一、NSThread開啟新線程的方式
1、構(gòu)造器方式
NSThread中提供了如下兩個(gè)類方法:
+ (void)detachNewThreadWithBlock:(void (^)(void))block)
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument
兩個(gè)方法作用類似,都是自動(dòng)開啟新線程,執(zhí)行任務(wù)。
2、初始化方式
通過手動(dòng)調(diào)用初始化方法,可以獲取到線程對象,從而更方便地對線程進(jìn)行配置以及獲取線程信息。示例如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"%@", [NSThread currentThread]);
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"1_%@", [NSThread currentThread]);
}];
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[thread2 start];
}
- (void)test {
NSLog(@"2_%@", [NSThread currentThread]);
}
運(yùn)行代碼,控制臺(tái)輸出如下:
2021-12-23 14:12:11.726978+0800 MyProject[38641:550626] <_NSMainThread: 0x6000039c8080>{number = 1, name = main}
2021-12-23 14:12:11.727314+0800 MyProject[38641:550803] 1_<NSThread: 0x600003983500>{number = 7, name = (null)}
2021-12-23 14:12:11.727395+0800 MyProject[38641:550804] 2_<NSThread: 0x600003983340>{number = 8, name = (null)}
需要注意的是,初始化方式創(chuàng)建線程,需要調(diào)用start方法來啟動(dòng)線程任務(wù)。
3、自定義線程
通過繼承NSThread可以創(chuàng)建自定義的線程,自定義線程通過內(nèi)部main函數(shù)設(shè)定要執(zhí)行的任務(wù)。示例如下:
#import "MyThread.h"
@implementation MyThread
- (void)main {
NSLog(@"自定義線程:%@", [NSThread currentThread]);
}
@end
自定義線程的使用方法如下:
MyThread *thread = [[MyThread alloc] init];
[thread start];
4、performSelector
只要是NSObject的子類或?qū)嵗伎梢酝ㄟ^調(diào)用方法進(jìn)入子線程和主線程,其實(shí)這些方法開辟的子線程,也是NSThread的一種體現(xiàn)方式。常用方法如下:
// 當(dāng)前線程,延時(shí)1s執(zhí)行
[self performSelector:@selector(text) withObject:nil afterDelay:1];
該方法也響應(yīng)了Objective-C的動(dòng)態(tài)性:延時(shí)到運(yùn)行時(shí)才綁定方法。需要注意的是,帶afterDelay的延時(shí)函數(shù),會(huì)在內(nèi)部創(chuàng)建一個(gè)NSTimer,然后添加到當(dāng)前線程的runloop中。如果當(dāng)前線程沒有開啟Runloop,則方法會(huì)失效,例如:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test) withObject:nil afterDelay:0];
});
這里test方法是不會(huì)執(zhí)行的,因?yàn)?br>
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
這個(gè)方法要?jiǎng)?chuàng)建提交任務(wù)到runloop上,而GCD底層創(chuàng)建的線程是默認(rèn)不開啟對應(yīng)的runloop的,所以test不會(huì)執(zhí)行。
如果將dispatch_get_global_queue改為主隊(duì)列,由于主隊(duì)列所在的主線程是默認(rèn)開啟runloop的,則回去執(zhí)行test。如果將dispatch_async改為dispatch_sync,同步在當(dāng)前線程執(zhí)行,如果當(dāng)前線程是主線程,則test可以執(zhí)行。
// 回到主線程
// waitUntilDone YES - 立刻執(zhí)行 NO - 等待當(dāng)前Runloop空閑后執(zhí)行
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
// 開辟子線程
[self performSelectorInBackground:@selector(test) withObject:nil];
// 在指定的線程中執(zhí)行任務(wù)
// 任務(wù)執(zhí)行依賴Runloop,所以線程Runloop必須開啟
[self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
二、相關(guān)屬性與方法
1、獲取線程信息的類方法、屬性
// 獲取當(dāng)前線程N(yùn)SThread對象
@property (class, readonly, strong) NSThread *currentThread;
// 獲取主線程N(yùn)SThread對象
@property (class, readonly, strong) NSThread *mainThread;
// 當(dāng)前應(yīng)用程序是否支持多線程
+ (BOOL)isMultiThreaded;
// 當(dāng)前是否為主線程
@property (class, readonly) BOOL isMainThread;
// 獲取當(dāng)前線程的優(yōu)先級(jí)
+ (double)threadPriority;
// 當(dāng)前線程執(zhí)行代碼的堆棧地址
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses;
// 當(dāng)前線程執(zhí)行代碼的堆棧信息
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;
2、控制線程行為的類方法
// 使當(dāng)前線程休眠到指定時(shí)間
+ (void)sleepUntilDate:(NSDate *)date;
// 使當(dāng)前線程休眠一定時(shí)間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 結(jié)束當(dāng)前線程
+ (void)exit;
// 設(shè)置當(dāng)前線程的優(yōu)先級(jí)
+ (BOOL)setThreadPriority:(double)p;
3、其他常用實(shí)例屬性、方法
// 線程名稱
@property (nullable, copy) NSString *name;
// 堆棧大小
@property NSUInteger stackSize;
// 是否為主線程
@property (readonly) BOOL isMainThread;
// 是否正在執(zhí)行
@property (readonly, getter=isExecuting) BOOL executing;
// 是否執(zhí)行完成
@property (readonly, getter=isFinished) BOOL finished;
// 是否已經(jīng)取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消線程(線程不會(huì)立即停止執(zhí)行,需要開發(fā)者根據(jù)cancelled屬性做邏輯處理)
- (void)cancel;
// 啟動(dòng)線程
- (void)start;
// 線程主體,自定義的NSThread子類通過重寫該方法來指定要執(zhí)行的任務(wù)
- (void)main;
三、相關(guān)通知
系統(tǒng)定義了幾個(gè)線程相關(guān)的通知,我們可以通過監(jiān)聽來關(guān)注多線程的運(yùn)行狀態(tài),具體名稱以及發(fā)送時(shí)機(jī)如下:
// 將進(jìn)入多線程運(yùn)行模式
FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
// 已經(jīng)進(jìn)入多線程運(yùn)行模式
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
// 某個(gè)線層結(jié)束
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;
四、NSThread+Runloop實(shí)現(xiàn)常駐線程
NSThread在實(shí)際開發(fā)中,比較常用到的場景就是實(shí)現(xiàn)常駐線程。
由于每次開辟子線程都會(huì)消耗CPU,所以頻繁開啟子線程會(huì)消耗大量CPU,而且創(chuàng)建的子線程都是任務(wù)執(zhí)行后,就會(huì)被釋放,不能再次利用。
常駐線程,就是一個(gè)使用完以后,不會(huì)釋放,可以再次利用的線程。
使用NSThead+Runloop實(shí)現(xiàn)常駐線程,示例代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(test) onThread:[self shareThread] withObject:nil waitUntilDone:NO];
}
- (void)test {
NSLog(@"test:%@", [NSThread currentThread]);
}
// 創(chuàng)建一個(gè)NSThread單例
- (NSThread *)shareThread {
static NSThread *shareThread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
[shareThread setName:@"MyThread"];
[shareThread start];
});
return shareThread;
}
- (void)threadTest {
@autoreleasepool {
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
}