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 load
mé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 NSButton
laquelle vous voulez changer les choses, mais pour toutes les NSButton
instances 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' _cmd
argument 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 NSButton
est 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 NSButton
classe 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' NSView
implé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 NSControl
et NSView
swizzled 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 load
pour accélérer les choses. Si vous vous précipitez load
et que vous apportez uniquement des modifications à la classe en cours de chargement, vous serez en sécurité. La load
mé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 store
pointé 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 store
pointeur 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.