
Esta biblioteca fornece suporte a corotas para o Objective-C e Swift. Adicionamos o Método Aguardo 、 Modelo de Gerador e Ator, como C#、 JavaScript e Kotlin. Por conveniência, adicionamos categorias de coroutina para algumas API da Fundação e Uikit na estrutura do Cokit, como NSfilemanager, JSON, NSDATA, UIIMAGE etc. Também adicionamos suporte de tupla no COOBJC.
COOOBJC 中文文档
Atualmente, atualmente o retorno de chamada de programação assíncrona baseado em blocos é o método de programação assíncrono mais amplamente utilizado para iOS. A biblioteca GCD fornecida pelo sistema iOS torna o desenvolvimento assíncrono muito simples e conveniente, mas há muitas desvantagens com base nesse método de programação:
Entre no inferno de retorno
A sequência de operações simples é composta não natural nos blocos aninhados. Esse "inferno de retorno de chamada" dificulta o controle do código que está em execução, e a pilha de fechamentos leva a muitos efeitos de segunda ordem.
Erros de manuseio se tornam difíceis e muito detalhados
A execução condicional é difícil e propensa a erros
Esqueça de ligar para o bloco de conclusão
Como os manipuladores de conclusão são estranhos, muitas APIs são definidas de maneira síncrona
Isso é difícil de quantificar, mas os autores acreditam que o constrangimento de definir e usar APIs assíncronas (usando manipuladores de conclusão) levou muitas APIs a serem definidas com comportamento aparentemente síncrono, mesmo quando eles podem bloquear. Isso pode levar a problemas problemáticos de desempenho e capacidade de resposta nos aplicativos da interface do usuário - por exemplo, cursor de rotação. Também pode levar à definição de APIs que não podem ser usadas quando a assincronia é fundamental para alcançar a escala, por exemplo, no servidor.
Acidentes com vários threads que são difíceis de localizar
Bloqueios e abuso de semáforo causados pelo bloqueio
Esses problemas foram enfrentados em muitos sistemas e muitos idiomas, e a abstração das coroutinas é uma maneira padrão de resolvê -los. Sem se aprofundar em teoria, as coroutinas são uma extensão das funções básicas que permitem que uma função retorne um valor ou seja suspensa. Eles podem ser usados para implementar geradores, modelos assíncronos e outros recursos - há um grande corpo de trabalho sobre a teoria, a implementação e a otimização deles.
O Kotlin é uma linguagem de programação estática suportada pela JetBrains que suporta aplicativos modernos de várias plataformas. Está bastante quente na comunidade de desenvolvedores nos últimos dois anos. 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 são componentes do programa de computador que generalizam as sub-rotinas para multitarefa não preventivas, permitindo que a execução seja suspensa e retomada. Coroutines são adequados para implementar componentes familiares de programa, como tarefas cooperativas, exceções, loops de eventos, iteradores, listas infinitas e tubos
O conceito de coroutina foi proposto na década de 1960. É amplamente utilizado no servidor. É extremamente adequado para uso em cenários de alta concorrência. Pode reduzir bastante o número de roscas em uma única máquina e melhorar os recursos de conexão e processamento de uma única máquina. Enquanto isso, o iOS atualmente não suporta o uso de coroutinas (É por isso que queremos apoiá -lo.)
O COOBJC é uma estrutura de desenvolvimento de coroutina que pode ser usada no iOS pela equipe de arquitetura do Alibaba Taobao-Mobile. Atualmente, ele suporta o uso do Objective-C e Swift. Utilizamos a linguagem Assembly e C para desenvolvimento, e a camada superior fornece a interface entre o Objective-C e o Swift. Atualmente, é de código aberto aqui na licença de código aberto Apache.
Crie uma coroutina usando o método co_launch
co_launch (^{
...
});A coroutina criada por co_launch está agendada por padrão no thread atual.
Na coroutina, usamos o método aguardando para esperar o método assíncrono executar, obtenha o resultado da execução assíncrona
- ( 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;
});
}O código acima transforma o código que originalmente precisa despacho_async duas vezes na execução seqüencial, e o código é mais conciso.
Na coroutina, todos os nossos métodos estão retornando diretamente o valor e nenhum erro é retornado. Nosso erro no processo de execução é obtido por co_getError (). Por exemplo, temos a seguinte interface para obter dados da rede. Quando a promessa será rejeitada: erro
- (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;
}Então podemos usar o método na coroutina:
co_launch (^{
id response = await ([ self co_GET: feedModel.feedUrl parameters: nil ]);
if ( co_getError ()){
// handle error message
}
...
});Usamos co_sequence para criar o gerador
COCoroutine *co1 = co_sequence(^{
int index = 0 ;
while ( co_isActive ()){
yield_val (@( index ));
index ++;
}
});Em outras coroutinas, podemos chamar o próximo método para obter os dados no gerador.
co_launch (^{
for ( int i = 0 ; i < 10 ; i++){
val = [[co1 next ] intValue ];
}
});O gerador pode ser usado em muitos cenários, como filas de mensagens, arquivos de download em lote, caches em massa de carga, etc.:
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 ]);
}
}
});Através do gerador, podemos carregar os dados do produtor tradicional-notificando o modelo do consumidor, transformando o consumidor em dados-> dizendo ao produtor para carregar o padrão, evitando a necessidade de usar muitas variáveis compartilhadas para o estado em computação multitrinada. A sincronização elimina o uso de bloqueios em certos cenários.
O conceito de ator vem de Erlang. Em Akka, um ator pode ser considerado um recipiente para armazenar estado, comportamento, caixa de correio e políticas de ator e supervisor infantil. Os atores não se comunicam diretamente, mas usam o correio para se comunicar.
Podemos usar co_actor_onqueue para criar um ator no thread especificado.
COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
... // Define the state variable of the actor
for (COActorMessage *message in channel){
... // handle message
}
});O método de envio do ator pode enviar uma mensagem ao ator
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 " );você pode armazenar qualquer valor em tupla
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));
}
}];
}Então você pode buscar o valor como este:
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 " );
});Use tupla, você pode obter vários valores do Aguardar Return
Vamos pegar o código da atualização do fluxo de feeds no projeto de código aberto do GCDFetchFeed como um exemplo para demonstrar os cenários de uso reais e vantagens da coroutina. A seguir, é apresentada a implementação original de não usar a 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 ;
}];
}A seguir, a chamada para o método acima no 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 ];
}];O código acima é relativamente ruim em termos de legibilidade e simplicidade. Vamos dar uma olhada no código depois de usar a transformação da 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 ;
}Aqui está o local no ViewDidload que usa a coroutina para chamar a interface:
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 ];
});O código após a transformação da Coroutine tornou-se mais fácil de entender e menos propenso a erros.
O COOBJC suporta totalmente o Swift através do encapsulamento de nível superior, permitindo-nos aproveitar a coroutina antes do tempo em Swift. Como Swift tem suporte de sintaxe mais rico e avançado, o COOBJC é mais elegante em Swift, por exemplo:
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 ) " )
}
}
} O COOBJC inclui um conjunto de testes de unidade no subdiretório de testes. Esses testes podem ser executados simplesmente executados a ação de teste na estrutura da plataforma que você gostaria de testar. Você pode encontrar testes de unidade do CoOBJC em exemplos/CoobjcBaseExample/coobjcbaseexampletests. Você pode encontrar testes de unidade do Cokit em cokit/exemplos/cokitexamples/cokitexamplestests.
Coobjc não poderia existir sem:
O COOBJC é liberado sob a licença Apache 2.0. Consulte a licença para obter detalhes.