web-dev-qa-db-fra.com

Comment éviter de s'auto-enregistrer en blocs lors de l'implémentation d'une API?

J'ai une application qui fonctionne et je travaille à la convertir en ARC sous Xcode 4.2. L'un des avertissements de pré-vérification implique la capture de self fortement dans un bloc menant à un cycle de conservation. 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 "bonne" méthode recommandée pour 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 les blocs pertinents à ma question
  • supposons que MyAPI récupère les données d'une source distante et que MyDataProcessor fonctionne sur ces données et génère 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: qu'est-ce que je fais "mal" et/ou comment cela devrait-il être modifié pour se conformer aux conventions ARC?

222
XJones

Réponse courte

Au lieu d'accéder directement à self, 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 automatique de références (ARC) , vous pouvez procéder comme suit:

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

Le mot clé __block marque les variables qui peuvent être modifiées à l'intérieur du bloc (ce n'est pas le cas), mais elles ne sont pas non plus conservées automatiquement lorsque le bloc est conservé (sauf si vous utilisez ARC). Dans ce cas, vous devez vous assurer que rien d'autre ne tentera d'exécuter le bloc après la publication de l'instance MyDataProcessor. (Étant donné 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 référence est conservée. Dans ce cas, vous devez le déclarer __weak au lieu.

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 d'extraire sa propriété delegate et envoyer une méthode au délégué. Si tout le reste de votre application libère sa référence à cet objet, son nombre de retenues ne sera pas égal à zéro (car le bloc le pointe) et le bloc ne fait rien de mal (car l'objet pointe vers lui), et ainsi de suite. la paire d'objets s'infiltrera dans le tas, occupant de la mémoire mais inaccessible à tout jamais sans un 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 cycles (visible à partir d'ici; le délégué peut conserver notre objet mais c'est hors de notre portée actuellement). 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 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 continuera d'envoyer des messages de mise à jour à l'ancien délégué. Que cela soit susceptible de se produire 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 self directement au délégué dans l'appel de méthode, vous devez donc le récupérer quelque part. Si vous avez le contrôle sur la définition du type de bloc, le mieux 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 changer le bloc, vous pouvez le gérer . La raison pour laquelle un cycle de conservation est un avertissement et non une erreur est qu’ils n’épellent pas nécessairement Doom pour votre application. Si MyDataProcessor est capable de libérer les blocs une fois l'opération terminée, le cycle sera interrompu avant que son parent ne tente de le libérer, et tout sera nettoyé correctement. Si vous pouviez en être sûr, 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 fichier. Mais ne désactivez pas l'avertissement pour l'ensemble du projet.)

Vous pouvez également utiliser une astuce similaire ci-dessus, en déclarant une référence faible ou non conservée 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];
}

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: __weak essaiera de mettre à zéro la référence lorsque l'objet sera publié; __unsafe_unretained vous laissera un pointeur invalide; __block ajoutera un autre niveau d'indirection et vous permettra de modifier la valeur de la référence à l'intérieur du bloc (sans importance dans ce cas, puisque dp n'est utilisé nulle part ailleurs).

Ce qui est 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.

509
benzado

Il est également possible de supprimer l’avertissement lorsque vous êtes certain que le cycle sera brisé à 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’aurez pas à vous morfondre avec __weak, self aliasing et préfixage ivar explicite.

25
zoul

Pour une solution commune, je les ai définis dans l'en-tête de précompilation. Évite la capture et active toujours l'aide du compilateur en évitant d'utiliser 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;
};
14
Damien Pontifex

Je pense que la solution sans ARC fonctionne également avec ARC, en utilisant le mot clé __block:

EDIT: Conformément à la passage aux notes de version d'ARC , un objet déclaré avec le stockage __block est toujours conservé. Utilisez __weak (préféré) ou __unsafe_unretained (pour la compatibilité avec les versions antérieures).

// 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];
11
Tony

En combinant quelques autres réponses, voici ce que j'utilise maintenant pour un moi 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 de complétion "welf" dans les méthodes/fonctions, qui apparaît après avoir tapé uniquement "nous".

warning => "se capturer à l'intérieur du bloc est susceptible de conduire à un cycle de rétention"

lorsque vous 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 est indiqué plus haut.

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

__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 conservation se produit généralement lorsque deux objets se référant l'un à l'autre et dont le nombre de références est égal à 1 et dont la méthode delloc n'est jamais appelée.

6
Anurag Bhakuni

La nouvelle façon de faire est d'utiliser @weakify et @strongify marco

@weakify(self);
[self methodThatTakesABlock:^ {
    @strongify(self);
    [self doSomething];
}];

Plus d'informations sur @Weakify @Strongify Marco

1
Jun Jie Gan