Penso che questa sia davvero una bella domanda, ed è un peccato che, anziché affrontare la vera domanda, la maggior parte delle risposte abbia aggirato il problema e semplicemente detto di non usare lo sfrigolio.
Usare il metodo sfrigolante è come usare coltelli affilati in cucina. Alcune persone hanno paura dei coltelli affilati perché pensano di tagliarsi male, ma la verità è che i coltelli affilati sono più sicuri .
Il metodo swizzling può essere utilizzato per scrivere codice migliore, più efficiente e più gestibile. Può anche essere abusato e portare a bug orribili.
sfondo
Come per tutti i modelli di progettazione, se siamo pienamente consapevoli delle conseguenze del modello, siamo in grado di prendere decisioni più informate sull'opportunità o meno di utilizzarlo. I singoli sono un buon esempio di qualcosa che è piuttosto controverso, e per una buona ragione: sono davvero difficili da implementare correttamente. Molte persone scelgono comunque di usare i singoli, però. Lo stesso si può dire per lo sfrigolio. Dovresti formare la tua opinione dopo aver compreso appieno sia il bene che il male.
Discussione
Ecco alcune delle insidie del metodo frizzante:
- Il metodo swizzling non è atomico
- Cambia il comportamento del codice non di proprietà
- Possibili conflitti di denominazione
- Swizzling cambia gli argomenti del metodo
- L'ordine degli swizzles è importante
- Difficile da capire (sembra ricorsivo)
- Difficile eseguire il debug
Questi punti sono tutti validi e, affrontandoli, possiamo migliorare sia la nostra comprensione del metodo che sfrigola sia la metodologia utilizzata per ottenere il risultato. Prenderò ognuno alla volta.
Il metodo swizzling non è atomico
Devo ancora vedere un'implementazione del metodo swizzling che è sicuro da usare contemporaneamente 1 . Questo non è in realtà un problema nel 95% dei casi in cui si desidera utilizzare il metodo swizzling. In genere, si desidera semplicemente sostituire l'implementazione di un metodo e si desidera che tale implementazione venga utilizzata per l'intera durata del programma. Questo significa che dovresti fare il tuo metodo sfrecciando +(void)load
. Il load
metodo class viene eseguito in serie all'inizio dell'applicazione. Non avrai problemi con la concorrenza se fai lo swizzling qui. Se avessi il passaggio rapido +(void)initialize
, tuttavia, potresti finire con una condizione di competizione nella tua implementazione rapida e il tempo di esecuzione potrebbe finire in uno stato strano.
Cambia il comportamento del codice non di proprietà
Questo è un problema con lo swizzling, ma è un po 'il punto. L'obiettivo è quello di poter cambiare quel codice. Il motivo per cui le persone lo indicano come un grosso problema è perché non stai cambiando solo le cose per l'istanza per NSButton
cui vuoi cambiare le cose, ma invece per tutte le NSButton
istanze nella tua applicazione. Per questo motivo, dovresti essere cauto quando sfrigoli, ma non devi evitarlo del tutto.
Pensala in questo modo ... se sovrascrivi un metodo in una classe e non chiami il metodo superclasse, potresti causare problemi. Nella maggior parte dei casi, la superclasse si aspetta che venga chiamato quel metodo (se non diversamente documentato). Se applichi lo stesso pensiero allo swizzling, hai coperto la maggior parte dei problemi. Chiama sempre l'implementazione originale. In caso contrario, probabilmente stai cambiando troppo per essere al sicuro.
Possibili conflitti di denominazione
I conflitti di denominazione sono un problema in tutto il cacao. Facciamo spesso prefissi nomi di classe e nomi di metodi in categorie. Sfortunatamente, i conflitti di denominazione sono una piaga nella nostra lingua. Nel caso dello swizzling, tuttavia, non devono esserlo. Dobbiamo solo cambiare il modo in cui pensiamo al metodo che gira leggermente. La maggior parte dello swizzling viene eseguita in questo modo:
@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
Funziona bene, ma cosa accadrebbe se my_setFrame:
fosse definito altrove? Questo problema non è unico per lo swizzling, ma possiamo comunque risolverlo. La soluzione alternativa ha un ulteriore vantaggio nell'affrontare anche altre insidie. Ecco cosa facciamo invece:
@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
Sebbene assomigli un po 'meno a Objective-C (poiché utilizza i puntatori a funzione), evita qualsiasi conflitto di denominazione. In linea di principio, sta facendo esattamente la stessa cosa dello swizzling standard. Questo potrebbe essere un po 'un cambiamento per le persone che hanno usato lo swizzling come è stato definito per un po', ma alla fine, penso che sia meglio. Il metodo frizzante è così definito:
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
La modifica rapida dei metodi che rinomina i metodi modifica gli argomenti del metodo
Questo è quello grande nella mia mente. Questo è il motivo per cui non si dovrebbe fare lo swizzling con il metodo standard. Stai cambiando gli argomenti passati all'implementazione del metodo originale. Questo è dove succede:
[self my_setFrame:frame];
Quello che fa questa linea è:
objc_msgSend(self, @selector(my_setFrame:), frame);
Che utilizzerà il runtime per cercare l'implementazione di my_setFrame:
. Una volta trovata l'implementazione, invoca l'implementazione con gli stessi argomenti che sono stati forniti. L'implementazione che trova è l'implementazione originale di setFrame:
, quindi va avanti e lo chiama, ma l' _cmd
argomento non è setFrame:
come dovrebbe essere. È adesso my_setFrame:
. L'implementazione originale viene chiamata con un argomento che non si aspettava che avrebbe ricevuto. Questo non va bene.
C'è una soluzione semplice: usa la tecnica di scorrimento alternativa definita sopra. Gli argomenti rimarranno invariati!
L'ordine degli swizzles è importante
L'ordine in cui i metodi si gonfiano è importante. Supponendo che setFrame:
sia definito solo su NSView
, immagina questo ordine di cose:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
Cosa succede quando il metodo NSButton
attivo viene spostato? Bene, la maggior parte dello swizzling assicurerà che non sostituisca l'implementazione di setFrame:
per tutte le viste, quindi tirerà su il metodo dell'istanza. Questo utilizzerà l'implementazione esistente per ridefinire setFrame:
la NSButton
classe in modo tale che lo scambio di implementazioni non influisca su tutte le viste. L'implementazione esistente è quella definita NSView
. La stessa cosa accadrà quando sfrigolerai NSControl
(di nuovo usando l' NSView
implementazione).
Quando si chiama setFrame:
un pulsante, verrà quindi chiamato il metodo swizzled e quindi si passerà direttamente al setFrame:
metodo originariamente definito NSView
. Le implementazioni NSControl
e NSView
swizzled non verranno chiamate.
E se l'ordine fosse:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Poiché la visualizzazione della vista viene eseguita per prima, il controllo della rotazione sarà in grado di richiamare il metodo giusto. Allo stesso modo, dal momento che la swizzling controllo era prima che il pulsante swizzling, il pulsante tirare su swizzled implementazione del controllo di setFrame:
. Questo è un po 'confuso, ma questo è l'ordine corretto. Come possiamo garantire questo ordine di cose?
Ancora una volta, basta usare load
per far girare le cose. Se ti muovi rapidamente load
e apporti solo modifiche alla classe che stai caricando, sarai al sicuro. Il load
metodo garantisce che il metodo di caricamento super class verrà chiamato prima di qualsiasi sottoclasse. Otterremo l'esatto ordine esatto!
Difficile da capire (sembra ricorsivo)
Guardando un metodo swizzled tradizionalmente definito, penso che sia davvero difficile dire cosa sta succedendo. Ma osservando il modo alternativo in cui abbiamo sfrigolato sopra, è abbastanza facile da capire. Questo è già stato risolto!
Difficile eseguire il debug
Una delle confusioni durante il debug è vedere uno strano backtrace in cui i nomi frizzanti si confondono e tutto viene confuso nella tua testa. Ancora una volta, l'implementazione alternativa risolve questo problema. Vedrai funzioni chiaramente denominate nei backtrace. Tuttavia, lo swizzling può essere difficile da eseguire il debug perché è difficile ricordare quale impatto sta avendo lo swizzling. Documenta bene il tuo codice (anche se pensi di essere l'unico che lo vedrà mai). Segui le buone pratiche e starai bene. Il debug non è più difficile del codice multi-thread.
Conclusione
Il metodo swizzling è sicuro se usato correttamente. Una semplice misura di sicurezza che puoi prendere è quella di entrare solo load
. Come molte cose nella programmazione, può essere pericoloso, ma capire le conseguenze ti permetterà di usarlo correttamente.
1 Utilizzando il metodo swizzling sopra definito, è possibile rendere le cose sicure se si utilizzano i trampolini. Avresti bisogno di due trampolini. All'inizio del metodo, dovresti assegnare il puntatore store
a funzione a una funzione che è stata eseguita fino a quando l'indirizzo a cui store
puntava è cambiato. Ciò eviterebbe qualsiasi condizione di competizione in cui è stato chiamato il metodo swizzled prima di poter impostare il store
puntatore alla funzione. Dovresti quindi utilizzare un trampolino nel caso in cui l'implementazione non sia già definita nella classe e avere la ricerca del trampolino e chiamare correttamente il metodo super class. Definire il metodo in modo che cerchi in modo dinamico la super implementazione assicurerà che l'ordine delle chiamate frizzanti non contenga.