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

235

J'ai entendu des gens dire que la méthode swizzling est une pratique dangereuse. Même le nom bouillonnant suggère que c'est un peu une triche.

La méthode Swizzling modifie le mappage de sorte que l'appel du sélecteur A invoque réellement l'implémentation B. Une de ses utilisations est d'étendre le comportement des classes source fermées.

Pouvons-nous formaliser les risques afin que quiconque décide d'utiliser le swizzling puisse prendre une décision en connaissance de cause si cela en vaut la peine pour ce qu'il essaie de faire.

Par exemple

  • Nommer les collisions : si la classe étend ultérieurement ses fonctionnalités pour inclure le nom de méthode que vous avez ajouté, cela entraînera une énorme quantité de problèmes. Réduisez le risque en nommant judicieusement les méthodes accélérées.
Robert
la source
Ce n'est déjà pas très bien d'obtenir quelque chose d'autre que vous attendez du code que vous avez lu
Martin Babacaev
cela ressemble à une façon impure de faire ce que les décorateurs de python font très proprement
Claudiu

Réponses:

439

Je pense que c'est une très bonne question, et c'est dommage qu'au lieu de s'attaquer à la vraie question, la plupart des réponses aient contourné le problème et aient simplement dit de ne pas utiliser le swizzling.

Utiliser la méthode grésillement, c'est comme utiliser des couteaux tranchants dans la cuisine. Certaines personnes ont peur des couteaux tranchants parce qu'elles pensent qu'elles se coupent mal, mais la vérité est que les couteaux tranchants sont plus sûrs .

La méthode swizzling peut être utilisée pour écrire un code meilleur, plus efficace et plus facile à gérer. Il peut également être abusé et conduire à des bugs horribles.

Contexte

Comme pour tous les modèles de conception, si nous sommes pleinement conscients des conséquences du modèle, nous sommes en mesure de prendre des décisions plus éclairées sur son utilisation ou non. Les singletons sont un bon exemple de quelque chose qui est assez controversé, et pour une bonne raison - 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 du 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:

  • La méthode swizzling n'est pas atomique
  • Modifie le comportement du code non possédé
  • Conflits de nommage possibles
  • Swizzling change les arguments de la méthode
  • L'ordre des swizzles est important
  • Difficile à comprendre (semble récursif)
  • Difficile à déboguer

Ces points sont tous valables, et en les abordant, nous pouvons améliorer à la fois notre compréhension de la méthode swizzling ainsi que la méthodologie utilisée pour atteindre le résultat. Je prendrai chacun à la fois.

La méthode swizzling n'est pas atomique

Je n'ai pas encore vu une implémentation de la méthode swizzling qui est sûre à utiliser simultanément 1 . Ce n'est en fait pas un problème dans 95% des cas où vous souhaitez utiliser la méthode swizzling. Généralement, vous souhaitez simplement remplacer l'implémentation d'une méthode et vous souhaitez que cette implémentation soit utilisée pendant toute la durée de vie de votre programme. Cela signifie que vous devez faire votre méthode rapidement +(void)load. La loadméthode de classe est exécutée en série au début de votre application. Vous n'aurez aucun problème avec la concurrence si vous effectuez votre navigation ici. Si vous deviez vous précipiter +(void)initialize, cependant, vous pourriez vous retrouver avec une condition de concurrence dans votre implémentation de swizzling et l'exécution pourrait se retrouver dans un état étrange.

Modifie le comportement du code non possédé

C'est un problème avec swizzling, mais c'est un peu le point. Le but est de pouvoir changer ce code. La raison pour laquelle les gens soulignent que c'est un gros problème est que vous ne changez pas seulement les choses pour l'instance pour NSButtonlaquelle vous voulez changer les choses, mais pour toutes les NSButtoninstances de votre application. Pour cette raison, vous devez être prudent lorsque vous tournez, mais vous n'avez pas besoin de l'éviter complètement.

Pensez-y de cette façon ... si vous remplacez une méthode dans une classe et que vous n'appelez pas la méthode de super classe, vous pouvez provoquer des problèmes. 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 pensée au swizzling, vous avez couvert la plupart des problèmes. Appelez toujours l'implémentation d'origine. Sinon, vous changez probablement trop pour être en sécurité.

Conflits de nommage possibles

Les conflits de dénomination sont un problème dans Cocoa. Nous préfixons fréquemment les noms de classe et les noms de méthode dans les catégories. Malheureusement, les conflits de noms sont un fléau dans notre langue. Dans le cas du swizzling, cependant, ils ne doivent pas l'être. Nous avons juste besoin de changer légèrement notre façon de penser à la méthode. Le swizzling se fait comme ceci:

@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 s'il my_setFrame:était défini ailleurs? Ce problème n'est pas propre au swizzling, mais nous pouvons le contourner quand même. La solution de contournement a l'avantage supplémentaire de résoudre également les 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 (car il utilise des pointeurs de fonction), cela évite tout conflit de dénomination. En principe, il fait exactement la même chose que le swizzling standard. Cela peut être un peu un changement pour les personnes qui utilisent le swizzling tel qu'il a été défini depuis un certain temps, mais au final, je pense que c'est mieux. La méthode de 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

Swizzling en renommant les méthodes modifie les arguments de la méthode

Ceci est le grand dans mon esprit. C'est la raison pour laquelle le swizzling de la méthode standard ne doit pas être effectué. Vous modifiez les arguments passés à l'implémentation de la méthode d'origine. C'est là que ça se passe:

[self my_setFrame:frame];

Ce que fait cette ligne, c'est:

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

Qui utilisera le runtime pour rechercher l'implémentation de my_setFrame:. Une fois l'implémentation trouvée, il invoque l'implémentation avec les mêmes arguments que ceux donnés. L'implémentation qu'il trouve est l'implémentation d'origine setFrame:, donc il continue et appelle cela, mais l' _cmdargument n'est pas setFrame:comme il devrait être. 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 alternative de swizzling définie ci-dessus. Les arguments resteront inchangés!

L'ordre des swizzles est important

L'ordre dans lequel les méthodes se propagent compte. En supposant setFrame:que 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 NSButtonest accélérée? Eh bien, la plupart des accélérations garantiront qu'elles ne remplacent pas l'implémentation de setFrame:pour toutes les vues, donc elles afficheront la méthode d'instance. Cela utilisera l'implémentation existante pour redéfinir setFrame:dans la NSButtonclasse afin 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 lors de l'activation NSControl(en utilisant à nouveau l' NSViewimplémentation).

Lorsque vous appelez setFrame:un bouton, il appellera donc votre méthode swizzled, puis passera directement à la setFrame:méthode initialement définie sur NSView. Les implémentations NSControlet NSViewswizzled ne seront pas appelées.

Et si la commande é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 le swizzling de la vue a lieu en premier, le swizzling de contrôle pourra tirer la bonne méthode. De même, puisque le swizzling du contrôle était avant le swizzling du bouton, le bouton affichera la mise en œuvre du swizzled du contrôle 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 loadpour accélérer les choses. Si vous vous précipitez loadet que vous apportez uniquement des modifications à la classe en cours de chargement, vous serez en sécurité. La loadméthode garantit que la méthode de chargement de super classe sera appelée avant toutes les sous-classes. Nous obtiendrons la bonne commande exacte!

Difficile à comprendre (semble récursif)

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

Difficile à déboguer

L'une des confusions pendant le débogage est de voir une étrange trame où les noms brouillés sont mélangés et tout se confond dans votre tête. Encore une fois, l'implémentation alternative résout ce problème. Vous verrez des fonctions clairement nommées dans les backtraces. Pourtant, le swizzling peut être difficile à déboguer car il est difficile de se rappeler quel impact le swizzling a. 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 à déboguer que le code multi-thread.

Conclusion

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


1 En utilisant la méthode de swizzling définie ci-dessus, vous pourriez rendre les choses sûres pour les fils si vous utilisiez des trampolines. Vous auriez besoin de deux trampolines. Au début de la méthode, vous devez affecter le pointeur de fonction store, à une fonction qui a tourné jusqu'à ce que l'adresse vers laquelle storepointé change. 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 storepointeur de fonction. Vous devrez alors utiliser un trampoline dans le cas où l'implémentation n'est pas déjà définie dans la classe et avoir la recherche de trampoline et appeler correctement la méthode de super classe. La définition de la méthode afin qu'elle recherche dynamiquement la super implémentation garantira que l'ordre des appels accélérés n'a pas d'importance.

wbyoung
la source
24
Réponse étonnamment informative et techniquement valable. La discussion est vraiment intéressante. Merci d'avoir pris le temps d'écrire ceci.
Ricardo Sanchez-Saez
Pourriez-vous indiquer si l'accélération de votre expérience est acceptable sur les boutiques d'applications. Je vous dirige également vers stackoverflow.com/questions/8834294 .
Dickey Singh
3
Swizzling peut être utilisé sur l'App Store. De nombreuses applications et frameworks le font (le nôtre inclus). Tout ce qui précède est toujours valable, et vous ne pouvez pas utiliser les méthodes privées (en fait, vous pouvez techniquement, mais vous risquez le rejet et les dangers de cela sont pour un thread différent).
wbyoung
Réponse incroyable. Mais pourquoi faire le swizzling dans class_swizzleMethodAndStore () au lieu de directement dans + swizzle: avec: store :? Pourquoi cette fonction supplémentaire?
Frizlab
2
@Frizlab bonne question! C'est vraiment juste une question de style. Si vous écrivez un tas de code qui fonctionne directement avec l'API d'exécution Objective-C (en C), il est agréable de pouvoir l'appeler style C pour plus de cohérence. La seule raison pour laquelle je peux penser en plus de cela est que si vous avez écrit quelque chose en C pur, alors c'est plus appelable. Il n'y a cependant aucune raison que vous ne puissiez pas tout faire dans la méthode Objective-C.
wbyoung
12

Je vais d'abord définir exactement ce que je veux dire par méthode swizzling:

  • Réacheminer tous les appels initialement envoyés vers 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 fait un peu de travail 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 parcourons. Si la classe change, notre swizzle peut cesser de fonctionner.

  • Difficile à entretenir . Non seulement vous devez écrire et maintenir la méthode accélérée. 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 bogues introduits par le swizzle (peut-être dû à des changements dans la classe d'origine), ils seront difficiles à résoudre.

En résumé, vous devez réduire au minimum le swizzling et considérer comment les changements dans la classe d'origine peuvent affecter votre swizzle. Vous devez également clairement commenter et documenter ce que vous faites (ou simplement l'éviter complètement).

Robert
la source
@everyone J'ai combiné vos réponses dans un joli format. N'hésitez pas à le modifier / y ajouter. Merci pour votre contribution.
Robert
Je suis fan de ta psychologie. "Avec une grande puissance de swizzling vient une grande responsabilité de swizzling."
Jacksonkr
7

Ce n'est pas le tourbillon 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 de framework. C'est en supposant que vous savez quelque chose sur le fonctionnement de ces cours privés qui est "dangereux". Même si vos modifications fonctionnent aujourd'hui, il y a toujours une chance qu'Apple change la classe à l'avenir et provoque la rupture de votre modification. De plus, si de nombreuses applications différentes le font, il est d'autant plus difficile pour Apple de changer le cadre sans casser beaucoup de logiciels existants.

Caleb
la source
Je dirais que ces développeurs devraient récolter ce qu'ils ont semé - ils ont étroitement couplé leur code à une implémentation particulière. C'est donc leur faute si cette implémentation change d'une manière subtile qui n'aurait pas dû casser leur code sans leur décision explicite de coupler leur code aussi étroitement qu'eux.
Arafangion
Bien sûr, mais disons qu'une application très populaire se casse sous la prochaine version d'iOS. Il est facile de dire que c'est la faute du développeur et qu'il devrait mieux le savoir, mais cela fait mal paraître Apple aux yeux de l'utilisateur. Je ne vois aucun inconvénient à utiliser vos propres méthodes ou même vos classes si vous voulez le faire, à part le fait que cela peut rendre le code quelque peu déroutant. Cela commence à devenir un peu plus risqué lorsque vous balayez du code contrôlé par quelqu'un d'autre - et c'est exactement la situation où le swizzling semble le plus tentant.
Caleb
Apple n'est pas le seul fournisseur de classes de base pouvant être modifiées via siwzzling. Votre réponse doit être modifiée pour inclure tout le monde.
AR
@AR Peut-être, mais la question est balisée iOS et iPhone, nous devons donc la considérer dans ce contexte. Étant donné que les applications iOS doivent lier statiquement toutes les bibliothèques et les cadres autres que ceux fournis par iOS, Apple est en fait la seule entité qui peut modifier la mise en œuvre des classes de cadre sans la participation des développeurs d'applications. Mais je suis d'accord avec le fait que des problèmes similaires affectent d'autres plates-formes, et je dirais que les conseils peuvent également être généralisés au-delà du swizzling. D'une manière générale, le conseil est: Gardez vos mains pour vous. Évitez de modifier les composants que d'autres personnes maintiennent.
Caleb
La méthode swizzling peut être aussi dangereuse. mais au fur et à mesure que les frameworks sortent, ils n'omettent pas complètement les précédents. vous devez toujours mettre à jour le framework de base attendu par votre application. et cette mise à jour casserait votre application. il est probable que le problème ne soit pas aussi important lorsque l'ios est mis à jour. Mon modèle d'utilisation consistait à remplacer le reuseIdentifier par défaut. Comme je n'avais pas accès à la variable _reuseIdentifier et que je ne voulais pas la remplacer à moins qu'elle n'ait pas été fournie, ma seule solution était de sous-classer tout le monde et de fournir l'identifiant de réutilisation, ou de balayer et de remplacer si rien. Le type d'implémentation est essentiel ...
The Lazy Coder
5

Utilisé avec soin et sagesse, il peut conduire à un code élégant, mais généralement, il conduit à un code confus.

Je dis qu'il devrait être interdit, sauf si vous savez qu'il présente une opportunité très élégante pour une tâche de conception particulière, mais vous devez savoir clairement pourquoi il s'applique bien à la situation et pourquoi les alternatives ne fonctionnent pas élégamment pour la situation .

Par exemple, une bonne application de la méthode swizzling est is swizzling, c'est ainsi qu'ObjC met en œuvre l'observation des valeurs clés.

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

Arafangion
la source
1
-1 pour la mention de KVO - dans le cadre de la méthode swizzling . isa swizzling est juste autre chose.
Nikolai Ruhe
@NikolaiRuhe: N'hésitez pas à fournir des références afin que je puisse être éclairé.
Arafangion
Voici une référence aux détails d'implémentation de KVO . La méthode swizzling est décrite sur CocoaDev.
Nikolai Ruhe
5

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

  • Il obscurcit votre code car il peut provoquer des effets secondaires non documentés, bien que souhaités. Quand on lit le code, il / elle peut ne pas être conscient du comportement d'effet secondaire requis, à moins qu'il ne se souvienne de rechercher dans la base de code pour voir s'il a été glissé. Je ne sais pas comment atténuer ce problème car il n'est pas toujours possible de documenter chaque endroit où le code dépend du comportement accéléré des effets secondaires.
  • Cela peut rendre votre code moins réutilisable car quelqu'un qui trouve un segment de code qui dépend du comportement accéléré qu'ils aimeraient utiliser ailleurs ne peut pas simplement le couper et le coller dans une autre base de code sans également trouver et copier la méthode accélérée.
user3288724
la source
4

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 «bogues» qui à leur tour vous conduisent sur la mauvaise voie pour trouver la solution. D'après mon expérience, le danger est un code illisible, déroutant et frustrant. Un peu comme quand quelqu'un sur-utilise les pointeurs de fonction en C ++.

AR
la source
Ok, donc le problème est qu'il est difficile à déboguer. Le problème est dû au fait que les examinateurs ne réalisent pas / n'oublient pas que le code a été transféré et recherchent au mauvais endroit lors du débogage.
Robert
3
Oui à peu près. De plus, en cas de surutilisation, vous pouvez entrer dans une chaîne sans fin ou un réseau de tourbillons. Imaginez ce qui pourrait arriver lorsque vous changez un seul d'entre eux à un moment donné dans le futur? Je compare l'expérience à empiler des tasses de thé ou à jouer au 'code jenga'
AR
4

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.

quantumpotato
la source
3

Le swizzling de méthode peut être très utile dans les tests unitaires.

Il vous permet d'écrire un objet simulé et d'utiliser cet objet simulé à 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 accélérer startUpdatingLocation afin qu'il fournisse un ensemble prédéterminé d'emplacements à votre délégué et que votre code n'ait pas à changer.

user3288724
la source
isa-swizzling serait un meilleur choix.
vikingosegundo