歡迎光臨
每天分享高質量文章

coobjc — 阿裡剛開源的 iOS 協程開發框架

來自:開源中國

鏈接: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(nilnil, &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");
});

使用元組你可以從 await 傳回值中獲取多個值。

赞(0)

分享創造快樂