这个库为 Objective-C 和 Swift 提供了协程支持。我们添加了 await 方法、生成器和 actor 模型,类似于 C#、JavaScript 和 Kotlin。为了方便,我们在 cokit 框架 中为一些 Foundation 和 UIKit API 添加了协程分类,如 NSFileManager、JSON、NSData、UIImage 等。我们还在 coobjc 中添加了元组支持。
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 文档
- 阅读 协程框架设计 文档。
- 阅读 coobjc Objective-C 指南 文档。
- 阅读 coobjc Swift 指南 文档。
- 阅读 cokit 框架 文档,了解如何使用系统接口的封装API。
- 我们为 coobjc 提供了 coobjcBaseExample 演示,为 coswift 提供了 coSwiftDemo,为 cokit 提供了 coKitExamples。
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的存在离不开以下项目
- Promises - Google的Objective-C和Swift承诺框架。
- libtask - 一个简单的协程库。
- movies - 一个iOS演示应用,我们在coobjc示例中使用其代码
- v2ex - v2ex.com的iOS客户端,我们在coobjc示例中使用其代码
- tuples - Objective-C元组。
- fishhook - 重新绑定Mach-O二进制文件中的符号
- Sol - Sol°巧妙地显示天气信息,以便您可以相应地计划您的一天。检查您当前位置或世界上任何城市的天气。使用了Objective-C实现。
- Swift - Swift编程语言
- libdispatch - 用于多核硬件并发性的libdispatch项目(又名Grand Central Dispatch)
- objc4 - apple objc框架
- https://blog.csdn.net/qq910894904/article/details/41911175
- http://www.voidcn.com/article/p-fwlohnnc-gc.html
- https://blog.csdn.net/shenlei19911210/article/details/61194617
coobjc遵守Apache 2.0许可证发布。有关详细信息,请参阅LICENSE。