Quais são os perigos do método Swizzling no Objective-C?

235

Ouvi pessoas afirmarem que o método swizzling é uma prática perigosa. Até o nome swizzling sugere que é um truque.

O método Swizzling está modificando o mapeamento para que o seletor de chamada A realmente invoque a implementação B. Um uso disso é estender o comportamento das classes de código fechado.

Podemos formalizar os riscos para que qualquer pessoa que esteja decidindo usar swizzling possa tomar uma decisão informada se vale a pena pelo que está tentando fazer.

Por exemplo

  • Nomeando colisões : se a classe posteriormente estender sua funcionalidade para incluir o nome do método que você adicionou, causará uma enorme variedade de problemas. Reduza o risco nomeando de forma sensata os métodos swizzled.
Robert
fonte
Já não é muito bom para obter outra coisa que você espera de código que você leu
Martin Babacaev
Isso soa como uma forma impura de fazer o que decoradores python fazer muito limpa
Claudiu

Respostas:

439

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 loadmé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 NSButtonqual deseja mudar as coisas, mas para todas as NSButtoninstâ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 _cmdargumento 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 NSButtonclasse, 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 NSViewimplementaçã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 NSControle NSViewnã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 loadpara mexer as coisas. Se você entrar loade fizer apenas alterações na classe que está sendo carregada, estará seguro. O loadmé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 storeapontado foi alterado. Isso evitaria qualquer condição de corrida na qual o método swizzled fosse chamado antes que você pudesse definir o storeponteiro 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.

wbyoung
fonte
24
Resposta surpreendentemente informativa e tecnicamente sólida. A discussão é realmente interessante. Obrigado por tomar o tempo para escrever isso.
Ricardo Sanchez-Saez
Você poderia apontar se o swizzling em sua experiência é aceitável nas lojas de aplicativos. Estou direcionando você para stackoverflow.com/questions/8834294 também.
usar o seguinte
3
Swizzling pode ser usado na App Store. Muitos aplicativos e estruturas fazem (o nosso incluído). Tudo o que está acima ainda é válido e você não pode usar métodos privados (na verdade, você pode tecnicamente, mas corre o risco de ser rejeitado e os perigos disso são para um segmento diferente).
Wdow #
Resposta incrível. Mas por que fazer swizzling em class_swizzleMethodAndStore () em vez de diretamente em + swizzle: with: store :? Por que essa função adicional?
Frizlab
2
@Frizlab good question! É realmente apenas uma coisa de estilo. Se você estiver escrevendo um monte de código que trabalha diretamente com a API de tempo de execução Objective-C (em C), é bom poder chamá-lo de estilo C por consistência. A única razão pela qual consigo pensar além disso é que, se você escreveu algo em C puro, é ainda mais exigível. Não há razão para que você não possa fazer tudo isso no método Objective-C.
wbyoung
12

Primeiro, definirei exatamente o que quero dizer com método swizzling:

  • Reencaminhe todas as chamadas que foram originalmente enviadas para um método (chamado A) para um novo método (chamado B).
  • Nós possuímos o Método B
  • Nós não possuímos o método A
  • O método B faz algum trabalho e depois chama o método A.

O método swizzling é mais geral do que isso, mas é esse o caso em que estou interessado.

Perigos:

  • Mudanças na classe original . Nós não possuímos a classe que estamos nadando. Se a classe mudar, nosso swizzle pode parar de funcionar.

  • Difícil de manter . Você não precisa apenas escrever e manter o método swizzled. você deve escrever e manter o código que pré-forma o swizzle

  • Difícil de depurar . É difícil acompanhar o fluxo de um swizzle, algumas pessoas podem nem perceber que o swizzle foi pré-formado. Se houver erros introduzidos no swizzle (talvez deva-se a alterações na classe original), eles serão difíceis de resolver.

Em resumo, você deve manter o mínimo de swizzling e considerar como as alterações na classe original podem afetar seu swizzle. Além disso, você deve comentar e documentar claramente o que está fazendo (ou apenas evitá-lo totalmente).

Robert
fonte
@ todo mundo Eu combinei suas respostas em um bom formato. Sinta-se livre para editar / adicionar a ele. Obrigado pela sua contribuição.
Robert
Sou fã da sua psicologia. "Com grande poder swizzling, vem grande responsabilidade swizzling."
Jacksonkr
7

Não é o próprio swizzling que é realmente perigoso. O problema é que, como você diz, é freqüentemente usado para modificar o comportamento das classes de estrutura. Supõe-se que você saiba algo sobre o funcionamento dessas classes particulares que é "perigoso". Mesmo que suas modificações funcionem hoje, sempre há uma chance de que a Apple mude de classe no futuro e faça com que suas modificações sejam interrompidas. Além disso, se muitos aplicativos diferentes fazem isso, torna muito mais difícil para a Apple alterar a estrutura sem interromper muito software existente.

Caleb
fonte
Eu diria que esses Devs devem colher o que plantam - eles acoplaram fortemente seu código a uma implementação específica. Portanto, a culpa é deles se essa implementação mudar de maneira sutil que não deveria ter quebrado seu código, se não fosse a decisão explícita de acoplar o código tão firmemente quanto eles.
Arafangion
Claro, mas digamos que um aplicativo muito popular seja quebrado na próxima versão do iOS. É fácil dizer que a culpa é do desenvolvedor e eles devem saber melhor, mas isso faz a Apple parecer ruim aos olhos do usuário. Não vejo mal algum em manipular seus próprios métodos ou até mesmo classes, se você quiser fazer isso, além de seu argumento de que ele pode tornar o código um tanto confuso. Começa a ficar um pouco mais arriscado quando você digita um código que é controlado por outra pessoa - e essa é exatamente a situação em que o processo parece mais tentador.
Caleb
A Apple não é o único fornecedor de classes base que pode ser modificado por meio de silenciamento. Sua resposta deve ser editada para incluir todos.
AR
@AR Talvez, mas a pergunta esteja marcada com iOS e iPhone, portanto devemos considerá-la nesse contexto. Como os aplicativos iOS devem vincular estaticamente quaisquer bibliotecas e estruturas diferentes das fornecidas pelo iOS, a Apple é de fato a única entidade que pode alterar a implementação das classes de estrutura sem a participação de desenvolvedores de aplicativos. Mas eu concordo com o fato de que problemas semelhantes afetam outras plataformas, e eu diria que o conselho também pode ser generalizado além de um swizzling. De forma geral, o conselho é: mantenha as mãos para si mesmo. Evite modificar componentes que outras pessoas mantêm.
Caleb
Método swizzling pode ser tão perigoso. mas à medida que as estruturas são exibidas, elas não omitem completamente as anteriores. você ainda precisa atualizar a estrutura base que seu aplicativo espera. e essa atualização quebraria seu aplicativo. provavelmente não será um problema tão grande quando o iOS for atualizado. Meu padrão de uso era substituir o reuseIdentifier padrão. Como eu não tinha acesso à variável _reuseIdentifier e não queria substituí-la, a menos que não fosse fornecida, minha única solução era subclassificar cada uma delas e fornecer o identificador de reutilização ou swizzle e substituir, se nulo. O tipo de implementação é a chave ...
The Lazy Coder
5

Usado com cuidado e sabedoria, pode levar a um código elegante, mas geralmente leva a um código confuso.

Eu digo que deve ser banido, a menos que você saiba que ela apresenta uma oportunidade muito elegante para uma tarefa de design específica, mas você precisa saber claramente por que ela se aplica bem à situação e por que alternativas não funcionam elegantemente para a situação .

Por exemplo, uma boa aplicação do método swizzling é um swizzling, que é como a ObjC implementa a Observação de Valor Chave.

Um mau exemplo pode ser confiar no método swizzling como um meio de estender suas classes, o que leva a um acoplamento extremamente alto.

Arafangion
fonte
1
-1 para a menção de KVO - no contexto de método swizzling . isa swizzling é apenas outra coisa.
Nikolai Ruhe
@NikolaiRuhe: Sinta-se à vontade para fornecer referências para que eu possa ser esclarecido.
Arafangion
Aqui está uma referência aos detalhes de implementação do KVO . O método swizzling é descrito no CocoaDev.
Nikolai Ruhe
5

Embora eu tenha usado essa técnica, gostaria de salientar que:

  • Ele ofusca o seu código porque pode causar efeitos colaterais não documentados, embora desejados. Quando alguém lê o código, ele / ela pode não ter conhecimento do comportamento de efeito colateral necessário, a menos que ele se lembre de pesquisar na base de código para ver se foi swizzled. Não sei ao certo como aliviar esse problema, porque nem sempre é possível documentar todos os lugares em que o código depende do comportamento deslocado do efeito colateral.
  • Isso pode tornar seu código menos reutilizável porque alguém que encontra um segmento de código que depende do comportamento swizzled que eles gostariam de usar em outros lugares não pode simplesmente recortá-lo e colá-lo em outra base de código sem também encontrar e copiar o método swizzled.
user3288724
fonte
4

Sinto que o maior perigo está em criar muitos efeitos colaterais indesejados, completamente por acidente. Esses efeitos colaterais podem se apresentar como 'bugs' que, por sua vez, levam você ao caminho errado para encontrar a solução. Na minha experiência, o perigo é código ilegível, confuso e frustrante. É como quando alguém usa demais os ponteiros de função em C ++.

AR
fonte
Ok, então o problema é que é difícil depurar. O problema é causado porque os revisores não percebem / esquecem que o código foi modificado e está procurando o lugar errado durante a depuração.
Robert
3
Sim, praticamente. Além disso, quando usado em excesso, você pode entrar em uma cadeia interminável ou teia de swizzles. Imagine o que pode acontecer quando você muda apenas um deles em algum momento no futuro? Eu comparo a experiência para empilhamento xícaras de chá ou jogar 'jenga código'
AR
4

Você pode acabar com um código com aparência estranha, como

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

do código de produção real relacionado a alguma mágica da interface do usuário.

quantumpotato
fonte
3

O método swizzling pode ser muito útil nos testes de unidade.

Ele permite que você escreva um objeto simulado e tenha esse objeto simulado em vez do objeto real. Seu código permanece limpo e seu teste de unidade tem um comportamento previsível. Digamos que você queira testar algum código que usa CLLocationManager. Seu teste de unidade pode fazer o swizzle startUpdatingLocation para alimentar um conjunto predeterminado de locais para o seu delegado e seu código não precisa ser alterado.

user3288724
fonte
isa-swizzling seria uma escolha melhor.
22414 vikingosegundo