什麼是主線程?
一個iOS程序運行之後,會默認開啓一個線程,這個線程就叫做“主線程”或“UI線程”
主線程的主要作用:
顯示/刷新UI界面
處理UI事件(比如點擊事件,滾動事件,拖拽事件等)
主線程的使用注意:
別將耗時操作放到主線程中
耗時操作會卡住主線程,如果將耗時操作放到主線程上,那麼只能等耗時操作執行完了之後,纔會響應事件。
正確的做法:開啓一個子線程,將耗時操作放在子線程(後臺線程,非主線程)
代碼實現:
//獲得主線程
NSThread *mainThread = [NSThread mainThread];
//獲得當前線程
NSThread *currentThread = [NSThread currentThread];
//判斷主線程
/*
1.
number = 1主線程
2.類方法
BOOL = isMainThread = [NSThread isMainThread];
3.對象方法
BOOL = isMainThread = [currentThread isMainThread];
*/
iOS中多線程實現方案
1.pthread
C語言
一套通用的多線程API
適用於Unix\Linux\Window等系統
跨平臺
幾乎不咋用
#import <pthread.h>
//創建線程對象
pthread_t thread;
//創建線程
/*
第一個參數:線程對象,傳遞地址
第二個參數:線程的屬性 NULL
第三個參數:指向函數的指針
第四個參數:函數需要接受的參數
*/
pthread_create(&thread, NULL, task, NULL);
2.NSThread
OC語言
面向對象
簡單易用,可直接操作線程對象
程序員手動管理生命週期
偶爾使用
/*
第一個參數:目標對象 self
第二個參數:方法選擇器 調用的方法
第三個參數:前面調用方法需要傳遞的參數
*/
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"ABC"];
//啓動線程之前設置屬性
thread.name = @"線程A";
thread.threadPriority = 123.231;
[thread start]; //這種方法創建線程需要手動啓動線程
-(void)run:(NSString *)param
{
NSLog(@"--run--",[NSThread currentThread]);
}
第二種方法:
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"子線程"];
//缺點:後面兩種方法沒法拿到線程對象,不能設置線程對象的屬性
第三種方法
[NSThread performSelectorInBackground:@selector(run:) toTarget:self withObject:@"子線程"];
**NSThread的生命週期:**當線程中的任務執行完畢之後纔會釋放
線程的狀態:
-(void)start;
//進入就緒狀態
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
//進入阻塞狀態
+(void)exit;
//強制停止線程
//進入死亡狀態
線程安全
資源共享
1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源,比如多個線程訪問同一個對象,同一個變量,同一個文件。當多個線程訪問到同一塊資源時,很容易引發數據錯亂和數據安全問題。
解決辦法:加互斥鎖
如果線程A和線程B可以訪問同一塊資源,那麼爲了解決線程安全的問題,蘋果官方提出了加互斥鎖的方法,即當線程A訪問資源的時候,資源A就被鎖住了,這時其他線程就不能再讀取資源數據,只有當線程A死亡的時候,資源纔會解鎖,這時線程B就可以訪問資源了。線程B訪問資源的時候同樣要加鎖。
//1.必須是全局唯一的。
//2.注意加鎖的位置,多線程共享同一塊資源。
//3.注意加鎖是需要代價的,需要耗費性能。
//4.加鎖的結果會形成一種現象,即線程同步,按順序執行任務
@synchronized(self){
//需要鎖定的代碼
}
原子和非原子屬性
OC在定義屬性時有nonatomic和atomic兩種選擇
atomic:線程安全的,需要消耗大量的資源
nonatomic:非線程安全的,適合內存小的移動設備
iOS開發的建議:
所有屬性都聲明爲nonatomic
儘量避免多線程搶奪同一塊資源
儘量將加鎖,資源搶奪的業務邏輯交給服務器端處理,減小移動端的壓力
NSThread線程之間的通信
什麼叫做線程間通信:
在1個線程中,線程往往不是孤立存在的,多個線程之間需要經常進行通信
線程間通信的體現:
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;
線程間通信示例----圖片下載
[NSThread detachNewThreadSelector:@selector(download) toTarget:self withObject:nil];
-(void)download{
NSURL *url = [NSURL URLWithString:@"http://123.com"];
NSData *imageData = [NSData dateWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
//回到主線程顯示UI
//waitUntilDone:是否等待 如果後邊還有其它的代碼,YES意味着showImage方法執行完畢之後纔會執行之後的代碼,NO則不需要等待
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:NO];
self.imageView.image = image;
}
-(void)showImage:(UIImage *)image{
self.imageView.image = image;
}
3.GCD
C語言
充分利用設備的多核
自動管理生命週期
經常使用
什麼是GCD?
全稱是Grand Central Dispatch, ”中樞調度器“
純C語言,提供了非常多強大的函數
GCD是蘋果公司爲多核的並行運算提出的解決方案
GCD會自動利用更多的CPU內核
GCD會自動管理線程的生命週期
程序員只需要告訴它想要幹什麼,不需要手動管理
GCD中有2個核心概念
任務:執行什麼操作
隊列:用來存放任務
GCD的使用
GCD的使用就兩個步驟:
定製任務:
確定想做的事情
將任務添加到隊列中:
GCD會自動將隊列中的任務取出,放到對應的線程中執行
任務的取出遵循先進先出,後進後出原則
//執行任務的兩種方式
//同步方式
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
//queue隊列 block任務
//異步
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
//同步:只能在當前線程中執行任務,不具備開啓新線程的能力
//異步:可以在新的線程中執行任務,具備開啓新線程的能力
GCD中的隊列可以分爲兩大類:
併發隊列:可以讓多個任務併發(同時)執行(自動開啓多個線程同時執行任務)
併發功能只有在異步函數下才有效
串行隊列:讓任務一個接着一個地執行(一個任務執行完才能執行另一個任務)
總結:同步和異步影響的是能不能開線程,併發和串行影響的是任務的執行方式
代碼:使用GCD如何開啓線程
//異步函數,併發隊列
-(void)asyncConcurrent{
//創建隊列
//第一個參數:C語言的字符串,標籤
//第二個參數:宏DISPATCH_QUEUE_CONCURRENT並行 DISPATCH_QUEUE_SERIAL串行
dispatch_queue_create("com.520it.download", DISPATCH_QUEUE_CONCURRENT);
//封裝任務 添加任務到隊列中
//第一個參數:隊列
//第二個參數:要執行的任物
dispatch_async(queue, ^{
//任務
});
}
//創建並行隊列還有一種方法
//第一個參數:優先級
//第二個參數:未來使用,傳0即可
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
//這種方式和前一種方式幾乎是一樣的,有一點區別就是上面的方法是創建一個隊列,這種方式是獲得系統的隊列。
注意:GCD在系統中開幾條線程並不是由任務的數量決定的,是由系統決定的的開啓最合適的線程數量。
創建串行隊列:
//創建隊列1
//第一個參數:C語言的字符串,標籤
//第二個參數:宏DISPATCH_QUEUE_CONCURRENT並行 DISPATCH_QUEUE_SERIAL串行
dispatch_queue_t queue = dispatch_queue_create("com.520it.com",NULL);
//獲得主隊列2
//使用主隊列,主隊列是GCD自帶的一種特殊的串行隊列
//放在主隊列中的任務,都會放到主線程中執行
dispatch_get_main_queue();
//異步函數+主隊列 所有任務都在主線程中執行,不會開啓線程
//同步函數+主隊列 產生了死鎖 如果主隊列發現當前主線程有任務在執行,那麼主隊列會暫停調度當前隊列中的任務,直到主線程空閒爲止。 解決方案是開一個子線程,讓子線程執行當前函數,然後函數中的任務就可以在主線程中執行了。
例如:
//同步函數+主隊列
void main(){
//開啓一個子線程,在子線程中執行函數
[NSThread detachNewThreadSelector:@selector(asyncConcurrent) toTarget:self withObject:nil];
}
-(void)asyncConcurrent{
//創建隊列
//第一個參數:C語言的字符串,標籤
//第二個參數:宏DISPATCH_QUEUE_CONCURRENT並行 DISPATCH_QUEUE_SERIAL串行
dispatch_queue_t queue = dispatch_get_main_queue();;
//封裝任務 添加任務到隊列中
//第一個參數:隊列
//第二個參數:要執行的任物
dispatch_async(queue, ^{
//任務1
});
dispatch_async(queue, ^{
//任務2
});
dispatch_async(queue, ^{
//任務3
});
}
同步函數和異步函數的區別總結:
1.同步函數不具備開啓線程的能力,異步函數具備開啓線程的能力
2.同步函數+主隊列會發生死鎖,異步函數+主隊列不會發生死鎖。
GCD實現線程間通信
dispatch_async(dispatch_get_global_queue(0,0), ^{
//在子線程執行的任務
dispatch_async(dispatch_get_main_queue(),^{
//在主線程要執行的任務
})
})
GCD常用函數
1、延遲函數
//延遲執行方法1
[self performSelector:@selector(task) withObject:nil afterDelay:2.0]; //2s之後調用task方法,不需要傳參
//延遲執行方法2
[NSTimer scheduledTimerWithTimerInterval:2.0 target:self selector:@selector(task) userInfo:nil repeat:YES]; //每隔2秒執行一次
//延遲執行
//第一個參數:從現在開始計算時間
//第二個參數:要延遲的時間
//第三個參數:隊列,可以是主隊列也可以是自動創建的併發隊列,這樣任務就會在子線程執行。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//要執行的代碼
})
2、一次性代碼
//一次性代碼:整個程序運行過程中只會運行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//代碼段
});
//一次性代碼是不能放在懶加載中的。
//主要作用:設計模型中單例模式
3、柵欄函數
//異步函數 ,可以控制任務的執行順序,在柵欄函數前面的先執行,後面的後執行
//柵欄函數在使用的時候不能使用全局併發隊列
dispatch_barrier_async(queue, ^{
NSLog(@"--dispatch_barrier_async-");
});
4、快速迭代
//快速迭代(開多個線程併發完成迭代操作)
//第一個參數:遍歷的次數
//第二個參數:併發隊列
//第三個參數:
dispatch_apply(10, queue, ^(size_t index) {
//代碼塊
});
5.隊列組
//創建隊列
dispatch_group_t group = dispatch_group_create();
//異步函數
dispatch_group_async(group, queue, ^{
//任務
});
//隊列組中的任務執行完畢之後,執行該函數
dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);
//進入羣組和離開羣組
dispatch_group_enter(group);//執行該函數後,後面異步執行的block會被gruop監聽
dispatch_group_leave(group);//異步block中,所有的任務都執行完畢,最後離開羣組
//注意:dispatch_group_enter|dispatch_group_leave必須成對使用
4.NSOperation
OC語言
自動管理生命週期
經常使用
NSOperation兩個概念:操作 隊列
配合使用NSOperation和NSOperationQueue也能實現多線程
NSOperation和NSOperationQueue實現多線程的具體步驟:
1.先將需要執行的操作封裝到一個NSOperation對象中
2.然後將NSOperation對象添加到NSOperationQueue中
3.系統會自動將NSOperationQueue中的NSOperation取出來
4.將取出的NSOperation封裝的操作放到一個新線程中執行
NSOperation是個抽象類,並不具備封裝操作的能力,必須使用它的子類
使用NSOperation子類的方式有3種
NSInvocationOperation
NSBlockOperation
自定義子類,實現方法
NSInvocationOperation
//1.創建操作,封裝任務
/*
第一個參數:目標對象 self
第二個參數:調用方法的名稱
第三個參數:前面方法需要接受的參數 nil
*/
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
[op1 start];
//如果只是封裝操作,並不會開啓一個新的線程
NSBlockOperation
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
//任務1
}]
//追加任務
//注意:如果一個操作中的任務數大於1,那麼會開子線程併發執行任務
//注意:不一定是子線程,有可能是主線程
op1 addExceptionBlock:^{
//任務2
};
op1 addExceptionBlock:^{
//任務3
};
op1 addExceptionBlock:^{
//任務4
};
[op1 start];
基本使用方法
NSInvocationOperation 和NSOperationQueue
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
2.創建隊列
主隊列:[NSOperationQueue mainQueue]; 和GCD中的主隊列一樣,串行隊列
非主隊列:[[NSOperationQueue alloc] init]; 非常特殊,(同時具備併發和串行的功能,默認情況下,非主隊列是一個併發隊列)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//添加操作到隊列
[queue addOperation:op1]; //內部已經調用了start方法
NSBlockOperation 和NSOperationQueue
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
//任務1
}]
[op1 addExceptionBlock:^{
//任務2
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//添加操作到隊列
[queue addOperation:op1]; //內部已經調用了start方法
簡便操作
[queue addOperationWithBlock:^{
//任務
}]
自定義子類繼承NSOperation
1.有利於代碼隱蔽
2.有利於代碼複用
//MydefineClass
-(void)main{
//任務寫在自定義的類中
}
//其它類中使用
MyDefineClass *op1 = [[MyDefineClass alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op1];
補充:執行addOperation時內部執行了start方法,而start方法內部又執行了main方法。
NSThread的start方法內部同理也是調用了main方法
其它應用
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//設置最大併發數,同一時間最多可以有多少個任務執行
queue.maxConcurrentOperationCount = 5; //設置0,不執行任務,設置-1表示最大值,不受限制
注意:串行執行任務不等於只開一條線程,只能說它是以線程同步的方式執行任務的
線程同步:一個任務執行完了之後再執行另一個任務。
隊列中的任務是有狀態的。已經執行完畢的 | 正在執行 | 等待狀態
如果點擊了暫停,是不能立刻暫停的,需要把當前正在執行的任務執行完才能暫停
暫停
[queue setSuspended:YES]; //可以恢復
恢復
[queue setSuspend:NO]; //
取消
[queue cancelAllOperations]; //不可以恢復,如果任務執行到一半的時候點擊取消,那麼當前隊列中的任務是不會恢復的
//該方法調用了當前隊列裏面所有操作的cancel方法。
如果是自定義類實現任務,那麼點擊暫停操作是不會暫停的。因爲一個類中只有一個任務。
暫停任務的小技巧:
-(void)main{
//任務寫在自定義的類中
//不建議放到循環內部去取消操作,因爲循環次數較多時比較耗時
if(self.isCancelled) return;
}
NSOperation操作依賴和監聽
操作依賴
//創建操作
NSBlockOption *op1 = [NSBlockOption blockOperationWithBlock:^{
//任務1
}];
NSBlockOption *op2 = [NSBlockOption blockOperationWithBlock:^{
//任務2
}];
NSBlockOption *op3 = [NSBlockOption blockOperationWithBlock:^{
//任務3
}];
NSBlockOption *op4 = [NSBlockOption blockOperationWithBlock:^{
//任務4
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[op1 addDependency:op4];
[op4 addDependency:op3];
[op3 addDependency:op2];
//執行順序 2 3 4 1
//注意點:不能循環依賴
//可以跨隊列依賴
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
操作監聽
op3.completionBlock = ^{
//任務3完成之後要執行的代碼
//這塊代碼和op3不一定在一個線程執行
}
線程間通信
//創建操作
NSBlockOption *op1 = [NSBlockOption blockOperationWithBlock:^{
//任務1
//返回主線程
[NSOperationQueue mainQueue] addOperationWithBlock:^{
//主線程執行的代碼
}
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op1];
多圖下載綜合案例
UITableView優化
1.UI不流暢---->開子線程下載圖片
1.圖片不會刷新---->手動刷新,因爲開子線程之後,圖片的下載就是異步的,這時會先創建cell,而圖片下載完成之後cell已經創建出來了,所以需要手動刷新一下cell。
2.圖片重新下載---->圖片下載需要時間,當圖片還未完全下載完成之前,又要下載,解決辦法也是添加一個內存緩存證明圖片下載操作已經存在了。
3.數據錯亂---->先清空之前的cell圖片,或者設置佔位圖片
2.圖片重複下載---->先把之前已經下載的圖片保存起來NSDictionary,先去內存緩存中有沒有這張圖片,如果沒有,再去沙盒緩存中看有沒有,如果還沒有,再去下載圖片。
沙盒緩存
documents:當手機連到ituns後會備份,蘋果官方不允許把緩存文件放到這個目錄下
library:兩個子目錄偏好設置和緩存路徑,偏好設置主要是用來保存賬號信息,緩存文件是專門用來保存緩存數據的
tmp:臨時路徑,這個文件夾中的文件隨時會被刪除
第三方框架SDWebImage