
该库为Objective-C和Swift提供了Coroutine支持。我们添加了等待方法,发电机和Actor模型,例如C#javaScript和Kotlin。为了方便起见,我们在COKIT框架中为某些基础和Uikit API添加了Coroutine类别,例如NSFileManager,JSON,NSDATA,UIIMAGE等。我们还在CooBJC中添加了元组支持。
Cooobjc中文文档
基于块的异步编程回调目前是iOS的最广泛使用的异步编程方法。 iOS系统提供的GCD库使异步开发非常简单,但基于此编程方法有很多缺点:
进入回调地狱
简单操作的序列在嵌套块中不自然地组成。这个“回调地狱”使得很难跟踪正在运行的代码,并且堆叠封闭会带来许多二阶效果。
处理错误变得困难,非常详细
有条件执行很难且容易出错
忘了致电完成块
因为完成处理程序很尴尬,所以同步定义了太多的API
这很难量化,但是作者认为,定义和使用异步API(使用完成处理程序)的尴尬性已导致许多API显然可以同步行为,即使它们可以阻止它们。这可能会导致UI应用中的性能和响应性问题,例如旋转光标。它还可以导致API的定义,当异步对于达到比例(例如在服务器上)至关重要时,无法使用。
难以找到的多线程崩溃
锁和信号量滥用是由阻塞造成的
这些问题已经在许多系统和许多语言中都面临,而Coroutines的抽象是解决这些问题的标准方法。如果没有太多的理论深入研究,则珊瑚酸是允许函数返回值或被暂停的基本功能的扩展。它们可用于实施发电机,异步模型和其他功能 - 关于其理论,实施和优化,有大量工作。
Kotlin是一种静态编程语言,由支持现代多平台应用程序的Jetbrains支持。在过去的两年中,开发人员社区一直很热。 In the Kotlin language, async/await based on coroutine, generator/yield and other asynchronous technologies have become syntactic standard, Kotlin coroutine related introduction, you can refer to:https://www.kotlincn.net/docs/reference/coroutines/basics.html
Coroutines是计算机程序组件,可以通过允许暂停和恢复执行来概括非首次多任务处理。 Coroutines非常适合实施熟悉的程序组件,例如合作任务,例外,事件循环,迭代器,无限列表和管道
1960年代提出了Coroutine的概念。它在服务器中广泛使用。它非常适合在高并发方案中使用。它可以大大减少单台计算机中的线程数量,并提高单台计算机的连接和处理功能。同时,iOS当前不支持Coroutines的使用(这就是为什么我们要支持它。)
COOBJC是一个Coroutine开发框架,可以在阿里巴巴淘ou-Mobile Architecture团队中使用iOS。目前,它支持Objective-C和Swift的使用。我们使用组装和C语言进行开发,上层提供了Objective-C和Swift之间的接口。目前,它是Apache开源许可证的开源。
使用co_launch方法创建一个Coroutine
co_launch (^{
...
});co_launch创建的coroutine默认情况下是在当前线程中的。
在Coroutine中,我们使用等待的方法等待异步方法执行,获取异步执行结果
- ( 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两次的代码转换为顺序执行,并且代码更简洁。
在Coroutine中,我们所有的方法都直接返回该值,并且未返回错误。我们在执行过程中的错误是由Co_getError()获得的。例如,我们有以下接口来从网络中获取数据。当承诺会拒绝时:错误
- (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;
}然后,我们可以在Coroutine中使用该方法:
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 ++;
}
});在其他Coroutines中,我们可以调用下一个方法以获取生成器中的数据。
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
}
});演员的发送方法可以向演员发送消息
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 )];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 " );您可以将任何值存储在元组中
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 " );
});使用元组,您可以从等待返回中获得多个值
让我们以GCDFetchFeed开源项目中的Feeds流更新代码为示例,以说明Coroutine的实际使用情况和优势。以下是不使用Coroutine的原始实现:
- (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 ];
}];上面的代码在可读性和简单性方面相对较差。让我们在使用Coroutine转换后查看代码:
- (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中使用Coroutine调用接口的地方:
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 ];
});Coroutine转换后的代码变得更容易理解,并且易于错误。
CooBJC通过顶级封装完全支持Swift,使我们能够在Swift中提前享受Coroutine。由于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 ) " )
}
}
} COOBJC在测试子目录中包括一套单位测试。可以在您要测试的平台框架上执行这些测试的测试操作。您可以在示例/coobjcbaseexample/coobjcbaseexampletest中找到COOBJC的单元测试。您可以在Cokit/示例/cokitexamples/cokitexamplests中找到Cokit的单元测试。
COOBJC不能没有:
COOBJC在Apache 2.0许可下发布。有关详细信息,请参见许可证。