Swift e Objective-C

As 3 etapas do encaminhamento de mensagens em Objective-C

Uma das mensagens mais comuns nos logs de crash do iOS é unrecognized selector sent to instance.

5 min de leitura
Imagem de capa de As 3 etapas do encaminhamento de mensagens em Objective-C

Uma das mensagens mais comuns nos logs de crash do iOS é unrecognized selector sent to instance.

Mas esse crash não é, na verdade, uma “morte instantânea”. Antes de causar o crash, o runtime do Objective-C dá ao objeto três oportunidades. Esse processo de recuperação é o encaminhamento de mensagens (Message Forwarding).

No artigo anterior sobre objc_msgSend, expliquei que “quando a busca pelo método falha, as 3 etapas de encaminhamento são executadas”. Neste artigo, analisamos cada etapa no código. O ponto central é entender por que a etapa 2 é chamada de Fast Forwarding e por que a etapa 3 precisa de uma assinatura de método.


Visão geral: três oportunidades e seus custos

Se a busca pelo método chegar à classe mais alta sem encontrar o IMP, um ponteiro de função que aponta para a implementação real do método, o runtime fará as perguntas abaixo na ordem.

Etapa Método Pergunta Custo
1. Resolução dinâmica de método resolveInstanceMethod: “Quer adicionar o método agora?” Baixo
2. Receptor alternativo forwardingTargetForSelector: “Existe algum objeto que possa recebê-lo no seu lugar?” Baixo (Fast)
3. Encaminhamento completo methodSignatureForSelector: + forwardInvocation: “Vou entregar a mensagem inteira; quer tentar lidar com ela?” Alto

A ordem também é a ordem de custo. O runtime tenta primeiro o método mais barato; se todos forem recusados, doesNotRecognizeSelector: é chamado e o famoso crash acontece.


Etapa 1: resolveInstanceMethod: — Adicionando o método em tempo de execução

A primeira pergunta chega como uma chamada de método de classe. Se você adicionar a implementação com class_addMethod e retornar YES, o envio da mensagem recomeçará desde o início e dessa vez terá sucesso.

void dynamicIMP(id self, SEL _cmd) {
    NSLog(@"Implementação adicionada dinamicamente");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

O terceiro argumento, "v@:", é a codificação de tipos. O retorno é void (v), o receptor é id (@) e o seletor é (:). Isso revela que todo método Objective-C recebe dois argumentos ocultos, self e _cmd.

Core Data é um usuário representativo dessa etapa. Propriedades declaradas com @dynamic não têm acessador em tempo de compilação; ele é criado dinamicamente nesta etapa quando a propriedade é chamada pela primeira vez.

Há um detalhe importante: resolveInstanceMethod: pode ser chamado mesmo quando não há encaminhamento. Ele também é invocado quando respondsToSelector: ou KVC (Key-Value Coding) consultam métodos internamente, então registrar logs aqui gera muito mais ocorrências do que o esperado.


Etapa 2: forwardingTargetForSelector: — A verdadeira natureza do Fast Forwarding

A segunda pergunta é: “Se você não consegue processar, pode indicar um objeto que faça isso no seu lugar?”

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.helper respondsToSelector:aSelector]) {
        return self.helper;
    }
    return [super forwardingTargetForSelector:aSelector];
}

Ao retornar um objeto diferente de nil, a mensagem inteira é enviada novamente a esse objeto. Comparada à etapa 3, fica claro por que essa etapa é chamada de Fast Forwarding: ela não cria um objeto NSInvocation, apenas troca o receptor, terminando com custo quase igual ao envio normal de mensagens.

Essa também é uma etapa com usos práticos bem claros.

  • Simulação de herança múltipla: delegar seletores para vários objetos helper permite combinar capacidades de várias classes sem herança.
  • weak proxy: o objeto intermediário que quebra o retain cycle do NSTimer usa este ponto — mais precisamente, o encaminhamento do NSProxy.
  • Rede de segurança da API: código defensivo que direciona métodos disponíveis apenas em versões novas do OS para um objeto alternativo em versões antigas.
Diagrama do fluxo de ramificação das 3 etapas de encaminhamento após a falha na busca do objc_msgSend, de resolveInstanceMethod a forwardInvocation
Se qualquer uma das três etapas tiver sucesso, não haverá crash

Etapa 3: Encaminhamento completo — Por que a assinatura vem primeiro

Se a etapa 2 for recusada, o runtime usa seu último recurso. Nesta etapa, é necessário sobrescrever dois métodos em conjunto, não apenas um.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
    if (!sig) {
        sig = [self.target methodSignatureForSelector:aSelector];
    }
    return sig;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    for (id target in self.targets) {
        if ([target respondsToSelector:invocation.selector]) {
            [invocation invokeWithTarget:target];
        }
    }
}

Por que a assinatura vem primeiro? Para empacotar a mensagem em um objeto NSInvocation, o runtime precisa saber quantos argumentos existem e quantos bytes cada um ocupa. Se methodSignatureForSelector: não retornar uma assinatura válida, forwardInvocation: nem será chamado e o fluxo irá direto para um crash.

Com um NSInvocation em mãos, é possível fazer muito mais. Você pode alterar argumentos ou, como no exemplo acima, enviar a mesma mensagem para vários objetos (delegado multicast), além de registrar uma resposta para reproduzi-la depois. O prepareWithInvocationTarget: do NSUndoManager usa exatamente essa abordagem: captura a chamada do método a ser desfeita como NSInvocation e a reproduz no undo.

É a opção mais flexível, mas também a mais cara. Como criar um NSInvocation e empacotar argumentos tem custo, a documentação da Apple afirma explicitamente que é “muito mais lento que o envio normal de mensagens”. Em caminhos críticos para desempenho, o correto é parar na etapa 2.


Armadilha: respondsToSelector: não conhece o encaminhamento

Mesmo um objeto que processa corretamente uma mensagem por encaminhamento responde NO quando consultado por respondsToSelector:. A busca pelo seletor verifica apenas a lista de métodos, enquanto o caminho de encaminhamento só funciona quando a mensagem é realmente enviada.

Por isso, para criar corretamente um proxy baseado em encaminhamento, também é preciso sobrescrever respondsToSelector: e fazê-lo sustentar a mentira: “Eu posso receber essa mensagem”. No mundo Objective-C, em que verificações de delegado (if ([delegate respondsToSelector:...])) são comuns, esquecer isso cria o mistério de um encaminhamento pronto que nunca chega a ser chamado.

Isso também explica por que NSProxy é uma classe raiz independente que não herda de NSObject. Quanto mais métodos herdados houver, mais mensagens serão tratadas por ela mesma sem chegar ao encaminhamento; por isso foi criada uma classe separada, deixando apenas a estrutura básica. Esse assunto será tratado em um artigo sobre NSProxy.

Ilustração de Fast Forwarding mostrando um robô entregando um envelope de mensagem a outro robô, o receptor alternativo
A etapa 2 encaminha a mensagem trocando apenas o receptor, por isso é rápida

Resumo

  • Um crash de unrecognized selector não é morte instantânea, mas o resultado de todas as 3 etapas de recuperação falharem.
  • A etapa 1, resolveInstanceMethod:, adiciona o método na hora — @dynamic e Core Data funcionam aqui.
  • A etapa 2, forwardingTargetForSelector:, é o Fast Forwarding, que apenas troca o receptor e termina com baixo custo, sem NSInvocation.
  • A etapa 3 é o conjunto de methodSignatureForSelector: e forwardInvocation: — é preciso ter uma assinatura para criar um NSInvocation.
  • Delegados multicast, NSUndoManager e weak proxy são aplicações construídas sobre essa estrutura.
  • Ao criar um proxy de encaminhamento, não se esqueça de sobrescrever respondsToSelector:.

Ao entender por que Swift escolhe a dispatch estática por padrão, abrindo mão dessa flexibilidade, e por que ainda é possível voltar a esse mundo por meio de @objc dynamic, as filosofias de design das duas linguagens se tornam muito mais claras.

Continue lendo