cocore 1.2.6

cocore 1.2.6

念纪 维护。



cocore 1.2.6

  • 作者
  • pengyutang125

coobjc

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

cooobjc 中文文档

0x0 iOS 异步编程问题

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

  • 进入回调地狱

    简单操作的顺序不自然地嵌套在嵌套的块中。这种“回调地狱”使得难以跟踪正在运行的代码,闭包栈导致了许多二次效应。

  • 处理错误变得困难且复杂

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

  • 忘记调用完成块

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

    这很难量化和评估,但作者认为定义和使用异步 API (使用完成处理器) 的不自然性导致了许多 API 被定义具有显然的同步行为,即使它们可以阻塞。这可能导致 UI 应用程序的性能和响应性问题 - 例如旋转的光标。这也可能导致在异步性对实现规模至关重要的场景下无法使用 API 的定义,例如服务器上。

  • 难以定位的多线程崩溃

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

0x1 解决方案

这些问题在很多系统和很多语言中都已经遇到,协程的抽象是一种处理它们的标准化方法。不过这里不深入理论,简单来说,协程是基本函数的一种扩展,能让函数返回值或挂起。它们可以用来实现生成器、异步模型等能力——关于它们的理论、实现和优化有大量的工作。

Kotlin 是由 JetBrains 支持的一种静态编程语言,它支持现代的多平台应用。在过去两年里,在开发者社区中非常流行。在 Kotlin 语言中,基于协程的 async/await、生成器/yield 等异步技术已经成为语法规范。有关 Kotlin 协程的相关介绍,可以参考:https://www.kotlincn.net/docs/reference/coroutines/basics.html

0x2 协程

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

协程的概念在 1960 年代就已经被提出。它在服务器中得到广泛应用。在高并发场景下极其适用。它可以在单机中极大地减少线程数量,并提高单机的连接和处理能力。同时,iOS 目前不支持协程的使用(这就是我们想要支持它的原因。)

0x3 coobjc 框架

coobjc 是一个可以由阿里巴巴淘宝移动架构团队在 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 功能

异步/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;
    }];
}

以下是在viewDidLoad中调用上述方法的方式

[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;
}

以下是viewDidLoad中使用协程调用接口的地方

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拥有更丰富、更高级的语法支持,coobjc在Swift中更加优雅,例如

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更容易使用,接口少。
    • 易于改造:现有代码只需要几处修改就可以协程化,我们还有大量的系统库协程接口。
  • 清晰
    • 同步写异步逻辑:以同步顺序写代码是人类最能够接受的写法,可以大大减少出错的可能性。
    • 高可读性:协程模式写出来的代码比块嵌套代码更易于阅读。
  • 高性能
    • 调度性能更快:协程本身不需要在内核线程之间切换,调度性能快,即使创建 tens of thousands of 个协程,也不会有压力。
    • 减少应用阻塞:协程的使用有助于减少对锁和信号量的滥用,并通过封装造成阻塞的 IO 协程接口等,从根本上减少停滞和拥堵,从而提高应用程序的整体性能。

0x5 通讯

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

0x6 单元测试

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

coobjc的存在离不开以下项目

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