
Esta biblioteca proporciona soporte de coroutine para Objective-C y Swift. Agregamos el método esperado 、 Modelo de generador y actor como C#、 JavaScript y Kotlin. Por conveniencia, agregamos categorías de coroutine para algunos fundamentos y API UIKIT en el marco Cokit como NSFilemanager, JSON, NSDATA, UIIMAGE, etc. También agregamos soporte de TUPLE en CoOBJC.
cooobjc 中文文档
La devolución de llamada de programación asíncrona basada en bloques es actualmente el método de programación asíncrono más utilizado para iOS. La biblioteca GCD proporcionada por el sistema iOS hace que el desarrollo asíncrono sea muy simple y conveniente, pero hay muchas desventajas basadas en este método de programación:
Entra en el infierno de devolución de llamada
La secuencia de operaciones simples está compuesta antinaturalmente en los bloques anidados. Este "infierno de devolución de llamada" hace que sea difícil realizar un seguimiento del código que se está ejecutando, y la pila de cierres conduce a muchos efectos de segundo orden.
El manejo de los errores se vuelve difícil y muy detallado
La ejecución condicional es dura y propensa a errores
Olvídate de llamar al bloque de finalización
Debido a que los manejadores de finalización son incómodos, demasiadas API se definen sincrónicamente
Esto es difícil de cuantificar, pero los autores creen que la incomodidad de definir y usar API asíncronas (usando controladores de finalización) ha llevado a que muchas API se definan con un comportamiento aparentemente sincrónico, incluso cuando pueden bloquear. Esto puede conducir a problemas problemáticos de rendimiento y capacidad de respuesta en las aplicaciones de la interfaz de usuario, por ejemplo, el cursor de giro. También puede conducir a la definición de API que no se pueden usar cuando la asincronía es fundamental para lograr la escala, por ejemplo, en el servidor.
Crashes múltiples que son difíciles de localizar
Bloqueos y abuso semáforo causado por el bloqueo
Estos problemas se han enfrentado en muchos sistemas y muchos idiomas, y la abstracción de las coroutinas es una forma estándar de abordarlos. Sin profundizar demasiado en la teoría, las coroutinas son una extensión de las funciones básicas que permiten que una función devuelva un valor o se suspenda. Se pueden utilizar para implementar generadores, modelos asincrónicos y otras capacidades: hay un gran trabajo sobre la teoría, la implementación y la optimización de ellos.
Kotlin es un lenguaje de programación estática compatible con JetBrains que admite aplicaciones multiplataformas modernas. Ha estado bastante caliente en la comunidad de desarrolladores durante los últimos dos años. En el lenguaje Kotlin, Async/Wait basado en Coroutine, Generador/rendimiento y otras tecnologías asíncronas se han convertido en un estándar sintáctico, Introducción relacionada con la coroutina de Kotlin, puede consultar: https: //www.kotlincn.net/docs/reference/coroutines/basics.html
Las corutinas son componentes del programa de computadora que generalizan las subrutinas para la multitarea no preventiva, al permitir que la ejecución se suspenda y reanude. Las corutinas son adecuadas para implementar componentes familiares del programa, como tareas cooperativas, excepciones, bucles de eventos, iteradores, listas infinitas y tuberías
El concepto de coroutina se ha propuesto en la década de 1960. Se usa ampliamente en el servidor. Es extremadamente adecuado para su uso en escenarios de alta concurrencia. Puede reducir en gran medida la cantidad de hilos en una sola máquina y mejorar las capacidades de conexión y procesamiento de una sola máquina. Mientras tanto, iOS actualmente no admite el uso de coroutinas (es por eso que queremos apoyarlo.)
COOBJC es un marco de desarrollo de Coroutine que puede ser utilizado en el equipo de arquitectura Alibaba Taobao-Mobile. Actualmente admite el uso de Objective-C y Swift. Utilizamos el montaje y el lenguaje C para el desarrollo, y la capa superior proporciona la interfaz entre Objective-C y Swift. Actualmente, es código abierto aquí bajo la licencia de código abierto Apache.
Crear una coroutina utilizando el método CO_LAUNCH
co_launch (^{
...
});La coroutina creada por CO_LAUNCH está programada de forma predeterminada en el hilo actual.
En el Coroutine usamos el método de espera para esperar a que se ejecute el método asincrónico, obtenga el resultado de la ejecución así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;
});
}El código anterior convierte el código que originalmente necesita Dispatk_async dos veces en una ejecución secuencial, y el código es más conciso.
En el Coroutine, todos nuestros métodos están devolviendo directamente el valor y no se devuelve ningún error. Nuestro error en el proceso de ejecución se obtiene por CO_GetError (). Por ejemplo, tenemos la siguiente interfaz para obtener datos de la red. Cuando la promesa rechazará: 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;
}Entonces podemos usar el método en el Coroutine:
co_launch (^{
id response = await ([ self co_GET: feedModel.feedUrl parameters: nil ]);
if ( co_getError ()){
// handle error message
}
...
});Usamos CO_SECHENCE para crear el generador
COCoroutine *co1 = co_sequence(^{
int index = 0 ;
while ( co_isActive ()){
yield_val (@( index ));
index ++;
}
});En otras coroutinas, podemos llamar al siguiente método para obtener los datos en el generador.
co_launch (^{
for ( int i = 0 ; i < 10 ; i++){
val = [[co1 next ] intValue ];
}
});El generador se puede usar en muchos escenarios, como colas de mensajes, archivos de descarga de lotes, cachés de carga a granel, 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 ]);
}
}
});A través del generador, podemos cargar los datos del productor tradicional, no lo que no suele el modelo de consumo, convirtiendo al consumidor en los datos, dando al productor que cargue el patrón, evitando la necesidad de usar muchas variables compartidas para el estado en la computación multitregrama. La sincronización elimina el uso de bloqueos en ciertos escenarios.
El concepto de actor proviene de Erlang. En Akka, un actor puede ser considerado como un contenedor para almacenar políticas de estado, comportamiento, buzón y actor infantil y supervisor. Los actores no se comunican directamente, pero usan el correo para comunicarse entre sí.
Podemos usar CO_actor_onqueue para crear un actor en el hilo especificado.
COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
... // Define the state variable of the actor
for (COActorMessage *message in channel){
... // handle message
}
});El método de envío del actor puede enviar un mensaje al actor
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 " );Puedes almacenar cualquier valor en tuple
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));
}
}];
}Entonces puedes obtener el 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 tuple puede obtener múltiples valores de la retorno de espera
Tomemos el código de la actualización de la transmisión de Feeds en el proyecto de código abierto GCDFetchFeed como ejemplo para demostrar los escenarios de uso y ventajas de la coroutina. La siguiente es la implementación original de no usar 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 ;
}];
}La siguiente es la llamada al método anterior en 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 ];
}];El código anterior es relativamente pobre en términos de legibilidad y simplicidad. Echemos un vistazo al código después de usar la transformación de 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 ;
}Aquí está el lugar en ViewDidload que usa la coroutina para llamar a la interfaz:
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 ];
});El código después de la transformación de Coroutine se ha vuelto más fácil de entender y menos propenso a errores.
COOBJC es compatible con Swift a través de la encapsulación de nivel superior, lo que nos permite disfrutar de la coroutina con anticipación en Swift. Debido a que Swift tiene un soporte de sintaxis más rico y avanzado, COOBJC es más elegante en Swift, por ejemplo:
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 incluye un conjunto de pruebas unitarias dentro del subdirectorio de pruebas. Estas pruebas se pueden ejecutar simplemente se ejecutarán la acción de prueba en el marco de la plataforma que desea probar. Puede encontrar las pruebas unitarias de CoOBJC en ejemplos/coobjcbaseExample/coobjcbaseExAmpletests. Puede encontrar las pruebas unitarias de Cokit en Cokit/Ejemplos/Cokitexamples/CokitexAMPLESTESTS.
Coobjc no podría existir sin:
COOBJC se lanza bajo la licencia Apache 2.0. Vea la licencia para más detalles.