web-dev-qa-db-fra.com

Quels sont les dangers de la méthode Swizzling dans Objective-C?

J'ai entendu des gens dire que balayer une méthode est une pratique dangereuse. Même le nom swizzling suggère qu'il s'agit d'un tricheur.

Method Swizzling modifie le mappage de sorte que le sélecteur d'appel A appelle réellement l'implémentation B. Une utilisation de cette méthode consiste à étendre le comportement des classes source fermées.

Pouvons-nous formaliser les risques de manière à ce que toute personne qui décide d'utiliser ou non l'utilisation de swizzling puisse décider en connaissance de cause si cela en vaut la peine?.

Par exemple.

  • Naming Collisions : Si la classe étend ultérieurement ses fonctionnalités pour inclure le nom de la méthode que vous avez ajoutée, cela entraînera une énorme quantité de problèmes. Réduisez les risques en nommant de manière judicieuse les méthodes qui ont fait l’objet d’un blizzard.
229
Robert

Je pense que c’est une très bonne question, et il est dommage qu’au lieu de s’attaquer à la vraie question, la plupart des réponses l’ont contournée et se bornent à dire de ne pas utiliser la magie.

Utiliser une méthode grésillant revient à utiliser des couteaux tranchants dans la cuisine. Certaines personnes ont peur des couteaux tranchants parce qu’elles pensent pouvoir se couper elles-mêmes, mais la vérité est que les couteaux tranchants sont plus sûrs .

La méthode swizzling peut être utilisée pour écrire du code meilleur, plus efficace et plus facile à gérer. Il peut aussi être maltraité et conduire à d’horribles insectes.

Contexte

Comme pour tous les modèles de conception, si nous sommes pleinement conscients de ses conséquences, nous pourrons prendre des décisions plus éclairées quant à l’utilisation ou non de celui-ci. Les singletons sont un bon exemple de ce qui est assez controversé, et pour cause, ils sont vraiment difficiles à mettre en œuvre correctement. Cependant, beaucoup de gens choisissent encore d'utiliser des singletons. La même chose peut être dite à propos de swizzling. Vous devez vous faire votre propre opinion une fois que vous comprenez parfaitement le bien et le mal.

Discussion

Voici quelques-uns des pièges de la méthode Swizzling:

  • Méthode swizzling n'est pas atomique
  • Change le comportement du code non possédé
  • Conflits de noms possibles
  • Swizzling change les arguments de la méthode
  • L'ordre des swizzles compte
  • Difficile à comprendre (l'air récursif)
  • Difficile de déboguer

Ces points sont tous valables et, en y apportant des solutions, nous pouvons améliorer notre compréhension de la méthode et la méthodologie utilisée pour obtenir le résultat. Je vais les prendre à la fois.

Méthode swizzling n'est pas atomique

Je n'ai pas encore vu d'implémentation de méthode swizzling pouvant être utilisée simultanément1. Ce n'est en réalité pas un problème dans 95% des cas où vous souhaiteriez utiliser la méthode Swizzling. Généralement, vous voulez simplement remplacer l'implémentation d'une méthode et vous voulez que cette implémentation soit utilisée pendant toute la durée de vie de votre programme. Cela signifie que vous devriez utiliser votre méthode en utilisant +(void)load. La méthode load class est exécutée en série au début de votre application. Vous n'aurez aucun problème avec la concurrence si vous faites votre swizzling ici. Si vous deviez balancer dans +(void)initialize, cependant, vous pourriez vous retrouver avec une situation de concurrence critique dans votre implémentation assourdissante et la durée d'exécution pourrait se retrouver dans un état étrange.

Change le comportement du code non possédé

C'est un problème de grésillement, mais c'est un peu le but de tout. Le but est de pouvoir changer ce code. La raison pour laquelle les gens soulignent cela comme étant un gros problème est que vous ne modifiez pas les choses uniquement pour la seule instance de NSButton pour laquelle vous voulez changer les choses, mais plutôt pour tous les NSButton cas dans votre application. Pour cette raison, vous devriez faire preuve de prudence lorsque vous balayez, mais vous n'avez pas besoin de l'éviter complètement.

Pensez-y de cette façon ... si vous substituez une méthode dans une classe et que vous n'appelez pas la méthode super class, des problèmes peuvent survenir. Dans la plupart des cas, la super-classe s'attend à ce que cette méthode soit appelée (sauf indication contraire). Si vous appliquez cette même idée à Swizzling, vous avez couvert la plupart des problèmes. Appelez toujours l'implémentation d'origine. Si vous ne le faites pas, vous changez probablement trop pour être en sécurité.

Conflits de noms possibles

Les conflits de dénomination sont un problème tout au long de cacao. Nous préfixons fréquemment les noms de classe et les noms de méthodes dans les catégories. Malheureusement, les conflits de nommage sont un fléau dans notre langue. Dans le cas de swizzling, cependant, ils ne doivent pas être. Nous avons juste besoin de changer notre façon de penser à la méthode Swizzling légèrement. La plupart des swizzling se font comme ça:

@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end

Cela fonctionne très bien, mais que se passerait-il si my_setFrame: Était défini ailleurs? Ce problème n’est pas propre à swizzling, mais nous pouvons quand même le contourner. La solution de contournement présente également l’avantage de traiter d’autres pièges. Voici ce que nous faisons à la place:

@implementation NSView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end

Bien que cela ressemble un peu moins à Objective-C (puisqu'il utilise des pointeurs de fonction), cela évite tout conflit de nommage. En principe, il fait exactement la même chose que swizzling standard. C'est peut-être un peu un changement pour les personnes qui utilisent le swizzling tel qu'il est défini depuis un certain temps, mais à la fin, je pense que c'est mieux. La méthode swizzling est définie ainsi:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

Balayer en renommant les méthodes change les arguments de la méthode

C'est le grand dans mon esprit. C’est la raison pour laquelle on ne devrait pas utiliser de méthode standard. Vous modifiez les arguments passés à l'implémentation de la méthode d'origine. C'est ici que ça se passe:

[self my_setFrame:frame];

Ce que fait cette ligne est:

objc_msgSend(self, @selector(my_setFrame:), frame);

Qui utilisera le runtime pour rechercher l'implémentation de my_setFrame:. Une fois que l'implémentation est trouvée, elle est invoquée avec les mêmes arguments que ceux fournis. L’implémentation trouvée est l’implémentation originale de setFrame:. Elle avance donc et appelle ainsi, mais l’argument _cmd N’est pas setFrame: Comme il se doit. C'est maintenant my_setFrame:. L'implémentation d'origine est appelée avec un argument qu'elle ne s'attendait pas à recevoir. Ce n'est pas bien.

Il existe une solution simple: utilisez la technique de balayage alternatif définie ci-dessus. Les arguments resteront inchangés!

L'ordre des swizzles compte

L'ordre dans lequel les méthodes sont balayées compte. En supposant que setFrame: N'est défini que sur NSView, imaginez cet ordre de choses:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

Que se passe-t-il lorsque la méthode sur NSButton est balayée? Bien que swizzling s'assure qu'il ne remplace pas l'implémentation de setFrame: Pour toutes les vues, il va extraire la méthode de l'instance. Ceci utilisera l'implémentation existante pour redéfinir setFrame: Dans la classe NSButton de sorte que l'échange d'implémentations n'affecte pas toutes les vues. L'implémentation existante est celle définie sur NSView. La même chose se produira lorsque balayer sur NSControl (en utilisant à nouveau l'implémentation NSView).

Lorsque vous appelez setFrame: Sur un bouton, il appellera votre méthode swizzled, puis passera directement à la méthode setFrame: Définie à l'origine sur NSView. Les implémentations NSControl et NSView swizzled ne seront pas appelées.

Mais si l’ordre était:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Étant donné que la vue survient en premier, le contrôle en mesure sera en mesure d'afficher la bonne méthode. De même, étant donné que le contrôle avait lieu avant le bouton, celui-ci affichera l'implémentation balayée du contrôle de setFrame:. C'est un peu déroutant, mais c'est le bon ordre. Comment pouvons-nous assurer cet ordre de choses?

Encore une fois, utilisez simplement load pour balancer des choses. Si vous balayez load et que vous ne modifiez que la classe en cours de chargement, vous serez en sécurité. La méthode load garantit que la méthode de chargement des super-classes sera appelée avant les sous-classes. Nous aurons le bon ordre exact!

Difficile à comprendre (l'air récursif)

En regardant une méthode traditionnellement définie swizzled, je pense qu'il est vraiment difficile de dire ce qui se passe. Mais en regardant la façon alternative que nous avons faite ci-dessus, il est assez facile à comprendre. Celui-ci a déjà été résolu!

Difficile de déboguer

Une des confusions lors du débogage est de voir une étrange trace en arrière, où les noms enchevêtrés sont mélangés et tout est mélangé dans votre tête. Encore une fois, la mise en œuvre alternative répond à cela. Vous verrez des fonctions clairement nommées dans les backtraces. Néanmoins, swizzling peut être difficile à déboguer, car il est difficile de se rappeler l’impact qu’a ce swizzling. Documentez bien votre code (même si vous pensez que vous êtes le seul à le voir). Suivez les bonnes pratiques et tout ira bien. Il n'est pas plus difficile de déboguer que du code multi-thread.

Conclusion

La méthode swizzling est sûre si elle est utilisée correctement. Une mesure de sécurité simple que vous pouvez prendre est de ne balayer que dans load. Comme beaucoup de choses dans la programmation, cela peut être dangereux, mais comprendre les conséquences vous permettra de l'utiliser correctement.


1 En utilisant la méthode swizzling définie ci-dessus, vous pouvez sécuriser les discussions si vous utilisiez des trampolines. Vous auriez besoin de deux trampolines. Au début de la méthode, vous devez attribuer le pointeur de fonction, store, à une fonction qui tourne jusqu'à ce que l'adresse vers laquelle store pointe vers change soit modifiée. Cela éviterait toute condition de concurrence critique dans laquelle la méthode swizzled a été appelée avant que vous ne puissiez définir le pointeur de fonction store. Vous devrez alors utiliser un trampoline dans le cas où l'implémentation n'est pas déjà définie dans la classe, demander la recherche de trampoline et appeler correctement la méthode de la super classe. Définir la méthode de manière à rechercher de manière dynamique la super-implémentation garantira que l'ordre des appels sifflants n'a pas d'importance.

426
wbyoung

Je vais d’abord définir avec exactitude ce que je veux dire par méthode:

  • Réacheminez tous les appels initialement envoyés à une méthode (appelée A) vers une nouvelle méthode (appelée B).
  • Nous possédons la méthode B
  • Nous ne possédons pas la méthode A
  • La méthode B travaille, puis appelle la méthode A.

La méthode swizzling est plus générale que cela, mais c'est le cas qui m'intéresse.

Dangers:

  • Changements dans la classe d'origine. Nous ne possédons pas la classe que nous balayons. Si la classe change, notre tourbillon peut cesser de fonctionner.

  • Difficile à maintenir. Non seulement avez-vous à écrire et à maintenir la méthode swizzled. vous devez écrire et maintenir le code qui préforme le swizzle

  • Difficile à déboguer. Il est difficile de suivre le flux d'un swizzle, certaines personnes peuvent même ne pas se rendre compte que le swizzle a été préformé. S'il y a des bugs introduits par le swizzle (peut-être des contributions à des changements dans la classe d'origine), ils seront difficiles à résoudre.

En résumé, vous devriez minimiser les grésillements et réfléchir à la manière dont les changements dans la classe d'origine pourraient affecter votre grésillement. Aussi, vous devriez clairement commenter et documenter ce que vous faites (ou simplement l'éviter complètement).

11
Robert

Ce n'est pas le grésillement lui-même qui est vraiment dangereux. Le problème est, comme vous le dites, qu’il est souvent utilisé pour modifier le comportement des classes du framework. En supposant que vous sachiez que le fonctionnement de ces cours privés est "dangereux". Même si vos modifications fonctionnent aujourd'hui, il est toujours probable que Apple changera la classe à l'avenir et entraînera la rupture de votre modification. En outre, si de nombreuses applications le font, cela en fait beaucoup plus difficile pour Apple de changer le framework sans casser beaucoup de logiciels existants.

7
Caleb

Bien que j'aie utilisé cette technique, je voudrais souligner que:

  • Cela obscurcit votre code car il peut provoquer des effets secondaires non documentés, bien que souhaités. Lorsqu’on lit le code, il peut ne pas être au courant du comportement requis en ce qui concerne les effets secondaires, à moins qu’il ne se souvienne de chercher dans la base de code pour voir s’il a été balayé. Je ne suis pas sûr de savoir comment résoudre ce problème car il n'est pas toujours possible de documenter tous les endroits où le code est tributaire du comportement avec effets secondaires.
  • Cela peut rendre votre code moins réutilisable, car une personne qui trouve un segment de code dépendant du comportement délicat qu’elle aimerait utiliser ailleurs ne peut tout simplement pas le copier-coller dans une autre base de code sans également rechercher et copier la méthode balayée.
5
user3288724

Utilisé avec précaution et sagesse, il peut conduire à un code élégant, mais généralement, il conduit à un code source de confusion.

Je dis que cela devrait être interdit, à moins que vous sachiez que cela représente une occasion très élégante pour une tâche de conception particulière, mais vous devez savoir clairement pourquoi cela s’applique bien à la situation et pourquoi les alternatives ne fonctionnent pas avec élégance pour la situation. .

Par exemple, une bonne application de la méthode swizzling est isa swizzling, qui permet à ObjC de mettre en œuvre l'observation de la valeur clé.

Un mauvais exemple pourrait être de s’appuyer sur une méthode sophistiquée pour étendre vos classes, ce qui conduit à un couplage extrêmement élevé.

5
Arafangion

Je pense que le plus grand danger est de créer de nombreux effets secondaires indésirables, complètement par accident. Ces effets secondaires peuvent se présenter comme des "bugs" qui vous amènent à emprunter le mauvais chemin pour trouver la solution. D'après mon expérience, le danger est un code illisible, déroutant et frustrant. Un peu comme si quelqu'un abusait des pointeurs de fonction en C++.

4
A.R.

Vous pouvez vous retrouver avec un code étrange comme

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

à partir du code de production réel lié à une certaine magie de l'interface utilisateur.

4
quantumpotato

La méthode swizzling peut être très utile en test unitaire.

Il vous permet d'écrire un objet fictif et de le faire utiliser à la place de l'objet réel. Votre code doit rester propre et votre test unitaire a un comportement prévisible. Supposons que vous souhaitiez tester du code utilisant CLLocationManager. Votre test unitaire pourrait basculer startUpdatingLocation de sorte qu'il transmettrait un ensemble d'emplacements prédéterminés à votre délégué et que votre code ne soit pas modifié.

3
user3288724