來自:開源中國
連結:https://www.oschina.net/p/coobjc
coobjc 為 Objective-C 和 Swift 提供了協程功能。coobjc 支援 await、generator 和 actor model,介面參考了 C# 、Javascript 和 Kotlin 中的很多設計。我們還提供了 cokit 庫為 Foundation 和 UIKit 中的部分 API 提供了協程化支援,包括 NSFileManager、JSON、NSData 與 UIImage 等。coobjc 也提供了元組的支援。
0x0 iOS 非同步程式設計問題
基於 Block 的非同步程式設計回呼是目前 iOS 使用最廣泛的非同步程式設計方式,iOS 系統提供的 GCD 庫讓非同步開發變得很簡單方便,但是基於這種程式設計方式的缺點也有很多,主要有以下幾點:
-
容易進入”巢狀地獄”
-
錯誤處理複雜和冗長
-
容易忘記呼叫 completion handler
-
條件執行變得很困難
-
從互相獨立的呼叫中組合傳回結果變得極其困難
-
在錯誤的執行緒中繼續執行
-
難以定位原因的多執行緒崩潰
-
鎖和訊號量濫用帶來的卡頓、卡死
上述問題反應到線上應用本身就會出現大量的多執行緒崩潰。
0x1 解決方案
上述問題在很多系統和語言中都會遇到,解決問題的標準方式就是使用協程。這裡不介紹太多的理論,簡單說協程就是對基礎函式的擴充套件,可以讓函式非同步執行的時候掛起然後傳回值。協程可以用來實現 generator ,非同步模型以及其他強大的能力。
Kotlin 是這兩年由 JetBrains 推出的支援現代多平臺應用的靜態程式語言,支援 JVM ,Javascript ,目前也可以在 iOS 上執行,這兩年在開發者社群中也是比較火。
在 Kotlin 語言中基於協程的 async/await ,generator/yield 等非同步化技術都已經成了語法標配,Kotlin 協程相關的介紹,大家可以參考:
https://www.kotlincn.net/docs/reference/coroutines/basics.html
0x2 協程
協程是一種在非搶佔式多工場景下生成可以在特定位置掛起和恢復執行入口的程式元件
協程的概念在60年代就已經提出,目前在服務端中應用比較廣泛,在高併發場景下使用極其合適,可以極大降低單機的執行緒數,提升單機的連線和處理能力,但是在移動研發中,iOS和android目前都不支援協程的使用
0x3 coobjc 框架
coobjc 是由手機淘寶架構團隊推出的能在 iOS 上使用的協程開發框架,目前支援 Objective-C 和 Swift 中使用,我們底層使用彙編和 C 語言進行開發,上層進行提供了 Objective-C 和 Swift 的介面,目前以 Apache 開源協議進行了開源。
0x31 安裝
-
cocoapods 安裝: pod ‘coobjc’
-
原始碼安裝: 所有程式碼在 ./coobjc 目錄下
0x32 檔案
-
閱讀 協程框架設計 檔案。
-
閱讀 coobjc Objective-C Guide 檔案。
-
閱讀 coobjc Swift Guide 檔案。
-
閱讀 cokit framework 檔案, 學習如何使用系統介面封裝的 api 。
0x33 特性
async/await
-
建立協程
使用 co_launch 方法建立協程
co_launch(^{
...
});
co_launch 建立的協程預設在當前執行緒進行排程
-
await 非同步方法
在協程中我們使用 await 方法等待非同步方法執行結束,得到非同步執行結果
- (void)viewDidLoad{
...
co_launch(^{
NSData *data = await(downloadDataFromUrl(url));
UIImage *image = await(imageFromData(data));
self.imageView.image = image;
});
}
上述程式碼將原本需要 dispatch_async 兩次的程式碼變成了順序執行,程式碼更加簡潔
-
錯誤處理
在協程中,我們所有的方法都是直接傳回值的,並沒有傳回錯誤,我們在執行過程中的錯誤是透過 co_getError() 獲取的,比如我們有以下從網路獲取資料的介面,在失敗的時候, promise 會 reject:error
- (CCOPromise*)co_GET:(NSString*)url
parameters:(NSDictionary*)parameters{
CCOPromise *promise = [CCOPromise promise];
[self GET:url parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[promise fulfill:responseObject];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[promise reject:error];
}];
return promise;
}
那我們在協程中可以如下使用:
co_launch(^{
id response = await([self co_GET:feedModel.feedUrl parameters:nil]);
if(co_getError()){
//處理錯誤資訊
}
...
});
生成器
-
建立生成器
我們使用 co_sequence 建立生成器
COCoroutine *co1 = co_sequence(^{
int index = 0;
while(co_isActive()){
yield_val(@(index));
index++;
}
});
在其他協程中,我們可以呼叫 next 方法,獲取生成器中的資料
co_launch(^{
for(int i = 0; i 10; i++){
val = [[co1 next] intValue];
}
});
-
使用場景
生成器可以在很多場景中進行使用,比如訊息佇列、批次下載檔案、批次載入快取等:
int unreadMessageCount = 10;
NSString *userId = @"xxx";
COSequence *messageSequence = sequenceOnBackgroundQueue(@"message_queue", ^{
//在後臺執行緒執行
while(1){
yield(queryOneNewMessageForUserWithId(userId));
}
});
//主執行緒更新UI
co(^{
for(int i = 0; i if(!isQuitCurrentView()){
displayMessage([messageSequence take]);
}
}
});
透過生成器,我們可以把傳統的生產者載入資料->通知消費者樣式,變成消費者需要資料->告訴生產者載入樣式,避免了在多執行緒計算中,需要使用很多共享變數進行狀態同步,消除了在某些場景下對於鎖的使用
Actor
_ Actor 的概念來自於 Erlang ,在 AKKA 中,可以認為一個 Actor 就是一個容器,用以儲存狀態、行為、Mailbox 以及子 Actor 與 Supervisor 策略。Actor 之間並不直接通訊,而是透過 Mail 來互通有無。_
-
建立 actor
我們可以使用 co_actor_onqueue 在指定執行緒建立 actor
CCOActor *actor = co_actor_onqueue(^(CCOActorChan *channel) {
... //定義 actor 的狀態變數
for(CCOActorMessage *message in channel){
...//處理訊息
}
}, q);
-
給 actor 傳送訊息
actor 的 send 方法可以給 actor 傳送訊息
CCOActor *actor = co_actor_onqueue(^(CCOActorChan *channel) {
... //定義actor的狀態變數
for(CCOActorMessage *message in channel){
...//處理訊息
}
}, q);
// 給actor傳送訊息
[actor send:@"sadf"];
[actor send:@(1)];
元組
-
建立元組
使用 co_tuple 方法來建立元組
COTuple *tup = co_tuple(nil, @10, @"abc");
NSAssert(tup[0] == nil, @"tup[0] is wrong");
NSAssert([tup[1] intValue] == 10, @"tup[1] is wrong");
NSAssert([tup[2] isEqualToString:@"abc"], @"tup[2] is wrong");
可以在元組中儲存任何資料
-
元組取值
可以使用 co_unpack 方法從元組中取值
id val0;
NSNumber *number = nil;
NSString *str = nil;
co_unpack(&val0;, &number;, &str;) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
co_unpack(&val0;, &number;, &str;) = co_tuple(nil, @10, @"abc", @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
co_unpack(&val0;, &number;, &str;, &number;, &str;) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");
NSString *str1;
co_unpack(nil, nil, &str1;) = co_tuple(nil, @10, @"abc");
NSAssert([str1 isEqualToString:@"abc"], @"str1 is wrong");
-
在協程中使用元組
首先建立一個 promise 來處理元組裡的值
COPromise*
cotest_loadContentFromFile(NSString *filePath){
return [COPromise promise:^(COPromiseFullfill _Nonnull resolve, COPromiseReject _Nonnull reject) {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
resolve(co_tuple(filePath, data, nil));
}
else{
NSError *error = [NSError errorWithDomain:@"fileNotFound" code:-1 userInfo:nil];
resolve(co_tuple(filePath, nil, error));
}
}];
}
然後,你可以像下麵這樣獲取元組裡的值:
co_launch(^{
NSString *tmpFilePath = nil;
NSData *data = nil;
NSError *error = nil;
co_unpack(&tmpFilePath;, &data;, &error;) = await(cotest_loadContentFromFile(filePath));
XCTAssert([tmpFilePath isEqualToString:filePath], @"file path is wrong");
XCTAssert(data.length > 0, @"data is wrong");
XCTAssert(error == nil, @"error is wrong");
});