这个库为 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 语言中,基于协程的异步/await、生成器/yield 等异步技术已经成为语法标准,关于 Kotlin 协程的介绍,您可以参考:https://www.kotlincn.net/docs/reference/coroutines/basics.html
0x2 协程
协程是计算机程序组件,通过对执行进行挂起和恢复来通用子程序,以实现非抢占式多任务处理。协程非常适合实现合作任务、异常、事件循环、迭代器、无限列表和管道等熟悉的程序组件
协程的概念在 20 世纪 60 年代被提出。它在服务器中得到广泛应用。非常适合在高并发场景中使用。它可以大大减少单台机器上的线程数量,并提高单台机器的连接和处理能力。与此同时,iOS 当前不支持协程的使用(这就是我们想要支持它的原因。 )
0x3 coobjc 框架
coobjc 是一个协程开发框架,由阿里巴巴淘宝-移动架构团队用于 iOS。目前它支持 Objective-C 和 Swift 语言。我们使用汇编和 C 语言进行开发,上层提供 Objective-C 与 Swift 之间的接口。目前,它以 Apache 开源许可证的形式开源。
0x31 安装
- 针对 Objective-C 的 cocoaPods: pod 'coobjc'
- 针对 Swift 的 cocoaPods: pod 'coswift'
- 针对 Cokit 的 cocoaPods: pod 'cokit'
- 源代码:所有代码都在 ./coobjc 目录下
0x32 文档
- 阅读 协程框架设计 文档。
- 阅读 coobjc Objective-C 指引 文档。
- 阅读 coobjc Swift 指引 文档。
- 阅读 cokit 框架 文档,学习如何使用系统接口的包装 API。
- 我们为 coobjc 提供了 coobjcBaseExample 示例,为 coswift 提供了 coSwiftDemo 示例,为 cokit 提供了 coKitExamples 示例。
0x33 功能
async/await
- 创建协程
使用 co_launch 方法创建协程
co_launch(^{
...
});
由 co_launch 创建的协程默认在当前线程中进行调度。
- 等待异步方法执行
在协程中,我们使用 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]);
}
}
});
通过生成器,我们可以将从传统的生产者-通知消费者模式加载数据,改为消费者告诉生产者加载数据的模式,从而避免在多线程计算中需要使用许多共享变量来存储状态。在特定场景下可以消除同步机制,避免使用锁。
Actor
Actor 的概念来自 Erlang。在 AKKA 中,Actor 可以被视为存储状态、行为、邮箱以及子 Actor 和监护人策略的容器。Actor 之间不进行直接通信,而是使用邮箱进行通信。
- 创建 Actor
我们可以使用 co_actor_onqueue 在指定的线程中创建 Actor。
COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
... //Define the state variable of the actor
for(COActorMessage *message in channel){
...//handle message
}
});
- 向 Actor 发送消息
演员的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返回值中获取多个值
Actual case using 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更易用,接口少
- 易于升级:现有代码只需进行少量更改即可协程化,我们系统库有大量协程接口
- 清晰
- 同步写异步逻辑:对于人类来说,代码以同步顺序方式编写是最可接受的方式,这可以大大降低出错的可能性。
- 高可读性:以协程模式编写的代码比块嵌套代码可读性要高得多。
- 高性能
- 更快的调度性能:协程本身不需要在内核级线程之间切换,调度性能快,即使创建了成千上万的协程,也没有压力。
- 减少应用程序阻塞:使用协程帮助减少锁和信号量的滥用,通过封装导致阻塞的协程接口(如IO)等来减少由根本原因引起的僵死和拥堵的数量,从而提高应用程序的整体性能。
0x5 通信
- 如果您需要帮助,请使用 Stack Overflow。 (使用标签 'coobjc')
- 如果您想提出一个一般性的问题,请使用 Stack Overflow。
- 如果您发现了错误,并且能提供可靠重现它的步骤,请打开一个问题。
- 如果您有功能请求,请打开一个问题。
- 如果您想做出贡献,请提交一个拉取请求。
- 如果您对加入阿里巴巴淘宝移动架构团队感兴趣,请将您的简历发送至 junzhan
0x6 单元测试
coobjc 包含了位于 Tests 子目录中的一系列单元测试。您可以通过执行想要测试的平台框架上的测试操作来简单地运行这些测试。您可以在 Examples/coobjcBaseExample/coobjcBaseExampleTests 中找到 coobjc 的单元测试。您可以在 cokit/Examples/coKitExamples/coKitExamplesTests 中找到 cokit 的单元测试。
0x7 致谢
没有以下内容,coobjc 将无法存在
- Promises - Google 的 Objective-C 和 Swift Promises 框架。
- 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 Objective-C 框架
- 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
0x8 作者
0x9 贡献
0xA 许可证
coobjc遵循Apache 2.0许可证发布。有关详细信息,请参阅LICENSE。