Eu acho que essa é uma pergunta realmente ótima, e é uma pena que, em vez de abordar a questão real, a maioria das respostas tenha contornado a questão e simplesmente dito para não usar swizzling.
Usar o método escaldante é como usar facas afiadas na cozinha. Algumas pessoas têm medo de facas afiadas porque pensam que se cortarão mal, mas a verdade é que facas afiadas são mais seguras .
O método swizzling pode ser usado para escrever um código melhor, mais eficiente e mais sustentável. Também pode ser abusado e levar a erros horríveis.
fundo
Como em todos os padrões de design, se estivermos totalmente cientes das conseqüências do padrão, poderemos tomar decisões mais informadas sobre se deve ou não usá-lo. Singletons são um bom exemplo de algo que é bastante controverso e por boas razões - eles são realmente difíceis de implementar adequadamente. Muitas pessoas ainda optam por usar singletons, no entanto. O mesmo pode ser dito sobre o swizzling. Você deve formar sua própria opinião depois de entender completamente o que é bom e o que é ruim.
Discussão
Aqui estão algumas das armadilhas do método swizzling:
- O método swizzling não é atômico
- Altera o comportamento do código não proprietário
- Possíveis conflitos de nomenclatura
- Swizzling altera os argumentos do método
- A ordem dos swizzles é importante
- Difícil de entender (parece recursivo)
- Difícil de depurar
Esses pontos são todos válidos e, ao abordá-los, podemos melhorar nosso entendimento do método swizzling, bem como a metodologia usada para alcançar o resultado. Vou levar cada um de cada vez.
O método swizzling não é atômico
Ainda estou para ver uma implementação do método swizzling que é seguro usar simultaneamente 1 . Na verdade, isso não é um problema em 95% dos casos em que você deseja usar o método swizzling. Geralmente, você simplesmente deseja substituir a implementação de um método e deseja que essa implementação seja usada durante toda a vida útil do seu programa. Isso significa que você deve executar seu método rapidamente +(void)load
. O load
método de classe é executado em série no início do seu aplicativo. Você não terá nenhum problema com a simultaneidade se fizer sua swizzling aqui. Se você fizer swizzle +(void)initialize
, no entanto, poderá acabar com uma condição de corrida em sua implementação swizzling, e o tempo de execução poderá terminar em um estado estranho.
Altera o comportamento do código não proprietário
Esse é um problema com o swizzling, mas é o tipo de questão. O objetivo é poder alterar esse código. A razão pela qual as pessoas apontam isso como um grande problema é porque você não está apenas mudando as coisas para a única instância da NSButton
qual deseja mudar as coisas, mas para todas as NSButton
instâncias em seu aplicativo. Por esse motivo, você deve ter cuidado ao swizzle, mas não precisa evitá-lo completamente.
Pense desta maneira ... se você substituir um método em uma classe e não chamar o método de superclasse, poderá causar problemas. Na maioria dos casos, a superclasse espera que esse método seja chamado (a menos que esteja documentado em contrário). Se você aplicar esse mesmo pensamento ao swizzling, cobrirá a maioria dos problemas. Sempre chame a implementação original. Caso contrário, você provavelmente está mudando demais para estar seguro.
Possíveis conflitos de nomenclatura
Os conflitos de nomes são um problema em todo o cacau. Geralmente, prefixamos nomes de classes e métodos em categorias. Infelizmente, conflitos de nomes são uma praga em nosso idioma. No caso de swizzling, no entanto, eles não precisam ser. Só precisamos mudar um pouco a maneira como pensamos sobre o método swizzling. A maioria dos swizzling é feita assim:
@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
Isso funciona muito bem, mas o que aconteceria se my_setFrame:
fosse definido em outro lugar? Esse problema não é exclusivo do swizzling, mas podemos contorná-lo de qualquer maneira. A solução alternativa tem um benefício adicional de abordar outras armadilhas também. Aqui está o que fazemos:
@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
Embora pareça um pouco menos com o Objective-C (já que ele usa ponteiros de função), evita qualquer conflito de nomeação. Em princípio, está fazendo exatamente a mesma coisa que o swizzling padrão. Isso pode ser uma mudança para as pessoas que usam o swizzling, já que há um tempo já foi definido, mas no final, acho que é melhor. O método swizzling é definido assim:
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 renomeando métodos altera os argumentos do método
Este é o grande problema em minha mente. Esta é a razão pela qual o método padrão swizzling não deve ser feito. Você está alterando os argumentos passados para a implementação do método original. É aqui que acontece:
[self my_setFrame:frame];
O que esta linha faz é:
objc_msgSend(self, @selector(my_setFrame:), frame);
O qual usará o tempo de execução para procurar a implementação do my_setFrame:
. Depois que a implementação é encontrada, ela invoca a implementação com os mesmos argumentos que foram fornecidos. A implementação encontrada é a implementação original setFrame:
, portanto prossegue e chama assim, mas o _cmd
argumento não é o setFrame:
que deveria ser. É agora my_setFrame:
. A implementação original está sendo chamada com um argumento que nunca esperava que fosse recebido. Isso não é bom.
Existe uma solução simples - use a técnica alternativa de swizzling definida acima. Os argumentos permanecerão inalterados!
A ordem dos swizzles é importante
A ordem na qual os métodos são misturados é importante. Supondo que setFrame:
seja definido apenas NSView
, imagine esta ordem das coisas:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
O que acontece quando o método on NSButton
é misturado? Bem mais swizzling irá garantir que ele não está substituindo a implementação de setFrame:
para todos os pontos de vista, por isso vai puxar para cima o método de instância. Isso usará a implementação existente para redefinir setFrame:
na NSButton
classe, para que a troca de implementações não afete todas as visualizações. A implementação existente é a definida em NSView
. A mesma coisa acontecerá quando o swizzling NSControl
(novamente usando a NSView
implementação).
Quando você setFrame:
clica em um botão, ele chama o método swizzled e, em seguida, pula diretamente para o setFrame:
método originalmente definido em NSView
. As implementações swizzled NSControl
e NSView
não serão chamadas.
Mas e se a ordem 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:)];
Desde o swizzling vista que ocorrer primeiro, o swizzling controle será capaz de puxar para cima o método certo. Da mesma forma, uma vez que o swizzling controle foi antes de o botão swizzling, o botão vai puxar para cima implementação swizzled do controle de setFrame:
. Isso é um pouco confuso, mas esta é a ordem correta. Como podemos garantir essa ordem das coisas?
Mais uma vez, basta usar load
para mexer as coisas. Se você entrar load
e fizer apenas alterações na classe que está sendo carregada, estará seguro. O load
método garante que o método de carregamento de superclasse será chamado antes de qualquer subclasse. Obteremos a ordem exata exata!
Difícil de entender (parece recursivo)
Olhando para um método swizzled tradicionalmente definido, acho realmente difícil dizer o que está acontecendo. Mas, olhando para a maneira alternativa como fizemos swizzling acima, é muito fácil de entender. Este já foi resolvido!
Difícil de depurar
Uma das confusões durante a depuração é ver um histórico estranho, onde os nomes misturados são misturados e tudo fica confuso na sua cabeça. Novamente, a implementação alternativa trata disso. Você verá funções claramente nomeadas em backtraces. Ainda assim, pode ser difícil depurar o swizzling, porque é difícil lembrar qual o impacto do swizzling. Documente bem seu código (mesmo que você ache que é o único que o verá). Siga as boas práticas e você ficará bem. Não é mais difícil depurar do que o código multiencadeado.
Conclusão
O método swizzling é seguro se usado corretamente. Uma medida simples de segurança que você pode adotar é apenas entrar na água load
. Como muitas coisas na programação, isso pode ser perigoso, mas entender as consequências permitirá que você a use corretamente.
1 Usando o método swizzling definido acima, você pode tornar as coisas mais seguras se usar trampolins. Você precisaria de dois trampolins. No início do método, você teria que atribuir o ponteiro da função store
, a uma função que girava até o endereço para o qual o store
apontado foi alterado. Isso evitaria qualquer condição de corrida na qual o método swizzled fosse chamado antes que você pudesse definir o store
ponteiro da função. Você precisaria usar um trampolim no caso em que a implementação ainda não esteja definida na classe e ter a pesquisa de trampolim e chamar o método de superclasse corretamente. Definir o método para procurar dinamicamente a super implementação garantirá que a ordem das chamadas swizzling não seja importante.