多線程在iOS開發中的應用

什麼是主線程?

一個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章