
Cette bibliothèque fournit une prise en charge de Coroutine pour Objective-C et Swift. Nous avons ajouté la méthode Await 、 Générateur et modèle d'acteur comme C # 、 JavaScript et Kotlin. Pour plus de commodité, nous avons ajouté des catégories de coroutine pour certaines API de fondation et UIKIT dans le cadre CoKit comme NSfileManager, JSON, NSDATA, UIIMage, etc. Nous ajoutons également le support de tuples dans COOBJC.
COOOBJC 中文文档
Le rappel de programmation asynchrone basé sur des blocs est actuellement la méthode de programmation asynchrone la plus utilisée pour iOS. La bibliothèque GCD fournie par le système iOS rend le développement asynchrone très simple et pratique, mais il existe de nombreux inconvénients basés sur cette méthode de programmation:
entrer dans l'enfer
La séquence d'opérations simples est composée de manière anormale dans les blocs imbriqués. Cet "Hellback Hell" rend difficile le suivi du code qui s'exécute, et la pile de fermetures conduit à de nombreux effets de deuxième ordre.
La gestion des erreurs devient difficile et très verbeuse
L'exécution conditionnelle est difficile et sujette aux erreurs
Oubliez d'appeler le bloc d'achèvement
Parce que les gestionnaires d'achèvement sont maladroits, trop d'API sont définis de manière synchrone
Ceci est difficile à quantifier, mais les auteurs croient que la maladresse de définir et d'utiliser des API asynchrones (utilisant des gestionnaires d'achèvement) a conduit à de nombreuses API définies avec un comportement apparemment synchrone, même lorsqu'ils peuvent bloquer. Cela peut entraîner des problèmes de performance et de réactivité problématiques dans les applications d'interface utilisateur - par exemple le curseur de rotation. Cela peut également conduire à la définition d'API qui ne peut pas être utilisée lorsque l'asynchronie est essentielle pour atteindre l'échelle, par exemple sur le serveur.
Des accidents multiples qui sont difficiles à localiser
Serrures et abus de sémaphore causés par le blocage
Ces problèmes ont été confrontés à de nombreux systèmes et à de nombreuses langues, et l'abstraction des coroutines est un moyen standard de les résoudre. Sans plonger trop dans la théorie, les coroutines sont une extension des fonctions de base qui permettent à une fonction de renvoyer une valeur ou d'être suspendue. Ils peuvent être utilisés pour mettre en œuvre des générateurs, des modèles asynchrones et d'autres capacités - il existe un grand nombre de travaux sur la théorie, la mise en œuvre et l'optimisation de eux.
Kotlin est un langage de programmation statique pris en charge par JetBrains qui prend en charge les applications multiplateformes modernes. Il fait assez chaud dans la communauté des développeurs depuis deux ans. Dans la langue Kotlin, async / Await basé sur la coroutine, le générateur / le rendement et les autres technologies asynchrones sont devenues standard syntaxiques, introduction liée à Kotlin Coroutine, vous pouvez vous référer à: https: //www.kotlincn.net/docs/reference/coroutines/basics.html
Les coroutines sont des composants de programme informatique qui généralisent les sous-programmes pour le multitâche non préemptif, en permettant à l'exécution d'être suspendue et repris. Les coroutines sont bien adaptées à la mise en œuvre de composants de programme familiers tels que des tâches coopératives, des exceptions, des boucles d'événements, des itérateurs, des listes infinies et des tuyaux
Le concept de coroutine a été proposé dans les années 1960. Il est largement utilisé dans le serveur. Il est extrêmement adapté à une utilisation dans des scénarios de concurrence élevés. Il peut réduire considérablement le nombre de threads dans une seule machine et améliorer les capacités de connexion et de traitement d'une seule machine. En attendant, iOS ne soutient actuellement pas l'utilisation des coroutines (C'est pourquoi nous voulons le soutenir.)
COOBJC est un cadre de développement Coroutine qui peut être utilisé sur l'iOS par l'équipe d'architecture Alibaba Taobao-Mobile. Actuellement, il prend en charge l'utilisation de Objective-C et Swift. Nous utilisons l'assemblage et le langage C pour le développement, et la couche supérieure fournit l'interface entre objectif-C et Swift. Actuellement, il est open source ici sous la licence open source Apache.
Créez une coroutine en utilisant la méthode co_launch
co_launch (^{
...
});La coroutine créée par Co_Launch est planifiée par défaut dans le thread actuel.
Dans la coroutine, nous utilisons la méthode Await pour attendre que la méthode asynchrone s'exécute, obtenez le résultat de l'exécution asynchrone
- ( 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;
});
}Le code ci-dessus transforme deux fois le code qui a besoin à l'origine Dispatch_async en exécution séquentielle, et le code est plus concis.
Dans la coroutine, toutes nos méthodes renvoient directement la valeur et aucune erreur n'est renvoyée. Notre erreur dans le processus d'exécution est obtenue par CO_GETERROR (). Par exemple, nous avons l'interface suivante pour obtenir des données du réseau. Lorsque la promesse rejetera: Erreur
- (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;
}Ensuite, nous pouvons utiliser la méthode dans la coroutine:
co_launch (^{
id response = await ([ self co_GET: feedModel.feedUrl parameters: nil ]);
if ( co_getError ()){
// handle error message
}
...
});Nous utilisons Co_Sequence pour créer le générateur
COCoroutine *co1 = co_sequence(^{
int index = 0 ;
while ( co_isActive ()){
yield_val (@( index ));
index ++;
}
});Dans d'autres coroutines, nous pouvons appeler la méthode suivante pour obtenir les données du générateur.
co_launch (^{
for ( int i = 0 ; i < 10 ; i++){
val = [[co1 next ] intValue ];
}
});Le générateur peut être utilisé dans de nombreux scénarios, tels que les files d'attente de messages, les fichiers de téléchargement par lots, les caches de chargement en vrac, 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 ]);
}
}
});Grâce au générateur, nous pouvons charger les données du producteur traditionnel - en notant le modèle de consommation, transformant le consommateur en données -> disant au producteur de charger le modèle, en évitant la nécessité d'utiliser de nombreuses variables partagées pour l'État dans l'informatique multi-thread. La synchronisation élimine l'utilisation de verrous dans certains scénarios.
Le concept d'acteur vient d'Erlang. À Akka, un acteur peut être considéré comme un conteneur pour stocker les politiques de l'État, du comportement, de la boîte aux lettres et de l'enfant acteur et superviseur. Les acteurs ne communiquent pas directement, mais utilisent le courrier pour communiquer entre eux.
Nous pouvons utiliser co_actor_onqueue pour créer un acteur dans le thread spécifié.
COActor *actor = co_actor_onqueue(q, ^(COActorChan *channel) {
... // Define the state variable of the actor
for (COActorMessage *message in channel){
... // handle message
}
});La méthode d'envoi de l'acteur peut envoyer un message à l'acteur
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 " );vous pouvez stocker n'importe quelle valeur 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));
}
}];
}Ensuite, vous pouvez récupérer la valeur comme ceci:
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 " );
});Utiliser Tuple, vous pouvez obtenir plusieurs valeurs en attendant le retour
Prenons le code de la mise à jour du flux de flux dans le projet open source GCDFetchFeed comme exemple pour démontrer les scénarios d'utilisation réels et les avantages de la coroutine. Ce qui suit est la mise en œuvre d'origine de ne pas utiliser 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 ;
}];
}Ce qui suit est l'appel à la méthode ci-dessus dans 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 ];
}];Le code ci-dessus est relativement médiocre en termes de lisibilité et de simplicité. Jetons un coup d'œil au code après avoir utilisé la transformation de la 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 ;
}Voici l'endroit dans ViewDidload qui utilise la coroutine pour appeler l'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 ];
});Le code après la transformation de la coroutine est devenu plus facile à comprendre et moins sujet aux erreurs.
COOBJC prend en charge Swift à travers l'encapsulation de haut niveau, nous permettant de profiter de la coroutine à l'avance à Swift. Parce que Swift a un support de syntaxe plus riche et plus avancé, COOBJC est plus élégant dans Swift, par exemple:
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 comprend une suite de tests unitaires dans le sous-répertoire des tests. Ces tests peuvent être exécutés simplement à exécuter l'action de test sur le cadre de la plate-forme que vous souhaitez tester. Vous pouvez trouver les tests unitaires de COOBJC dans des exemples / coobjcbaseexample / coobjcbaseExamplests. Vous pouvez trouver des tests unitaires de Cokit dans CoKit / Exemples / Cokitexamples / CokiteXamplestes.
Coobjc ne pourrait pas exister sans:
COOBJC est publié sous la licence Apache 2.0. Voir la licence pour plus de détails.