Ich denke, dies ist eine wirklich großartige Frage, und es ist eine Schande, dass die meisten Antworten das Problem umgangen haben und einfach gesagt haben, dass sie kein Swizzling verwenden sollen, anstatt die eigentliche Frage anzugehen.
Mit der Methode Brutzeln ist wie mit scharfen Messern in der Küche. Einige Menschen haben Angst vor scharfen Messern, weil sie denken, dass sie sich schlecht schneiden werden, aber die Wahrheit ist, dass scharfe Messer sicherer sind .
Das Methoden-Swizzling kann verwendet werden, um besseren, effizienteren und wartbareren Code zu schreiben. Es kann auch missbraucht werden und zu schrecklichen Fehlern führen.
Hintergrund
Wie bei allen Entwurfsmustern können wir fundiertere Entscheidungen darüber treffen, ob wir das Muster verwenden oder nicht, wenn wir uns der Konsequenzen des Musters voll bewusst sind. Singletons sind ein gutes Beispiel für etwas, das ziemlich kontrovers ist, und das aus gutem Grund - sie sind wirklich schwer richtig zu implementieren. Viele Menschen entscheiden sich jedoch immer noch für Singletons. Gleiches gilt für das Swizzling. Sie sollten sich eine eigene Meinung bilden, wenn Sie sowohl das Gute als auch das Schlechte vollständig verstanden haben.
Diskussion
Hier sind einige der Fallstricke des Methoden-Swizzling:
- Methoden-Swizzling ist nicht atomar
- Ändert das Verhalten von nicht im Besitz befindlichem Code
- Mögliche Namenskonflikte
- Durch Swizzling werden die Argumente der Methode geändert
- Die Reihenfolge der Swizzles ist wichtig
- Schwer zu verstehen (sieht rekursiv aus)
- Schwer zu debuggen
Diese Punkte sind alle gültig, und wenn wir sie ansprechen, können wir sowohl unser Verständnis des Methoden-Swizzling als auch die Methodik, mit der das Ergebnis erzielt wird, verbessern. Ich werde jeden nach dem anderen nehmen.
Methoden-Swizzling ist nicht atomar
Ich habe noch keine Implementierung von Methoden-Swizzling gesehen, die sicher gleichzeitig verwendet werden kann 1 . Dies ist in 95% der Fälle, in denen Sie das Methoden-Swizzling verwenden möchten, eigentlich kein Problem. Normalerweise möchten Sie einfach die Implementierung einer Methode ersetzen und möchten, dass diese Implementierung für die gesamte Lebensdauer Ihres Programms verwendet wird. Dies bedeutet, dass Sie Ihre Methode einschalten sollten +(void)load
. Die load
Klassenmethode wird zu Beginn Ihrer Anwendung seriell ausgeführt. Sie werden keine Probleme mit der Parallelität haben, wenn Sie hier Ihre Swizzling machen. Wenn Sie jedoch einschalten +(void)initialize
, könnte dies zu einer Racebedingung in Ihrer Swizzling-Implementierung führen, und die Laufzeit könnte in einem seltsamen Zustand enden.
Ändert das Verhalten von nicht im Besitz befindlichem Code
Dies ist ein Problem mit dem Swizzling, aber es ist der springende Punkt. Das Ziel ist es, diesen Code ändern zu können. Der Grund, warum die Leute darauf hinweisen, ist, dass Sie nicht nur Dinge für die eine Instanz ändern, für die NSButton
Sie Dinge ändern möchten, sondern für alle NSButton
Instanzen in Ihrer Anwendung. Aus diesem Grund sollten Sie beim Swizzeln vorsichtig sein, aber Sie müssen es nicht ganz vermeiden.
Stellen Sie sich das so vor ... Wenn Sie eine Methode in einer Klasse überschreiben und die Superklassenmethode nicht aufrufen, können Probleme auftreten. In den meisten Fällen erwartet die Superklasse, dass diese Methode aufgerufen wird (sofern nicht anders dokumentiert). Wenn Sie denselben Gedanken auf das Swizzling anwenden, haben Sie die meisten Probleme behandelt. Rufen Sie immer die ursprüngliche Implementierung auf. Wenn Sie dies nicht tun, ändern Sie wahrscheinlich zu viel, um sicher zu sein.
Mögliche Namenskonflikte
Namenskonflikte sind in ganz Cocoa ein Thema. Wir stellen häufig Klassennamen und Methodennamen in Kategorien voran. Namenskonflikte sind in unserer Sprache leider eine Plage. Im Falle von Swizzling müssen sie es jedoch nicht sein. Wir müssen nur die Art und Weise ändern, wie wir über Methoden denken, die leicht schwanken. Das meiste Swizzling wird so gemacht:
@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
Das funktioniert gut, aber was würde passieren, wenn my_setFrame:
es woanders definiert würde ? Dieses Problem betrifft nicht nur das Swizzling, aber wir können es trotzdem umgehen. Die Problemumgehung hat den zusätzlichen Vorteil, dass auch andere Fallstricke behoben werden. Folgendes tun wir stattdessen:
@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
Dies ähnelt zwar etwas weniger Objective-C (da es Funktionszeiger verwendet), vermeidet jedoch Namenskonflikte. Im Prinzip macht es genau das Gleiche wie Standard-Swizzling. Dies mag für Leute, die Swizzling verwenden, wie es seit einiger Zeit definiert ist, eine kleine Veränderung sein, aber am Ende denke ich, dass es besser ist. Die Swizzling-Methode ist folgendermaßen definiert:
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
Durch Umbenennen von Methoden werden die Argumente der Methode geändert
Dies ist der große in meinem Kopf. Dies ist der Grund, warum das Swizzling mit Standardmethoden nicht durchgeführt werden sollte. Sie ändern die an die Implementierung der ursprünglichen Methode übergebenen Argumente. Hier passiert es:
[self my_setFrame:frame];
Was diese Zeile tut, ist:
objc_msgSend(self, @selector(my_setFrame:), frame);
Welches wird die Laufzeit verwenden, um die Implementierung von nachzuschlagen my_setFrame:
. Sobald die Implementierung gefunden wurde, ruft sie die Implementierung mit denselben Argumenten auf, die angegeben wurden. Die Implementierung, die es findet, ist die ursprüngliche Implementierung von setFrame:
, also geht es weiter und nennt das, aber das _cmd
Argument ist nicht so, setFrame:
wie es sein sollte. Es ist jetzt my_setFrame:
. Die ursprüngliche Implementierung wird mit einem Argument aufgerufen, das sie niemals erwartet hätte. Das ist nicht gut.
Es gibt eine einfache Lösung - verwenden Sie die oben definierte alternative Swizzling-Technik. Die Argumente bleiben unverändert!
Die Reihenfolge der Swizzles ist wichtig
Die Reihenfolge, in der Methoden durcheinander gebracht werden, spielt eine Rolle. Angenommen, setFrame:
nur definiert ist auf NSView
, stellen Sie sich diese Reihenfolge der Dinge vor:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
Was passiert, wenn die Methode NSButton
aktiviert ist? Nun die meisten Swizzling werden dafür sorgen , dass es nicht die Umsetzung des Ersatz setFrame:
für alle Ansichten, so wird es nach oben zieht die Instanz - Methode. Dadurch wird die vorhandene Implementierung verwendet, um setFrame:
die NSButton
Klasse neu zu definieren, sodass der Austausch von Implementierungen nicht alle Ansichten betrifft. Die vorhandene Implementierung ist die am definierte NSView
. Das gleiche passiert beim Swizzling NSControl
(wieder mit der NSView
Implementierung).
Wenn Sie setFrame:
eine Schaltfläche aufrufen , wird daher Ihre Swizzled-Methode aufgerufen, und Sie springen direkt zu der setFrame:
ursprünglich definierten Methode NSView
. Die NSControl
und NSView
swizzled Implementierungen werden nicht aufgerufen.
Aber was wäre, wenn die Reihenfolge wäre:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Da die Ansicht Swizzling Platz zuerst stattfindet, wird die Steuer Swizzling der Lage sein, nach oben zu ziehen , die richtigen Methode. Ebenso , da die Steuer Swizzling vor dem Button Swizzling war, wird der Knopf nach oben zieht die umgestellte Umsetzung der Kontrolle setFrame:
. Das ist etwas verwirrend, aber dies ist die richtige Reihenfolge. Wie können wir diese Ordnung sicherstellen?
Wieder nur verwenden load
, um Dinge zu swizzeln. Wenn Sie einschalten load
und nur Änderungen an der zu ladenden Klasse vornehmen, sind Sie in Sicherheit. Die load
Methode garantiert, dass die Lademethode der Superklasse vor allen Unterklassen aufgerufen wird. Wir bekommen genau die richtige Bestellung!
Schwer zu verstehen (sieht rekursiv aus)
Wenn ich mir eine traditionell definierte Swizzled-Methode anschaue, denke ich, dass es wirklich schwer ist zu sagen, was los ist. Aber wenn man sich die alternative Art und Weise ansieht, wie wir oben geschwommen sind, ist das ziemlich leicht zu verstehen. Dieser ist bereits gelöst!
Schwer zu debuggen
Eine der Verwirrungen beim Debuggen ist das Sehen einer seltsamen Rückverfolgung, in der die Namen durcheinander gebracht werden und alles in Ihrem Kopf durcheinander gerät. Auch hier spricht die alternative Implementierung dies an. In Backtraces werden klar benannte Funktionen angezeigt. Trotzdem kann das Debuggen schwierig sein, da es schwer zu merken ist, welche Auswirkungen das Swizzling hat. Dokumentieren Sie Ihren Code gut (auch wenn Sie denken, dass Sie der einzige sind, der ihn jemals sehen wird). Befolgen Sie die guten Praktiken, und alles wird gut. Es ist nicht schwieriger zu debuggen als Multithread-Code.
Fazit
Das Methoden-Swizzling ist bei sachgemäßer Anwendung sicher. Eine einfache Sicherheitsmaßnahme, die Sie ergreifen können, besteht darin, nur einzuspritzen load
. Wie viele Dinge in der Programmierung kann es gefährlich sein, aber wenn Sie die Konsequenzen verstehen, können Sie es richtig verwenden.
1 Mit der oben definierten Swizzling-Methode können Sie die Thread-Sicherheit erhöhen, wenn Sie Trampoline verwenden. Sie würden zwei Trampoline brauchen. Zu Beginn der Methode müssten Sie den Funktionszeiger store
einer Funktion zuweisen, die sich dreht, bis sich die Adresse, auf die sie zeigt store
, geändert hat. Dies würde jede Racebedingung vermeiden, in der die Swizzled-Methode aufgerufen wurde, bevor Sie den store
Funktionszeiger setzen konnten. Sie müssten dann ein Trampolin verwenden, wenn die Implementierung noch nicht in der Klasse definiert ist, und die Trampolin-Suche durchführen und die Super-Klassen-Methode ordnungsgemäß aufrufen. Wenn Sie die Methode so definieren, dass sie dynamisch nach der Super-Implementierung sucht, wird sichergestellt, dass die Reihenfolge der Swizzling-Aufrufe keine Rolle spielt.