NSThread的封裝性在多線程中是最差的, 最偏向于底層, 主要基于thread使用
一: 多線程的概念:
線程: 指一個獨立執(zhí)行的代碼路徑(比如: 一個工作的人)
進(jìn)程: 指一個可執(zhí)行程序, 它可以包含多個線程(比如: 一個運行的部門)
當(dāng)一個可執(zhí)行程序中擁有多個獨立執(zhí)行的代碼路徑的時候, 這就叫做多線程
二: 線程的創(chuàng)建:
對于NSThread來說, 每一個對象就代表著一個線程, NSThread提供了2種創(chuàng)建線程的方法:
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
● detach方法: 直接創(chuàng)建并啟動一個線程去執(zhí)行方法, 由于沒有返回值, 如果需要獲取新創(chuàng)建的線程, 需要在執(zhí)行的selector方法中調(diào)用-[NSThread currentThread]獲取
● init方法: 初始化線程, 并返回, 線程的入口函數(shù)由selector傳入, 線程創(chuàng)建出來之后需要手動調(diào)用-start方法啟動
三: 線程的操作:
創(chuàng)建好線程之后需要對線程進(jìn)行操作, NSThread給線程提供的主要操作方法有啟動, 睡眠, 取消, 退出
1: 啟動
我們使用init方法將線程創(chuàng)建出來之后, 線程并不會立即運行, 只有我們手動調(diào)用-start方法之后才會啟動線程:(- detachNewThreadSelector: toTarget: withObject: 是個例外)
- (void)start;
注意: 部分線程屬性需要在啟動前設(shè)置 , 線程啟動之后再設(shè)置會無效. 如qualityOfService屬性
2: 睡眠
NSThread提供了2種讓線程睡眠的方法, 一個是根據(jù)NSDate傳入睡眠時間,一個是直接傳入NSTimeInterval(睡眠時間)
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
看到sleepUntilDate: 大家可能會想起runloop的runUtilDate: 他們都有阻塞線程的效果, 但是阻塞之后的行為又有不一樣的地方, 使用的時候, 我們需要根據(jù)具體需求選擇合適的API
- sleepUtilDate: 相當(dāng)于執(zhí)行一個sleep的任務(wù). 在執(zhí)行過程中, 即使有其他任務(wù)傳入runloop, runloop也不會立即響應(yīng), 必須sleep任務(wù)完成之后, 才會響應(yīng)其他任務(wù)
- runUtilDate: 雖然會阻塞線程, 但是阻塞過程中并不妨礙新任務(wù)的執(zhí)行. 當(dāng)有新任務(wù)的時候, 會先執(zhí)行接收到的新任務(wù), 新任務(wù)執(zhí)行完之后, 如果時間到了, 再繼續(xù)執(zhí)行runUntilDate: 之后的代碼
3: 取消
對于線程的取消, NSThread提供了一個取消的方法和一個屬性
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;
不過大家千萬不要被它的名字迷惑, 調(diào)用-cancel方法并不會立刻取消線程, 它僅僅是將cancelled屬性設(shè)置為YES. cancelled也僅僅是一個用于記錄狀態(tài)的屬性. 線程取消的功能需要我們在main函數(shù)中自己實現(xiàn)
要實現(xiàn)取消的功能, 我們需要自己在線程的main函數(shù)中定期檢查isCancelled狀態(tài)來判斷線程是否需要退出, 當(dāng)isCancelled為YES的時候, 我們手動退出. 如果我們沒有在main函數(shù)中檢查isCancelled狀態(tài), 那么調(diào)用cancel將沒有任何意義.
示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//開啟線程
[thread start];
//睡眠3秒
[NSThread sleepForTimeInterval:3];
//取消線程
[thread cancel];
//判斷是否已經(jīng)取消
if (thread.isCancelled) {
CCLog(@"線程已經(jīng)取消");
}
else {
CCLog(@"線程沒有取消");
}
- (void)demo {
CCLog(@"1-----");
for (int i = 0; i < 5; i++) {
//睡眠1秒
[NSThread sleepForTimeInterval:1];
CCLog(@"第%d個索引",i);
}
//判斷是否已經(jīng)取消
if (NSThread.currentThread.isCancelled) {
return;
}
CCLog(@"2-----");
}
輸出結(jié)果:
1——
第0個索引
第1個索引
線程已經(jīng)取消
第2個索引
第3個索引
第4個索引
可以看到:在取消子線程之后, 子線程仍然在執(zhí)行, 直到判斷子線程已經(jīng)取消后, 我們手動終止線程, 子線程才停止
4: 退出
與充滿不確定性的-cancel相比, +exit函數(shù)可以讓線程立即退出:
+ (void)exit;
+exit屬于核彈級別的終極API, 調(diào)用之后會立即終止線程, 即使任務(wù)還沒有執(zhí)行完成也會中斷, 這就非常有可能導(dǎo)致內(nèi)存泄漏等嚴(yán)重問題, 所以一般不推薦使用.
對于有runloop的線程, 可以使用CFRunLoopStop()結(jié)束runloop配合-cancel結(jié)束線程
示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//開啟線程
[thread start];
- (void)demo {
CCLog(@"1-----");
for (int i = 0; i < 5; i++) {
//睡眠1秒
[NSThread sleepForTimeInterval:1];
if (i == 3) {
[NSThread exit];
}
CCLog(@"第%d個索引",i);
}
CCLog(@"2-----");
}
輸出:
1——
第0個索引
第1個索引
第2個索引
四: 線程通訊
線程準(zhǔn)備好之后, 經(jīng)常需要從主線程把耗時的任務(wù)丟給輔助線程, 當(dāng)任務(wù)完成之后輔助線程再把結(jié)果傳回主線程, 這些線程通訊一般用的都是perform方法:
①
- (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;
③
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AV;
④
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
①: 將selector丟給主線程執(zhí)行, 可以指定runloop mode
②: 將selector丟給主線程執(zhí)行, runloop mode默認(rèn)為common mode
③: 將selector丟給指定線程執(zhí)行, 可以指定runloop mode
④: 將selector丟給指定線程執(zhí)行, runloop mode默認(rèn)為default mode
所以我們一般用③④方法將任務(wù)丟給輔助線程, 任務(wù)執(zhí)行完成之后再使用①②方法將結(jié)果傳回主線程;
注意: perform方法只對擁有runloop的線程有效, 如果創(chuàng)建的線程沒有添加runloop, perform的selector將無法執(zhí)行;
五: 線程的優(yōu)先級:
每個線程的緊急程度是不一樣的, 有的線程中的任務(wù)你也許希望盡快執(zhí)行, 有的線程中的任務(wù)也不并不是那么緊急, 這時就需要設(shè)置優(yōu)先級, NSThread有4個優(yōu)先級的API
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
@property double threadPriority;
@property NSQualityOfService qualityOfService;
前兩個是類方法, 用于設(shè)置和獲取當(dāng)前線程的優(yōu)先級
threadPriority屬性可以通過對象來設(shè)置和獲取優(yōu)先級
由于線程優(yōu)先級是一個比較抽象的東西, 沒人知道0.5和0.6到底有多大區(qū)別, 所以iOS8之后新增了qualityOfService枚舉屬性, 大家可以通過枚舉值設(shè)置優(yōu)先級
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
};
NSQualityOfService主要有5個枚舉值, 優(yōu)先級別從高到底排布:
- NSQualityOfServiceUserInteractive: 最高優(yōu)先級, 主要用于直接參與提供交互式UI的工作, 例如: 處理點擊事件或繪制圖像到屏幕上
- NSQualityOfServiceUserInitiated: 次高優(yōu)先級, 用于執(zhí)行用戶明確請求的工作, 并且必須立即顯示結(jié)果, 以便進(jìn)行進(jìn)一步的用戶交互
- NSQualityOfServiceDefault: 默認(rèn)優(yōu)先級, 當(dāng)沒有設(shè)置優(yōu)先級的時候, 線程默認(rèn)優(yōu)先級
- NSQualityOfServiceUtility: 普通優(yōu)先級,主要用于不需要立即返回的任務(wù)
- NSQualityOfServiceBackground: 后臺優(yōu)先級, 用于完全不緊急的任務(wù)
一般主線程和沒有設(shè)置優(yōu)先級的線程都是默認(rèn)優(yōu)先級;
六: 主線程和當(dāng)前線程:
NSThread也提供了非常方便的獲取和判斷主線程的API:
@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread;
@property (class, readonly, strong) NSThread *mainThread;
- isMainThread: 判斷當(dāng)前線程是否是主線程;
- mainThread: 獲取主線程的thread
除了獲取主線程, 我們也可以使用屬性currentThread來獲取當(dāng)前線程
@property (class, readonly, strong) NSThread *currentThread;
七: 線程通知:
NSThread有三個線程相關(guān)的通知:
FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;
- NSWillBecomeMultiThreadedNotification: 由當(dāng)前線程派生出第一個其他線程時發(fā)送, 一般一個線程只發(fā)送一次
- NSDidBecomeSingleThreadedNotification: 這個通知目前沒有實際意義, 可以忽略
- NSThreadWillExitNotification: 線程退出之前發(fā)送這個通知
八: NSThread實例:
只看API畢竟比較抽象, 下面我用一個例子給大家展示NSThread的使用方法:
1: 線程的創(chuàng)建:
我們首先來創(chuàng)建一個線程, 并用self.thread持有, 以便后面操作線程和線程通訊使用:
//①創(chuàng)建線程
self.thred = [NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
//②設(shè)置線程的優(yōu)先級
self.thread.qualityOfService = NSQualityOfServiceDefault;
//③啟動線程
[self.thread start];
①: 創(chuàng)建線程: 并指定入口main函數(shù)為-threadMain
②: 設(shè)置線程的優(yōu)先級: qualityOfService屬性必須在線程啟動之前設(shè)置, 啟動之后將無法再設(shè)置
③: 調(diào)用start方法啟動線程
由于線程的創(chuàng)建和銷毀非常消耗性能, 大多數(shù)情況下, 我們需要復(fù)用一個長期運行的線程來執(zhí)行任務(wù).
在線程啟動之后會首先執(zhí)行- threadMain, 正常情況下threadMain方法執(zhí)行結(jié)束之后, 線程就會退出, 為了線程可以長期復(fù)用接收消息, 我們需要在threadMain中給thread添加runloop
-(void)threadMain {
[[NSThread currentThread] setName:@“myThread”]; //①給線程設(shè)置名字
NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; //②給線程添加runloop
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; //③給runloop添加數(shù)據(jù)源
While(![[NSThread currentThread] isCancelled]) { //④檢查isCancelled
[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10];//⑤啟動runloop
}
}
①: 設(shè)置線程的名字, 這一步不是必須的, 主要是為了debug的時候更方便, 可以直接看出這是哪個線程
②: 自定義的線程默認(rèn)是沒有runloop的, 調(diào)用- currentRunLoop, 方法內(nèi)部會為線程創(chuàng)建runloop
③: 如果沒有數(shù)據(jù)源, runloop會在啟動之后立刻退出, 所以需要給runloop添加一個數(shù)據(jù)源, 這里添加的是NSPort數(shù)據(jù)源
④: 定期檢查isCancelled, 當(dāng)外部調(diào)用-cabncel方法將isCancelled設(shè)置為YES的時候, 線程可以退出;
⑤: 啟動runloop
2: 線程通訊:
線程創(chuàng)建好了之后我們就可以給線程丟任務(wù)了, 當(dāng)我們有一個需要比較耗時的任務(wù)的時候, 我們可以調(diào)用perform方法將task丟給這個線程:
[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO];
3: 結(jié)束線程:
當(dāng)我們想要結(jié)束線程的時候, 我們可以使用CFRunLoopStop()配合-cancel來結(jié)束線程
-(void)cancelThread {
[[NSThread currentThread] cancel];
CFRunLoopStop(CFRunLoopGetCurrent());
}
不過這個方法必須在self.thread線程下調(diào)用, 如果當(dāng)前是主線程, 可以perform到self.thread下調(diào)用這個方法結(jié)束線程;
原文:https://www.open-open.com/lib/view/open1452993005808.html#articleHeader14