Comment éviter de capturer soi-même dans les blocs lors de l'implémentation d'une API?

J'ai une application qui fonctionne et je travaille sur la conversion en ARC dans Xcode 4.2. L'un des avertissements de pré-vérification consiste à capturer self fortement dans un bloc menant à un cycle de rétention. J'ai fait un simple exemple de code pour illustrer le problème. Je crois comprendre ce que cela signifie mais je ne suis pas sûr de la manière «correcte» ou recommandée de mettre en œuvre ce type de scénario.

  • self est une instance de la class MyAPI
  • le code ci-dessous est simplifié pour ne montrer que les interactions avec les objects et les blocs pertinents à ma question
  • supposons que MyAPI obtient des données d'une source distante et MyDataProcessor fonctionne sur ces données et produit une sortie
  • le processeur est configuré avec des blocs pour communiquer la progression et l'état

échantillon de code:

 // code sample self.delegate = aDelegate; self.dataProcessor = [[MyDataProcessor alloc] init]; self.dataProcessor.progress = ^(CGFloat percentComplete) { [self.delegate myAPI:self isProcessingWithProgress:percentComplete]; }; self.dataProcessor.completion = ^{ [self.delegate myAPIDidFinish:self]; self.dataProcessor = nil; }; // start the processor - processing happens asynchronously and the processor is released in the completion block [self.dataProcessor startProcessing]; 

Question: qu'est-ce que je fais de "mal" et / ou comment cela devrait-il être modifié pour se conformer aux conventions de l'ARC?

    Réponse courte

    Au lieu d'accéder directement à vous-même, vous devez y accéder indirectement, à partir d'une reference qui ne sera pas conservée. Si vous n'utilisez pas le comptage automatique des references (ARC , Automatic Reference Counting) , vous pouvez le faire:

     __block MyDataProcessor *dp = self; self.progressBlock = ^(CGFloat percentComplete) { [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete]; } 

    Le mot-key __block marque les variables qui peuvent être modifiées à l'intérieur du bloc (ce que nous ne faisons pas) mais elles ne sont pas automatiquement conservées lorsque le bloc est conservé (sauf si vous utilisez ARC). Si vous faites cela, vous devez être sûr que rien d'autre ne va essayer d'exécuter le bloc après la publication de l'instance MyDataProcessor. (Compte tenu de la structure de votre code, cela ne devrait pas poser de problème.) En savoir plus sur __block .

    Si vous utilisez ARC , la sémantique de __block change et la reference sera conservée, auquel cas vous devriez la déclarer __weak place.

    Longue réponse

    Disons que vous avez un code comme celui-ci:

     self.progressBlock = ^(CGFloat percentComplete) { [self.delegate processingWithProgress:percentComplete]; } 

    Le problème ici est que le soi garde une reference au bloc; pendant ce time, le bloc doit conserver une reference à soi pour récupérer sa propriété de délégué et envoyer une méthode au délégué. Si tout le rest de votre application libère sa reference à cet object, son count de retenue ne sera pas nul (parce que le bloc pointe dessus) et le bloc ne fait rien de mal (parce que l'object pointe vers lui) et ainsi la paire d'objects va couler dans le tas, occupant la memory mais toujours inaccessible sans débogueur. Tragique, vraiment.

    Ce cas pourrait être facilement résolu en faisant cela à la place:

     id progressDelegate = self.delegate; self.progressBlock = ^(CGFloat percentComplete) { [progressDelegate processingWithProgress:percentComplete]; } 

    Dans ce code, le self retient le bloc, le bloc retient le délégué, et il n'y a pas de cycles (visibles d'ici, le délégué peut retenir notre object mais c'est hors de nos mains en ce moment). Ce code ne risque pas une fuite de la même manière, car la valeur de la propriété delegate est capturée lors de la création du bloc, au lieu de la searchr lors de son exécution. Un effet secondaire est que, si vous modifiez le délégué après la création de ce bloc, le bloc enverra toujours des messages de mise à jour à l'ancien délégué. Que cela se produise ou non dépend de votre application.

    Même si vous étiez cool avec ce comportement, vous ne pouvez toujours pas utiliser cette astuce dans votre cas:

     self.dataProcessor.progress = ^(CGFloat percentComplete) { [self.delegate myAPI:self isProcessingWithProgress:percentComplete]; }; 

    Ici, vous vous passez directement au délégué dans l'appel de la méthode, vous devez donc l'insert quelque part. Si vous avez le contrôle sur la définition du type de bloc, la meilleure chose serait de passer le délégué dans le bloc en tant que paramètre:

     self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) { [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete]; }; 

    Cette solution évite le cycle de rétention et appelle toujours le délégué en cours.

    Si vous ne pouvez pas changer le bloc, vous pouvez le gérer . La raison pour laquelle un cycle de rétention est un avertissement, et non une erreur, est qu'ils n'équivalent pas nécessairement à votre request. Si MyDataProcessor est capable de libérer les blocs lorsque l'opération est terminée, avant que son parent essaie de le libérer, le cycle sera rompu et tout sera nettoyé correctement. Si vous pouviez être sûr de cela, alors la bonne chose à faire serait d'utiliser un #pragma pour supprimer les avertissements pour ce bloc de code. (Ou utilisez un indicateur de compilateur par file, mais ne désactivez pas l'avertissement pour l'set du projet.)

    Vous pouvez également utiliser une astuce similaire ci-dessus, en déclarant une reference faible ou non retenue et en l'utilisant dans le bloc. Par exemple:

     __weak MyDataProcessor *dp = self; // OK for iOS 5 only __unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up __block MyDataProcessor *dp = self; // OK if you aren't using ARC self.progressBlock = ^(CGFloat percentComplete) { [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete]; } 

    Tous les trois ci-dessus vous donneront une reference sans retenir le résultat, bien qu'ils se comportent tous un peu différemment: __weak essayera de __weak à zéro la reference quand l'object est libéré; __unsafe_unretained vous laissera avec un pointeur invalide; __block appenda un autre niveau d'indirection et vous permettra de changer la valeur de la reference depuis le bloc (ce qui n'est pas pertinent dans ce cas puisque dp n'est utilisé nulle part ailleurs).

    Le mieux dépendra du code que vous pouvez changer et de ce que vous ne pouvez pas. Mais j'espère que cela vous a donné quelques idées sur la façon de procéder.

    Il y a aussi la possibilité de supprimer l'avertissement lorsque vous êtes certain que le cycle se cassera à l'avenir:

     #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-retain-cycles" self.progressBlock = ^(CGFloat percentComplete) { [self.delegate processingWithProgress:percentComplete]; } #pragma clang diagnostic pop 

    De cette façon, vous n'avez pas à faire le singe avec __weak , self aliasing et le préfixe ivar explicite.

    Pour une solution commune, j'ai ces définir dans l'en-tête de précompilation. Evite la capture et permet toujours l'aide du compilateur en évitant d'utiliser l' id

     #define BlockWeakObject(o) __typeof(o) __weak #define BlockWeakSelf BlockWeakObject(self) 

    Ensuite, dans le code, vous pouvez faire:

     BlockWeakSelf weakSelf = self; self.dataProcessor.completion = ^{ [weakSelf.delegate myAPIDidFinish:weakSelf]; weakSelf.dataProcessor = nil; }; 

    Je crois que la solution sans ARC fonctionne également avec ARC, en utilisant le mot-key __block :

    EDIT: Selon les notes de publication de transition vers ARC , un object déclaré avec __block est toujours conservé. Utilisez __weak (préféré) ou __unsafe_unretained (pour la rétrocompatibilité).

     // code sample self.delegate = aDelegate; self.dataProcessor = [[MyDataProcessor alloc] init]; // Use this inside blocks __block id myself = self; self.dataProcessor.progress = ^(CGFloat percentComplete) { [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete]; }; self.dataProcessor.completion = ^{ [myself.delegate myAPIDidFinish:myself]; myself.dataProcessor = nil; }; // start the processor - processing happens asynchronously and the processor is released in the completion block [self.dataProcessor startProcessing]; 

    En combinant quelques autres réponses, voici ce que j'utilise maintenant pour un self faible typé à utiliser dans les blocs:

     __typeof(self) __weak welf = self; 

    J'ai défini cela comme un extrait de code XCode avec un préfixe d'achèvement de "welf" dans les methods / fonctions, qui frappe après avoir tapé seulement "nous".

    warning => "capturer soi-même à l'intérieur du bloc est susceptible de mener un cycle de rétention"

    lorsque vous vous référez à soi-même ou à sa propriété à l'intérieur d'un bloc qui est fortement retenu par soi-même qu'il ne l'indique ci-dessus.

    donc pour l'éviter, nous devons faire une semaine ref

     __weak typeof(self) weakSelf = self; 

    donc au lieu d'utiliser

     blockname=^{ self.PROPERTY =something; } 

    nous devrions utiliser

     blockname=^{ weakSelf.PROPERTY =something; } 

    Remarque: le cycle de retenue se produit généralement lorsque deux objects se référant l'un à l'autre par lequel les deux ont la reference count = 1 et leur méthode delloc n'est jamais appelée.

    Si vous êtes sûr que votre code ne créera pas de cycle de conservation, ou que le cycle sera interrompu plus tard, le moyen le plus simple de faire taire l'avertissement est:

     // code sample self.delegate = aDelegate; self.dataProcessor = [[MyDataProcessor alloc] init]; [self dataProcessor].progress = ^(CGFloat percentComplete) { [self.delegate myAPI:self isProcessingWithProgress:percentComplete]; }; [self dataProcessor].completion = ^{ [self.delegate myAPIDidFinish:self]; self.dataProcessor = nil; }; // start the processor - processing happens asynchronously and the processor is released in the completion block [self.dataProcessor startProcessing]; 

    La raison pour laquelle cela fonctionne est que si l'access par points des propriétés est pris en count par l'parsing de Xcode, et donc

     xyz = ^{ block that retains x} 

    est considéré comme ayant une retenue par x de y (sur le côté gauche de l'affectation) et par y de x (sur le côté droit), les appels de methods ne sont pas soumis à la même parsing, même s'il s'agit d'appels de méthode qui sont équivalentes à dot-access, même lorsque ces methods d'access aux propriétés sont générées par le compilateur, donc dans

     [xy].z = ^{ block that retains x} 

    seul le côté droit est considéré comme créant une retenue (par y de x), et aucun avertissement de cycle de rétention n'est généré.