Comment éviter de se capturer en blocs lors de la mise en œuvre d'une API?

222

J'ai une application qui fonctionne et je travaille à la convertir en ARC dans Xcode 4.2. L'un des avertissements de pré-vérification implique une capture selfforte dans un bloc conduisant à un cycle de rétention. J'ai créé un exemple de code simple 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 classe MyAPI
  • le code ci-dessous est simplifié pour ne montrer que les interactions avec les objets et blocs pertinents à ma question
  • supposons que MyAPI obtient les 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

exemple 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: que fais-je "mal" et / ou comment cela devrait-il être modifié pour se conformer aux conventions de l'ARC?

XJones
la source

Réponses:

509

Réponse courte

Au lieu d'accéder selfdirectement, vous devez y accéder indirectement, à partir d'une référence qui ne sera pas conservée. Si vous n'utilisez pas le comptage de référence automatique (ARC) , vous pouvez le faire:

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

Le __blockmot-clé marque les variables qui peuvent être modifiées à l'intérieur du bloc (nous ne le faisons pas), mais elles ne sont pas non plus 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 libération de l'instance MyDataProcessor. (Compte tenu de la structure de votre code, cela ne devrait pas poser de problème.) En savoir plus__block .

Si vous utilisez ARC , la sémantique des __blockmodifications et la référence seront conservées, auquel cas vous devez la déclarer à la __weakplace.

Longue réponse

Disons que vous aviez un code comme celui-ci:

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

Le problème ici est que self conserve une référence au bloc; pendant ce temps, le bloc doit conserver une référence à self afin de récupérer sa propriété de délégué et d'envoyer au délégué une méthode. Si tout le reste de votre application libère sa référence à cet objet, son nombre de retenues ne sera pas nul (car le bloc le pointe) et le bloc ne fait rien de mal (parce que l'objet le pointe) et ainsi la paire d'objets s'infiltrera dans le tas, occupant de la mémoire mais toujours inaccessible sans débogueur. Tragique, vraiment.

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

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

Dans ce code, self retient le bloc, le bloc retient le délégué, et il n'y a pas de cycle (visible d'ici; le délégué peut conserver notre objet mais cela 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é déléguée est capturée lors de la création du bloc, au lieu d'être recherchée 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é. La probabilité 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 passez selfdirectement au délégué dans l'appel de méthode, vous devez donc le faire 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 conservation et appelle toujours le délégué actuel.

Si vous ne pouvez pas modifier le bloc, vous pouvez y faire face . La raison pour laquelle un cycle de rétention est un avertissement, et non une erreur, est qu'il n'épelle pas nécessairement le destin de votre application. Si MyDataProcessorest capable de libérer les blocs lorsque l'opération est terminée, avant que son parent n'essaye de le libérer, le cycle sera interrompu et tout sera nettoyé correctement. Si vous pouvez en être sûr, la bonne chose à faire serait d'utiliser un #pragmapour supprimer les avertissements pour ce bloc de code. (Ou utilisez un indicateur de compilation par fichier. Mais ne désactivez pas l'avertissement pour l'ensemble du projet.)

Vous pouvez également chercher à utiliser une astuce similaire ci-dessus, déclarer une référence faible ou non retenue et l'utiliser 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];
}

Les trois éléments ci-dessus vous donneront une référence sans conserver le résultat, bien qu'ils se comportent tous un peu différemment: __weaktentera de mettre à zéro la référence lorsque l'objet est relâché; __unsafe_unretainedvous laissera un pointeur invalide; __blockajoutera en fait un autre niveau d'indirection et vous permettra de modifier la valeur de la référence depuis l'intérieur du bloc (non pertinent dans ce cas, car il dpn'est utilisé nulle part ailleurs).

Le meilleur dépendra du code que vous pouvez modifier 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.

benzado
la source
1
Réponse géniale! Merci, j'ai une bien meilleure compréhension de ce qui se passe et comment tout cela fonctionne. Dans ce cas, j'ai le contrôle sur tout, je vais donc ré-architecturer certains objets selon les besoins.
XJones
18
O_O Je venais juste de passer avec un problème légèrement différent, je suis resté coincé à lire, et maintenant je laisse cette page bien informée et cool. Merci!
Orc JMR
est correct, que si, pour une raison quelconque, le moment de l'exécution du bloc dpsera publié (par exemple, s'il s'agissait d'un contrôleur de vue et qu'il a été déclenché), la ligne [dp.delegate ...provoquera EXC_BADACCESS?
peetonn
La propriété contenant le bloc (par exemple dataProcess.progress) doit-elle être strongou weak?
djskinner
1
Vous pourriez jeter un œil à libextobjc qui fournit deux macros pratiques appelées @weakify(..)et @strongify(...)qui vous permet d'utiliser selfen bloc de manière non conservante.
25

Il existe également la possibilité de supprimer l'avertissement lorsque vous êtes certain que le cycle sera rompu à 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 tour avec __weak, l' selfalias et le préfixe ivar explicite.

zoul
la source
8
Cela ressemble à une très mauvaise pratique qui prend plus de 3 lignes de code qui peuvent être remplacées par __weak id faibleSelf = self;
Ben Sinclair
3
Il existe souvent un bloc de code plus important qui peut bénéficier des avertissements supprimés.
zoul
2
Sauf que le __weak id weakSelf = self;comportement est fondamentalement différent de la suppression de l'avertissement. La question a commencé par "... si vous êtes certain que le cycle de rétention sera rompu"
Tim
Trop souvent, les gens affaiblissent aveuglément les variables, sans vraiment comprendre les ramifications. Par exemple, j'ai vu des gens affaiblir un objet puis, dans le bloc, ils le font: [array addObject:weakObject];si le faible objet a été libéré, cela provoque un crash. De toute évidence, cela n'est pas préféré à un cycle de rétention. Vous devez comprendre si votre bloc vit réellement assez longtemps pour justifier sa fragilisation, et aussi si vous voulez que l'action dans le bloc dépende de la validité de l'objet faible.
mahboudz
14

Pour une solution commune, je les ai définis dans l'en-tête de précompilation. Évite la capture et permet toujours l'aide du compilateur en évitant d'utiliserid

#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;
};
Damien Pontifex
la source
D'accord, cela pourrait provoquer un problème à l'intérieur du bloc. ReactiveCocoa a une autre solution intéressante pour ce problème qui vous permet de continuer à utiliser à l' selfintérieur de votre bloc @weakify (self); bloc id = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex
@dmpontifex c'est une macro de libextobjc github.com/jspahrsummers/libextobjc
Elechtron
11

Je crois que la solution sans ARC fonctionne également avec ARC, en utilisant le __blockmot - clé:

EDIT: Selon les notes de mise à jour de la transition vers ARC , un objet déclaré avec __blockstockage est toujours conservé. Utilisez __weak(préféré) ou __unsafe_unretained(pour une compatibilité ascendante).

// 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];
Tony
la source
Je ne savais pas que le __blockmot clé évitait de conserver son référent. Merci! J'ai mis à jour ma réponse monolithique. :-)
benzado
3
Selon Apple docs "En mode de comptage manuel des références, __block id x; a pour effet de ne pas conserver x. En mode ARC, __block id x; par défaut, il conserve x (comme toutes les autres valeurs)."
XJones
11

Combinant quelques autres réponses, voici ce que j'utilise maintenant pour un soi faible typé à utiliser en blocs:

__typeof(self) __weak welf = self;

J'ai défini cela comme un extrait de code XCode avec un préfixe de complétion "welf" dans les méthodes / fonctions, qui apparaît après avoir tapé uniquement "nous".

Kendall Helmstetter Gelner
la source
Êtes-vous sûr? Ce lien et les documents clang semblent penser que les deux peuvent et doivent être utilisés pour conserver une référence à l'objet mais pas un lien qui provoquera un cycle de conservation: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner
A partir des documents clang: clang.llvm.org/docs/BlockLanguageSpec.html "Dans les langages Objective-C et Objective-C ++, nous autorisons le spécificateur __weak pour les variables __block de type objet. Si la récupération de place n'est pas activée, ce qualificatif provoque ces variables doivent être conservées sans conserver les messages envoyés. "
Kendall Helmstetter Gelner du
6

warning => "la capture de soi à l'intérieur du bloc est susceptible d'entraîner un cycle de rétention"

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

donc pour l'éviter, nous devons en faire une semaine de référence

__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 rétention se produit généralement lorsque certains comment deux objets se référant l'un à l'autre par lesquels les deux ont le nombre de références = 1 et leur méthode delloc n'est jamais appelé.

Anurag Bhakuni
la source
-1

Si vous êtes sûr que votre code ne créera pas de cycle de rétention ou que le cycle sera interrompu plus tard, la façon la plus simple de désactiver l'avertissement est la suivante:

// 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, bien que l'accès aux points des propriétés soit pris en compte par l'analyse de Xcode, et donc

x.y.z = ^{ 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 méthode ne sont pas soumis à la même analyse, même lorsqu'ils sont des appels de méthode d'accès à la propriété qui sont équivalents à dot-access, même lorsque ces méthodes d'accès aux propriétés sont générées par le compilateur, donc dans

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

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

Ben Artin
la source