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.
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.
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:eforwardInvocation:— é 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.

