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.
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.
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.
Voici quelques-uns des pièges de la méthode Swizzling:
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.
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.
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é.
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
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 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!
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!
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.
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.
Je vais d’abord définir avec exactitude ce que je veux dire par méthode:
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).
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.
Bien que j'aie utilisé cette technique, je voudrais souligner que:
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é.
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++.
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.
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é.