
該庫為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許可下發布。有關詳細信息,請參見許可證。