1. 進程的概念
進程是指在系統中正在進行的一個應用程序;每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內。
比如,同時打開QQ和Xcode,系統就會分別啟動2個進程。
通過“活動監視器”可以查看Mac系統中所開的進程
2. 線程的概念
一個進程要想執行任務,必須得有線程(每一個進程至少要有一條線程)。線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行。
比如使用酷狗播放音樂、使用迅雷下載電影,都需要在線程中執行。
3. 線程的串行
1個線程中任務的執行是串行的,如果要在1個線程中執行多個任務,那么只能一個一個按照順序執行這些任務,即,在同一時間內,1個線程只能執行1個任務。
因此也可以認為 線程是進程中的一條執行任務。
4. 多線程
1個進程中可以開啟多條線程,每條線程可以並行(同時)執行不同的任務。多線程技術可以提高程序的執行效率。CPU只能處理1條線程,只有一條線程在工作。
5.多線程的原理
同一時間內,CPU只能處理1條線程,只有1條線程在工作(執行);多線程並發(同時)執行,其實是CPU快速地在多條線程之間調度(切換)。如果CPU調度線程的時間足夠快,就造成了多線程並發執行的假象。
思考:如果線程非常非常多,會發生什么情況?
CPU會在N多線程之間調度,CPU會類似,消耗大量的CPU資源;每條線程被調度執行的頻次會較低(線程的執行效率減低)。一般開到3-5條線程。
6. 多線程的優缺點
優點:能適當提高程序的執行效率;能適當提高資源利用率(CPU、內存利用率)。
缺點:開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能;線程越多,CPU在調度線程上的開銷就越大;程序設計更加復雜,例如線程之間的通信、多線程的數據共享。
創建線程大約需要90毫秒。
7. 多線程在iOS開發中的應用
7.1什么是主線程 1個iOS程序運行后,會默認開啟1條線程,稱為“主線程”或者“UI線程”。 7.2主線程的主要作用 顯示/刷新UI界面,處理UI時間(比如點擊時間、滾動時間、拖拽事件等) 7.3主線程的使用注意 別將比較消耗的操作放到主線程中; 耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗。例如,在一個界面上,點擊一個按鈕,需要耗時5秒;按下按鈕的后,立刻拖拽表格,那么,表格拖拽的反應會在5秒后才響應。
7.4 子線程
將耗時操作放在子線程中
8.NSThread的使用
- 8.0 獲取線程(幾乎所有的人都在主線程中執行)
//獲取主線程 NSThread *mianThread = [NSTread mainThread]; //獲取當前線程 NSTread *currentTread = [NSTread currentThread];
//判斷是否是主線程
判斷number是否等於1; 調用方法 isMainThread
- 8.1 創建和啟動線程
一個NSThread對象就代表一條線程。
創建和啟動線程的方法:
第一種方法:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start]; //必須要調用這個方法,否者就不會啟動
第二種方法:開啟后台線程 隱式創建
[self performSelectorInBackground:@select(run) withObject:nil];
第三種方法:直接分離出一條子線程
[self detaNewThreaeSelector:@select(run) toTarget:self withObject:nil];
對比:
第一種:代碼量比較大,但是能拿到線程對象
第二種和第三種:無法拿到線程對象進行詳細設置
線程一啟動,就會告訴CPU准備就緒,可以隨時接受CPU調度,CPU調度當前線程后,就會在線程thread中執行self的run方法。
當線程完成任務后,就是被銷毀。
- 8.2 主線程相關用法
+ (NSThread *)mainThread; // 獲得主線程 - (BOOL)isMainThread; // 是否為主線程 + (BOOL)isMainThread; // 是否為主線程
線程的調度優先級:
+ (double)threadPriority; + (BOOL)setThreadPriority:(double)p; - (double)threadPriority; - (BOOL)setThreadPriority:(double)p; 調度優先級的取值范圍是0.0 ~ 1.0,默認0.5,值越大,優先級越高
線程的名字:
- (void)setName:(NSString *)n; - (NSString *)name;
- 8.3 線程的狀態
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start];
- 8.4 控制線程狀態
啟動線程: - (void)start; (進入就緒狀態 -> 運行狀態。當線程任務執行完畢,自動進入死亡狀態。) 阻塞(暫停)線程: + (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti; 強制停止線程: + (void)exit; (進入死亡狀態)
- 8.5 多線程的安全隱患
(1)資源共享: 1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源。比如,多個線程訪問同一個對象、同一個變量和同一個文件 (2)當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。比如,下圖的存錢和取錢的問題以及買票和取票的問題。
- 8.6 安全隱患解決-互斥鎖
互斥鎖使用格式: @synchronized(鎖對象) { // 需要鎖定的代碼 } 注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的。
鎖對象,可以用self代替
要注意加鎖的位置,在多個線程搶奪資源的時候才需要加鎖。
互斥鎖的優缺點: 優點:能有效防止因多線程搶奪資源造成的數據安全問題; 缺點:需要消耗大量的CPU資源。 互斥鎖的使用前提:多條線程搶奪同一塊資源。 相關專業術語:線程同步 線程同步的意思:多條線程在同一條線上執行(按順序地執行任務),互斥鎖,就是使用了線程同步技術
測試代碼如下:
#pragma mark - 賣票 - (void)addThread { self.count = 100; NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil]; NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil]; NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil]; threadA.name = @"第一個"; threadB.name = @"第二個"; threadC.name = @"第三個"; [threadA start]; [threadB start]; [threadC start]; } - (void)selectTick { while (1) { if (self.count > 0) { self.count --; NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count); }else { NSLog(@"%@賣完了",[NSThread currentThread].name); break; } } }
當我們調用上面那一段代碼的時候,是沒有出現賣票上票數不對的問題的; 因為代碼中並沒有做耗時的操作,每個方法調用一次就結束了,所以不會出現資源搶奪的情況; 如果在selctTick的方法中,添加一個耗時的操作,那么問題就會暴露,將selectTick的方法修改成如下: - (void)selectTick { while (1) { if (self.count > 0) { self.count --; for (NSInteger i = 0; i < 100000; i++) { //這是一個耗時的操作 } NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count); }else { NSLog(@"%@賣完了",[NSThread currentThread].name); break; } } } 查看打印的數據,就會發現有的票重復賣了。
解決方法:
加上一個同步鎖: - (void)selectTick { while (1) { @synchronized (self) { if (self.count > 0) { self.count --; for (NSInteger i = 0; i < 1000; i++) { } NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count); }else { NSLog(@"%@賣完了",[NSThread currentThread].name); break; } } } } 注意加鎖的位置,如果鎖加在了while(1)之前,等於說,是一個人執行了賣票的流程,那么只有一個人把票全部賣完了,其他人才能賣票。
- 8.7 原子與非原子屬性
(1)OC在定義屬性時有nonatomic 和 atomic兩種選擇: atomic:原子屬性,為setter方法加鎖(默認就是atomic); nonatomic:非原子屬性,不會為setter方法加鎖。 (2)nonatomic和atomic對比: atomic:線程安全,需要消耗大量的資源; nonatomic:非線程安全,適合內存小的移動設備。 (3)ios開發建議: 所有屬性都聲明為nonatomic; 盡量避免多線程搶奪同一塊資源; 盡量將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力。
- 8.8 線程間通信
- 線程間通信:在1個進程種,線程往往不是獨立存在的,多個線程之間需要經常進行通信。
- 線程間通信的體現:
- 1個線程傳遞數據給另1個線程;
- 在1個線程中執行完特定任務后,轉到另一個線程繼續執行任務。
- 線程間通信常用方法:
- - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
9. GCD的使用
9.1 什么是GCD 全稱:Grand Central Dispatch, 可譯為“牛逼的中樞調度器”;純C語言,提供了非常多強大的函數。 9.2 GCD的優勢 GCD是蘋果公司為多核的並行運算提出的解決方案 GCD會自動利用更多的CPU內核(比如雙核、四核) GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程) 程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼 9.3 任務和隊列 (1)GCD中有2個核心概念: 任務:執行什么操作; 隊列:用來存放任務。 (2)GCD使用的2個步驟: 定制任務:確定想做的事情; 將任務添加到隊列中:GCD會自動將隊列中的任務取出,放到對應的線程中執行;任務的取出遵循隊列的FIFO原則:先進先出,后進后出。
- 9.4 執行任務
GCD中有2個用來執行任務的函數: (1)用同步的方式執行任務 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); queue:隊列的意思;block:任務 (2)用異步的方式執行任務 dispatch_async(dispatch_queue_t queue, dispatch_block_t block); (3)同步和異步的區別: 同步:只能在當前線程中執行任務,不具備開啟新線程的能力 異步:可以在新的線程中執行任務,具備開啟新線程的能力
- 9.5 並發隊列
GCD默認已經提供了全局的並發隊列,供整個應用使用,不需要手動創建。
使用dispatch_get_global_queue函數獲得全局的並發隊列。
獲取一個全局並發隊列:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); 將任務添加到隊列中: dispatch_async(queue, ^{ //任務 NSLog(@"renwu1%@",[NSThread currentThread]); });
- 全局並發隊列的優先級:
#define DISPATCH_QUEUE_PRIORITY_HIGH =2 // 高 #define DISPATCH_QUEUE_PRIORITY_DEFAULT = 0 // 默認(中) #define DISPATCH_QUEUE_PRIORITY_LOW =(-2) // 低 #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
- 9.6 串行隊列
GCD中獲得串行有2中途徑: (1) 使用dispatch_queue_create函數創建串行隊列; // 創建一個串行隊列 dispatch_queue_t queue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL); // 將任務添加到隊列中 dispatch_async(queue, ^{ // 任務1 NSLog(@"耗時任務1:%@",[NSThread currentThread]); }); (2)使用dispatch_get_main_queue()獲得主隊列:放在主隊列中的任務,都會放到主線程中執行; dispatch_queue_t queue = dispatch_get_main_queue();
- 9.7 線程間通信示例
從子線程回到主線程:
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行耗時的異步操作... dispatch_async(dispatch_get_main_queue(), ^{ // 回到主線程,執行UI刷新操作 }); });
- 9.8 延時執行
iOS常見的延時執行有2種方法: (1) 調用NSObject的方法: [self performSelector:@selector(run) withObject:nil afterDelay:2.0]; (2) 使用GCD函數: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后執行這里的代碼... 在哪個線程執行,跟隊列類型有關 });
(3)[NSTimer scheduledTimeWithTimeInterval:2.0 target:self seclector:@select(run) userInfo:nil repeat:No];
- 9.9 一次性代碼(單利模式)
單例模式的作用:可以保證在程序運行過程種,一個類只有一個實例,而且該實例易於供外界訪問;從而方便地控制了實例個數,並節約系統資源。
單例模式的使用場合:在整個應用程序中,共享一份資源(這份資源只需要創建初始化1次)。
單例模式在ARC/MRC環境下的寫法有所不同,需要編寫2套不同的代碼。可以用宏定義判斷是否為ARC環境:
#if __has_feature(objc_arc) // ARC #else // MRC #endif
使用dispatch_once函數能保證某段代碼在程序運行過程中只被執行1次
static dispatch_once_t onceToken; //內部實現原理:判斷onceToken的值 == 0 來覺得是否執行block中的任務;當執行過后,onceToken的值為 -1 dispatch_once(&onceToken, ^{ // 只執行1次的代碼(這里面默認是線程安全的) });
alloc會調用allocWithZone(這個方法會分配存儲空間)
給某個類添加一個單利的方法:
// 提供一個全局的靜態變量(對外界隱藏)
MYCUserModelInfo *_userInfo;
@implementation MYCUserModelInfo
// 從寫alloc方法保證永遠只分配一次存儲空間 alloc會調用allocWithZone(這個方法會分配存儲空間)
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_userInfo = [super allocWithZone:zone];
});
return _userInfo;
}
// 提供類方法
+ (instancetype)shareUserInfo {
return [[self alloc] init];
}
//重寫 copy 需要繼承NSMutableCoping
- (id)copyWithZone:(NSZone *)zone {
return _userInfo;
}
//需要繼承NSCoping
- (id)mutableCopyWithZone:(NSZone *)zone {
return _userInfo;
}
- 9.10 隊列組
有這么1種需求:分別異步執行2個耗時的操作,等2異步操作都執行完畢后,再回到主線程執行操作。
如果想要快速高效地實現上述需求,可以考慮用隊列組:
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的異步操作都執行完畢后,回到主線程... });
9.11 柵欄函數
dispath_barrier_asyn(queue,^ { NSlog(@"111111"); }) 柵欄函數,之前的任務是並發執行,之后的任務也是並發執行;
10. NSOperation的使用
基礎知識:
一般在開發中,直接使用GCD 開啟線程,做多線程的操作。如果,自己需要自定義框架/需要管理操作,這個時候,選擇NSOperation。 管理操作:取消操作/暫停/回復操作。 10.1 NSOperation的子類 NSOperation是個抽象類,並不具備封裝操作的能力,必須使用它的子類。 使用NSOperation子類的方式有3中: (1)NSInvocationOperation (2)NSBlockOperation (3)自定義子類繼承NSOperation,實現內部相應的方法 10.2 NSInvocationOperation
使用方式: // 創建一個操作.-創建對象 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil]; // 在當前線程執行. start 會在當前線程執行.直接啟動 [op start];
一旦執行操作,就會調用target的self方法。 注意:默認情況下,調用了start方法后並不會開一條新線程去執行操作,而是在當前線程同步執行操作。只有將NSOpeation放到一個NSOperationQueue中,才會異步執行操作。
- 10.3 NSBlockOperation
(1)創建NSBlockOperation對象: + (id)blockOperationWithBlock:(void (^)(void))block; (2)通過addExecutionBlock:方法添加更多操作 - (void)addExecutionBlock:(void (^)(void))block; 使用方法: // 創建一個操作. // 如果 NSBlockOperation 中封裝的操作數 > 1(追加操作了),這個時候,不能保證任務在哪條線程中執行.即使將操作添加到主隊列,依然會有任務在子線程執行. // 建議:如果使用 NSBlockOperation,為了確保任務在自己想要的線程執行,最好不要追加操作. NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ // [self test]; }]; // 追加操作/任務 [op addExecutionBlock:^{ NSLog(@"--------------1 %@",[NSThread currentThread]); }];
- 10.4 NSOperationQueue
NSOperationQueue 的作用: (1)NSOperation可以調用start方法來執行任務,但默認是同步執行的; (2)如果將NSOperation添加到NSOperationQueue(操作隊列中),系統會自動異步執行NSOperationQueue中的操作。 添加操作到NSOperationQueue中: - (void)addOperation:(NSOperation *)op; - (void)addOperationWithBlock:(void (^)(void))block; 使用方法: // 1.主隊列:主隊列中的操作,都要交給主線程執行. // 獲取主隊列 NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; // 將操作添加到隊列中 [mainQueue addOperation:op]; // 2.非主隊列.添加到非主隊列中的操作,都交給子線程來執行. // 創建一個非主隊列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 將操作添加到非主隊列中 [queue addOperation:op]; //內部會調用start方法,start方法會調用main方法 // 這一句代碼,相當於之前的兩句. [mainQueue addOperationWithBlock:^{ NSLog(@"直接往操作隊列中添加操作%@",[NSThread currentThread]); }];
- 常規操作
10.5 最大並發數 並發數的概念:同時執行的任務數,比如同時開3個線程執行3個任務,並發數就是3. 最大並發數的相關方法: - (NSInteger)maxConcurrentOperationCount; - (void)setMaxConcurrentOperationCount:(NSInteger)cnt; 10.6 隊列的取消、暫停和恢復及監聽 (1)取消隊列的所有操作:- (void)cancelAllOperations; (2)取消單個操作:- (void)cancel;(NSOperation的方法) 只能暫停后面的操作,不能暫停當前的任務 (3)暫停和恢復隊列: - (void)setSuspended:(BOOL)b; // YES代表暫停隊列,NO代表恢復隊列 - (BOOL)isSuspended; (4)操作的監聽:可以監聽一個操作的執行完畢; - (void (^)(void))completionBlock; - (void)setCompletionBlock:(void (^)(void))block; // 創建操作 NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ [self test1]; }]; // 當某個操作完成之后,就會回調這個方法. [op3 setCompletionBlock:^{ // 對於Block,盡量用 set 方法,這樣就不用去猜測 Block 的類型了. NSLog(@"操作完成之后的回調"); }];
- 10.7 操作依賴
NSOperation之間可以設置依賴來保證執行順序,比如一定要讓操作A執行完后,才能執行操作B,可以這么實現:
[operationB addDependency:operationA]; // 操作B依賴於操作A
但是要注意,不能相互依賴,即:A依賴B,而B依賴A;可以在不同queue的NSOperation之間創建依賴關系
- 10.8 自定義NSOperation
自定義NSOperation的步驟很簡單:第一個新建一個類,繼承NSOperation;第二步:重寫- (void)main 方法,在里面實現想執行的任務。
重寫- (void)mian方法的注意點:
自己創建自動釋放池(因為如果是異步操作,無法訪問主線程的自動釋放池);
經常通過- (BOOL)isCancelled方法檢驗操作是否被取消,對取消做出響應。蘋果官方建議:在自定義操作的時候,每執行完一個耗時操作就判斷一下當前是否取消,如果取消就返回。
// 當調用了 NSOperation 的 start 方法或者 將操作添加到操作隊列中之后,就會調用這個main 方法.(就會執行main 方法中的內容.) // @autoreleasepool {} 自動釋放池.一般情況下,操作在子線程執行,子線程的運行循環一般情況下不會開啟.子線程中的對象默認情況下不會訪問主線程中的自動釋放池,所以需要手動添加. -(void)main { @autoreleasepool { // 當程序運行完下面這句代碼之后,圖片就下載成功. UIImage *image = [self downloadWebImageWithUrlString:self.urlString];
if(self.isCannel){
return;
} // 顯示圖片.必須在主線程顯示. dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"顯示圖片%@",[NSThread currentThread]); self.imageView.image = image; }); } } // 下載網絡圖片的方法 - (UIImage *)downloadWebImageWithUrlString:(NSString *)urlString { NSLog(@"downloadWebImageWithUrlString:%@",[NSThread currentThread]); NSURL *url = [NSURL URLWithString:urlString]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; return image; }