coobjc 1.2.0

coobjc 1.2.0

念纪维护。



coobjc 1.2.0

  • 作者
  • pengyutang125

coobjc

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

cooobjc 中文文档

0x0 iOS 异步编程问题

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

  • 进入回调地狱

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

  • 处理错误变得困难且啰嗦

  • 条件执行很困难且容易出错

  • 忘记调用完成块

  • 由于完成处理器的笨拙,太多的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 是一个可用于 iOS 的协程开发框架,由阿里巴巴淘宝 mobile 架构团队开发。目前它支持 Objective-C 和 Swift 的使用。我们使用汇编和 C 语言进行开发,而上层提供 Objective-C 和 Swift 之间的接口。目前,它在 Apache 开源许可证下开源。

0x31 安装

  • cocoapods for objective-c:  pod 'coobjc'
  • cocoapods for swift: pod 'coswift'
  • cocoapods for 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");
  • 在协程中使用元组,首先创建一个解析元组值的promise。
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 stream更新代码为例,来展示协程的实际使用场景和优势。以下是不使用协程的原生实现:

- (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,使我们能提前 enjoy 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更容易使用,接口更少
    • 易于迁移:现有代码只需做少量更改即可使用协程,我们为系统库提供了大量的协程接口。
  • 清晰
    • 同步写异步逻辑:以同步的方法编写代码是人类最容易接受的方法,可以大大降低出错的可能性
    • 高可读性:用协程模式编写的代码比嵌套块代码可读性更高
  • 高性能
    • 更快地调度性能:协程本身不需要在内核级线程之间切换,调度性能快,即使创建了成千上万的协程也不会有压力。
    • 减少应用阻塞:使用协程可以帮助减少锁和信号量的滥用,并通过封装导致阻塞的IO等协程接口从根源上减少停顿和拥堵,从而提高应用的整体性能。

0x5 通信

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

0x6 单元测试

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

0x7 赞助者

coobjc 的存在离不开

0x8 作者

0x9 贡献

0x10 许可证

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