coswift 1.2.5

coswift 1.2.5

念纪 维护。



coswift 1.2.5

  • pengyutang125

coobjc

这个库为 Objective-C 和 Swift 提供了协程支持。我们添加了 await 方法、生成器和actor模型,类似于 C#、JavaScript 和 Kotlin。为了方便,我们在 cokit 框架中为一些 Foundation 和 UIKit API 添加了协程类别,例如 NSFileManager、JSON、NSData、UIImage 等。我们还在 coobjc 中添加了包支持。

cooobjc 中文文档

0x0 iOS 异步编程问题

基于 Blocks 的异步编程回调是目前最广泛使用的 iOS 异步编程方法。iOS 系统提供的 GCD 库使得异步开发非常简单方便,但是基于这种编程方式存在许多缺点:

  • 陷入回调地狱

    简单的操作顺序不合理地组合在嵌套的 blocks 中。这种“回调地狱”使跟踪正在运行的代码变得困难,而闭包栈导致许多二阶效应。

  • 处理错误变得困难且冗长

  • 条件执行难以进行且容易出错

  • 忘记调用完成块

  • 因为完成处理器不成熟,太多的 API 被定义为同步的

    这很抽象,但作者认为,定义和使用异步 API(使用完成处理器)的不成熟导致了许多 API 被定义为具有明显同步行为,即使它们可以阻塞。这可能导致 UI 应用程序中的问题性能和响应性问题 - 例如旋转的光标。这还可能导致在异步至关重要时无法使用的 API 定义,例如在服务器上。

  • 难以定位的多线程崩溃

  • 由于阻塞引起的锁和信号量滥用

0x1 解决方案

这些问题在很多系统和多种语言中都曾出现过,协程的抽象是一个解决这些问题的标准方法。不深入了解理论的话,协程是基本函数的扩展,允许函数返回值或被挂起。它们可用于实现生成器、异步模式和其他功能——关于它们的理论、实现和优化有大量的研究。

Kotlin 是 JetBrains 支持的静态编程语言,支持现代多平台应用。过去两年在开发社区中非常受欢迎。在 Kotlin 语言中,基于协程的 async/await、generator/yield 等异步技术已成为语法标准。有关 Kotlin 协程的介绍,您可以参考:https://www.kotlincn.net/docs/reference/coroutines/basics.html

0x2 协程

协程是计算机程序组件,通过允许执行被挂起和恢复来泛化子程序以支持非抢占式多任务。协程非常适合实现诸如协作任务、异常、事件循环、迭代器、无限列表和管道等熟悉的程序组件

协程的概念在 20 世纪 60 年代被提出。它在服务器中被广泛应用。非常适合用于高并发场景。它可以大大减少一台机器中线程的数量,并提高一台机器的连接和处理器能力。同时,iOS 目前不支持使用协程(这就是我们希望支持它的原因。)

0x3 coobjc 框架

coobjc 是阿里巴巴 Taobao-Mobile 架构团队在 iOS 上使用的协程开发框架。目前它支持 Objective-C 和 Swift 的使用。我们使用汇编和 C 语言进行开发,并为 Objective-C 和 Swift 提供了接口。目前,它在 Apache 开源许可证下开源。

0x31 安装

  • cocoapods 对 Objective-C: pod 'coobjc'
  • cocoapods 对 Swift: pod 'coswift'
  • cocoapods 对 Cokit: pod 'cokit'
  • 源代码:所有代码都在 ./coobjc 目录下

0x32 文档

0x33 功能

async/await

  • 创建协程

使用 co_launch 方法创建协程

co_launch(^{
    ...
});

co_launch 创建的协程默认在当前线程调度。

  • await 异步方法

在协程中,我们使用 await 方法等待异步方法执行,获取异步执行结果

- (void)viewDidLoad {
    ...
    co_launch(^{
        // async downloadDataFromUrl
        NSData *data = await(downloadDataFromUrl(url));
        
        // async transform data to image
        UIImage *image = await(imageFromData(data));

        // set image to imageView
        self.imageView.image = image;
    });
}

上面的代码将原本需要调用 dispatch_async 两次的代码变为顺序执行,代码更为简洁。

  • 错误处理

在协程中,我们所有的方法直接返回值,不返回错误。我们在执行过程中的错误通过 co_getError() 获取。例如,我们有以下接口从网络获取数据。当 promise 将会拒绝时:error

- (COPromise*)co_GET:(NSString*)url parameters:(NSDictionary*)parameters{
    COPromise *promise = [COPromise 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()){
        //handle error message
    }
    ...
});

生成器

  • 创建生成器

我们使用 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 = co_sequence_onqueue(background_queue, ^{
   //thread execution in the background
    while(1){
        yield(queryOneNewMessageForUserWithId(userId));
    }
});

//Main thread update UI
co_launch(^{
   for(int i = 0; i < unreadMessageCount; i++){
       if(!isQuitCurrentView()){
           displayMessage([messageSequence next]);
       }
   }
});

通过生成器,我们可以从传统的生产者-通知消费者模型加载数据,将消费者转换为数据告诉生产者加载模式的模式,避免在多线程计算中需要使用许多共享变量的状态。同步消除了在某些场景中使用锁的需要。

演员

演员的概念来源于Erlang。在AKKA中,演员可以视为一个用于存储状态、行为、邮箱和子演员及管理策略的容器。演员之间不直接通信,而是使用邮箱来相互通信。

  • 创建演员

我们可以使用co_actor_onqueue在指定的线程中创建演员。

COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
    ...  //Define the state variable of the actor

    for(COActorMessage *message in channel){
        ...//handle message
    }
});
  • 向演员发送消息

演员的send方法可以向演员发送消息

COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
    ...  //Define the state variable of the actor

    for(COActorMessage *message in channel){
        ...//handle message
    }
});

// send a message to the 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");
  • 在协程中使用元组首先创建一个解析元组值的承诺
COPromise<COTuple*>*
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返回获得多个值

使用coobjc的实际案例

让我们以GCDFetchFeed开源项目中Feeds流更新的代码为例,来展示实际的用例场景和协程的优点。以下是未使用协程的原始实现:

- (RACSignal *)fetchAllFeedWithModelArray:(NSMutableArray *)modelArray {
    @weakify(self);
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self);
        //Create a parallel queue
        dispatch_queue_t fetchFeedQueue = dispatch_queue_create("com.starming.fetchfeed.fetchfeed", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t group = dispatch_group_create();
        self.feeds = modelArray;
        for (int i = 0; i < modelArray.count; i++) {
            dispatch_group_enter(group);
            SMFeedModel *feedModel = modelArray[i];
            feedModel.isSync = NO;
            [self GET:feedModel.feedUrl parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
                dispatch_async(fetchFeedQueue, ^{
                    @strongify(self);
                    //parse feed
                    self.feeds[i] = [self.feedStore updateFeedModelWithData:responseObject preModel:feedModel];
                    //save to db
                    SMDB *db = [SMDB shareInstance];
                    @weakify(db);
                    [[db insertWithFeedModel:self.feeds[i]] subscribeNext:^(NSNumber *x) {
                        @strongify(db);
                        SMFeedModel *model = (SMFeedModel *)self.feeds[i];
                        model.fid = [x integerValue];
                        if (model.imageUrl.length > 0) {
                            NSString *fidStr = [x stringValue];
                            db.feedIcons[fidStr] = model.imageUrl;
                        }
                        //sendNext
                        [subscriber sendNext:@(i)];
                        //Notification single completion
                        dispatch_group_leave(group);
                    }];
                    
                });//end dispatch async
                
            } failure:^(NSURLSessionTask *operation, NSError *error) {
                NSLog(@"Error: %@", error);
                dispatch_async(fetchFeedQueue, ^{
                    @strongify(self);
                    [[[SMDB shareInstance] insertWithFeedModel:self.feeds[i]] subscribeNext:^(NSNumber *x) {
                        SMFeedModel *model = (SMFeedModel *)self.feeds[i];
                        model.fid = [x integerValue];
                        dispatch_group_leave(group);
                    }];
                    
                });//end dispatch async
                
            }];
            
        }//end for
        //Execution event after all is completed
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            [subscriber sendCompleted];
        });
        return nil;
    }];
}

以下是在viewModel中调用上述方法的代码:

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
self.fetchingCount = 0;
@weakify(self);
[[[[[[SMNetManager shareInstance] fetchAllFeedWithModelArray:self.feeds] map:^id(NSNumber *value) {
    @strongify(self);
    NSUInteger index = [value integerValue];
    self.feeds[index] = [SMNetManager shareInstance].feeds[index];
    return self.feeds[index];
}] doCompleted:^{
    @strongify(self);
    NSLog(@"fetch complete");
    self.tbHeaderLabel.text = @"";
    self.tableView.tableHeaderView = [[UIView alloc] init];
    self.fetchingCount = 0;
    [self.tableView.mj_header endRefreshing];
    [self.tableView reloadData];
    if ([SMFeedStore defaultFeeds].count > self.feeds.count) {
        self.feeds = [SMFeedStore defaultFeeds];
        [self fetchAllFeeds];
    }
    [self cacheFeedItems];
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {
    @strongify(self);
    self.tableView.tableHeaderView = self.tbHeaderView;
    self.fetchingCount += 1;
    self.tbHeaderLabel.text = [NSString stringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsigned long)self.fetchingCount,(unsigned long)self.feeds.count];
    feedModel.isSync = YES;
    [self.tableView reloadData];
}];

上述代码在可读性和简单性方面相对较差。让我们看看使用协程转换后的代码:

- (SMFeedModel*)co_fetchFeedModelWithUrl:(SMFeedModel*)feedModel{
    feedModel.isSync = NO;
    id response = await([self co_GET:feedModel.feedUrl parameters:nil]);
    if (response) {
        SMFeedModel *resultModel = await([self co_updateFeedModelWithData:response preModel:feedModel]);
        int fid = [[SMDB shareInstance] co_insertWithFeedModel:resultModel];
        resultModel.fid = fid;
        if (resultModel.imageUrl.length > 0) {
            NSString *fidStr = [@(fid) stringValue];
            [SMDB shareInstance].feedIcons[fidStr] = resultModel.imageUrl;
        }
        return resultModel;
    }
    int fid = [[SMDB shareInstance] co_insertWithFeedModel:feedModel];
    feedModel.fid = fid;
    return nil;
}

以下是viewModel中用于调用接口的协程调用的地方

co_launch(^{
    for (NSUInteger index = 0; index < self.feeds.count; index++) {
        SMFeedModel *model = self.feeds[index];
        self.tableView.tableHeaderView = self.tbHeaderView;
        self.tbHeaderLabel.text = [NSString stringWithFormat:@"正在获取%@...(%lu/%lu)",model.title,(unsigned long)(index + 1),(unsigned long)self.feeds.count];
        model.isSync = YES;
        SMFeedModel *resultMode = [[SMNetManager shareInstance] co_fetchFeedModelWithUrl:model];
        if (resultMode) {
            self.feeds[index] = resultMode;
            [self.tableView reloadData];
        }
    }
    self.tbHeaderLabel.text = @"";
    self.tableView.tableHeaderView = [[UIView alloc] init];
    self.fetchingCount = 0;
    [self.tableView.mj_header endRefreshing];
    [self.tableView reloadData];
    [self cacheFeedItems];
});

经过协程转换后的代码变得更易于理解,也更不容易出错。

Swift

coobjc完全支持Swift通过顶层封装,使得我们可以提前享受到Swift中的协程。因为Swift语法支持更为丰富和先进,所以在Swift中使用coobjc更加优雅,例如

func test() {
    co_launch {//create coroutine
        //fetch data asynchronous
        let resultStr = try await(channel: co_fetchSomething())
        print("result: \(resultStr)")
    }

    co_launch {//create coroutine
        //fetch data asynchronous
        let result = try await(promise: co_fetchSomethingAsynchronous())
        switch result {
            case .fulfilled(let data):
                print("data: \(String(describing: data))")
                break
            case .rejected(let error):
                print("error: \(error)")
        }
    }
}

0x4 协程的优势

  • 简洁性
    • 概念少:只有几个操作符,与响应中成百上千的操作符相比,它更加简单。
    • 原理简单:协程的实现原理非常简单,整个协程库只有几千行代码
  • 易于使用
    • 使用简单:使用方式比GCD简单,接口少。
    • 易于改造:现有代码只需做少量修改即可使用协程,我们还有大量针对系统库的协程接口。
  • 清晰性
    • 同步写异步逻辑:以同步顺序方式编写代码是人类最容易接受的方式,这可以大幅降低错误发生的概率。
    • 高可读性:以协程模式编写的代码比嵌套的代码块更具可读性。
  • 高性能
    • 调度性能更快:协程本身不需要在内核级线程之间切换,调度性能快,即使创建成千上万的协程,也不会有压力。
    • 减少应用阻塞:通过使用协程帮助减少锁和信号量的滥用,通过封装导致阻塞的I/O等协程接口等方式,从根源上减少停滞和拥堵,从而提高应用的总体性能。

0x5 通信

  • 如果您需要帮助,请使用Stack Overflow。(标签:“coobjc”)
  • 如果您想提出一般性问题,请使用Stack Overflow
  • 如果您发现了一个错误,并且能提供可靠重放的步骤,请提交一个issue。
  • 如果您有特性请求,请提交一个issue。
  • 如果您想要贡献,请提交一个pull request。
  • 如果您对加入阿里巴巴淘宝移动架构团队感兴趣,请将您的简历发送至junzhan

0x6 单元测试

coobjc 包含在 Tests 子目录中的一组单元测试。您可以通过在您要测试的平台框架上执行测试操作来简单地运行这些测试。您可以在 Examples/coobjcBaseExample/coobjcBaseExampleTests 中找到 coobjc 的单元测试。您可以在 cokit/Examples/coKitExamples/coKitExamplesTests 中找到 cokit 的单元测试。

0x7 致谢

coobjc 的存在离不开它们

0x8 作者

0x9 贡献

0xA 许可证

coobjc 在 Apache 2.0 许可证下发布。有关详细信息,请参阅 LICENSE