Quali sono i pericoli del metodo Swizzling in Objective-C?


235

Ho sentito la gente affermare che il metodo swizzling è una pratica pericolosa. Anche il nome frizzante suggerisce che è un po 'un imbroglio.

Metodo Swizzling sta modificando la mappatura in modo tale che il selettore di chiamata A invochi effettivamente l'implementazione B. Un uso di questo è estendere il comportamento delle classi di sorgenti chiuse.

Possiamo formalizzare i rischi in modo tale che chiunque decida di usare lo swizzling possa prendere una decisione informata se ne vale la pena per quello che stanno cercando di fare.

Per esempio

  • Denominazione delle collisioni : se la classe in seguito estende la sua funzionalità per includere il nome del metodo che hai aggiunto, causerà un enorme problema. Ridurre il rischio nominando in modo ragionevole i metodi swizzled.

Non è già molto buono ottenere qualcos'altro che ti aspetti dal codice che hai letto
Martin Babacaev,

sembra un modo impuro di fare ciò che i decoratori di pitone fanno in modo molto pulito
Claudiu,

Risposte:


439

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 loadmetodo 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 NSButtoncui vuoi cambiare le cose, ma invece per tutte le NSButtonistanze 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' _cmdargomento 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 NSButtonattivo 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 NSButtonclasse 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' NSViewimplementazione).

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 NSControle NSViewswizzled 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 loadper far girare le cose. Se ti muovi rapidamente loade apporti solo modifiche alla classe che stai caricando, sarai al sicuro. Il loadmetodo 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 storea funzione a una funzione che è stata eseguita fino a quando l'indirizzo a cui storepuntava è cambiato. Ciò eviterebbe qualsiasi condizione di competizione in cui è stato chiamato il metodo swizzled prima di poter impostare il storepuntatore 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.


24
Risposta incredibilmente istruttiva e tecnicamente valida. La discussione è davvero interessante. Grazie per aver dedicato del tempo a scrivere questo.
Ricardo Sanchez-Saez,

Potresti precisare se lo sfreccio della tua esperienza è accettabile negli app store. Ti sto indirizzando anche su stackoverflow.com/questions/8834294 .
Dickey Singh,

3
Swizzling può essere utilizzato su App store. Molte app e framework fanno (incluso il nostro). Tutto quanto sopra è ancora valido e non puoi far girare i metodi privati ​​(in realtà, tecnicamente puoi, ma rischierai il rifiuto e i pericoli di questo sono per un thread diverso).
wbyoung,

Risposta incredibile. Ma perché effettuare lo swizzling in class_swizzleMethodAndStore () anziché direttamente in + swizzle: con: store :? Perché questa funzione aggiuntiva?
Frizlab,

2
@Frizlab bella domanda! È davvero solo una cosa di stile. Se stai scrivendo un sacco di codice che funziona direttamente con l'API di runtime Objective-C (in C), è bello poterlo chiamare in stile C per coerenza. L'unica ragione a cui riesco a pensare oltre a questo è se hai scritto qualcosa in puro C, quindi è ancora più richiamabile. Non c'è motivo per cui non puoi fare tutto nel metodo Objective-C.
mercoledì

12

Per prima cosa definirò esattamente cosa intendo per metodo swizzling:

  • Reinstradare tutte le chiamate originariamente inviate a un metodo (chiamato A) a un nuovo metodo (chiamato B).
  • Possediamo il Metodo B
  • Non possediamo il metodo A
  • Il metodo B fa un po 'di lavoro, quindi chiama il metodo A.

Il metodo swizzling è più generale di questo, ma questo è il caso che mi interessa.

pericoli:

  • Cambiamenti nella classe originale . Non possediamo la classe che stiamo gonfiando. Se la classe cambia, il nostro swizzle potrebbe smettere di funzionare.

  • Difficile da mantenere . Non solo devi scrivere e mantenere il metodo swizzled. devi scrivere e mantenere il codice che preforma lo swizzle

  • Difficile eseguire il debug . È difficile seguire il flusso di uno swizzle, alcune persone potrebbero anche non rendersi conto che lo swizzle è stato preformato. Se ci sono bug introdotti dallo swizzle (forse dovuti a cambiamenti nella classe originale) saranno difficili da risolvere.

In breve, dovresti ridurre al minimo lo swizzling e considerare in che modo i cambiamenti nella classe originale potrebbero influire sul tuo swizzle. Inoltre, dovresti commentare e documentare chiaramente ciò che stai facendo (o semplicemente evitarlo del tutto).


@tutti ho combinato le tue risposte in un bel formato. Sentiti libero di modificare / aggiungere ad esso. Grazie per il tuo contributo.
Robert,

Sono un fan della tua psicologia. "Con un grande potere frizzante arriva una grande responsabilità frizzante."
Jacksonkr,

7

Non è lo stesso vortice che è davvero pericoloso. Il problema è, come dici tu, che è spesso usato per modificare il comportamento delle classi del framework. Si presume che tu sappia qualcosa su come funzionano quelle classi private che è "pericoloso". Anche se le tue modifiche funzionano oggi, c'è sempre la possibilità che Apple cambierà classe in futuro e causerà l'interruzione delle modifiche. Inoltre, se lo fanno molte app diverse, per Apple è molto più difficile cambiare il framework senza rompere molti software esistenti.


Direi che tali sviluppatori dovrebbero raccogliere ciò che seminano: hanno strettamente associato il loro codice a una particolare implementazione. È quindi colpa loro se quell'implementazione cambia in modo sottile che non avrebbe dovuto rompere il loro codice se non fosse per la loro decisione esplicita di accoppiare il loro codice strettamente come hanno fatto.
Arafangion,

Certo, ma dire che un'app molto popolare si rompe con la prossima versione di iOS. È facile dire che è colpa dello sviluppatore e dovrebbero conoscerlo meglio, ma fa sembrare Apple male agli occhi dell'utente. Non vedo alcun danno nel far girare i tuoi metodi o persino le tue classi se vuoi farlo, a parte il fatto che può rendere il codice un po 'confuso. Inizia a diventare un po 'più rischioso quando si scorre il codice che è controllato da qualcun altro - ed è esattamente la situazione in cui lo swizzling sembra più allettante.
Caleb,

Apple non è l'unico fornitore di classi di base che può essere modificato tramite siwzzling. La tua risposta dovrebbe essere modificata per includere tutti.
AR

@AR Forse, ma la domanda è taggata iOS e iPhone, quindi dovremmo considerarla in quel contesto. Dato che le app iOS devono collegare staticamente qualsiasi libreria e framework diversi da quelli forniti da iOS, Apple è in effetti l'unica entità in grado di modificare l'implementazione delle classi framework senza una partecipazione degli sviluppatori di app. Ma sono d'accordo sul fatto che problemi simili interessano altre piattaforme e direi che i consigli possono essere generalizzati oltre a sfrigolare. In generale, il consiglio è: tieni le mani per te. Evita di modificare i componenti che altre persone mantengono.
Caleb,

Il metodo sfrigolare può essere così pericoloso. ma quando escono i quadri non omettono completamente quelli precedenti. devi ancora aggiornare il framework di base previsto dalla tua app. e quell'aggiornamento interromperebbe la tua app. è probabile che non sia un grosso problema quando si aggiorna iOS. Il mio modello di utilizzo era quello di sovrascrivere il reuseIdentifier predefinito. Dal momento che non avevo accesso alla variabile _reuseIdentifier e non volevo sostituirla a meno che non fosse fornita la mia unica soluzione era quella di sottoclassare tutti e fornire l'identificatore di riutilizzo, o swizzle e override se zero. Il tipo di implementazione è fondamentale ...
The Lazy Coder

5

Utilizzato con cura e saggezza, può portare a un codice elegante, ma di solito, porta solo a codice confuso.

Dico che dovrebbe essere vietato, a meno che tu non sappia che presenta un'opportunità molto elegante per un particolare compito di progettazione, ma devi sapere chiaramente perché si applica bene alla situazione e perché le alternative non funzionano elegantemente per la situazione .

Ad esempio, una buona applicazione del metodo swizzling è un swizzling, che è il modo in cui ObjC implementa l'osservazione del valore chiave.

Un cattivo esempio potrebbe essere affidarsi al metodo di swizzling come mezzo per estendere le tue classi, il che porta a un accoppiamento estremamente elevato.


1
-1 per la menzione di KVO, nel contesto del metodo frizzante . isa swizzling è solo qualcos'altro.
Nikolai Ruhe,

@NikolaiRuhe: non esitate a fornire riferimenti in modo che io possa essere illuminato.
Arafangion

Ecco un riferimento ai dettagli di implementazione di KVO . Il metodo swizzling è descritto su CocoaDev.
Nikolai Ruhe,

5

Anche se ho usato questa tecnica, vorrei sottolineare che:

  • Offusca il tuo codice perché può causare effetti collaterali non documentati, sebbene desiderati. Quando si legge il codice, potrebbe non essere a conoscenza del comportamento dell'effetto collaterale richiesto, a meno che non si ricordi di cercare nella base di codice per vedere se è stato spostato. Non sono sicuro di come alleviare questo problema perché non è sempre possibile documentare ogni luogo in cui il codice dipende dall'effetto collaterale swizzled.
  • Può rendere il codice meno riutilizzabile perché qualcuno che trova un segmento di codice che dipende dal comportamento swizzled che vorrebbero usare altrove non può semplicemente tagliarlo e incollarlo in qualche altra base di codice senza trovare e copiare il metodo swizzled.

4

Sento che il pericolo più grande è quello di creare molti effetti collaterali indesiderati, completamente per caso. Questi effetti collaterali possono presentarsi come "bug" che a loro volta ti portano sulla strada sbagliata per trovare la soluzione. Nella mia esperienza, il pericolo è il codice illeggibile, confuso e frustrante. Un po 'come quando qualcuno abusa dei puntatori a funzioni in C ++.


Ok, quindi il problema è che è difficile eseguire il debug. Il problema è causato dal fatto che i revisori non si rendono conto / dimenticano che il codice è stato spostato e stanno cercando nel posto sbagliato durante il debug.
Robert,

3
Sì praticamente. Inoltre, se abusato, puoi entrare in una catena infinita o in una rete di vortici. Immagina cosa potrebbe accadere quando ne cambi solo uno ad un certo punto in futuro? Ho paragonato l'esperienza di impilare tazze da tè o suonare "code jenga"
AR

4

Potresti finire con un codice dall'aspetto strano come

- (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];

dal vero codice di produzione relativo ad alcune magie dell'interfaccia utente.


3

Il metodo swizzling può essere molto utile nei test unitari.

Ti permette di scrivere un oggetto finto e di usare quell'oggetto finto al posto dell'oggetto reale. Il codice per rimanere pulito e il test dell'unità ha un comportamento prevedibile. Supponiamo che tu voglia testare del codice che utilizza CLLocationManager. Il test unitario potrebbe far girare startUpdatingLocation in modo da fornire al delegato un set di posizioni predeterminato e il codice non dovrebbe cambiare.


isa-swizzling sarebbe una scelta migliore.
vikingosegundo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.