Una de las frases más frecuentes en los registros de fallos de iOS es unrecognized selector sent to instance.
Pero este fallo no es, en realidad, una «muerte instantánea». Antes de provocar el fallo, el runtime de Objective-C le da al objeto tres oportunidades. Este proceso de recuperación es el reenvío de mensajes (Message Forwarding).
En el artículo anterior sobre objc_msgSend expliqué que, «si falla la búsqueda del método, se ejecutan las 3 etapas de reenvío». En este artículo desglosamos las tres con código. La clave es entender por qué la etapa 2 recibe el apodo de Fast Forwarding y por qué la etapa 3 necesita una firma de método.
Visión general: tres oportunidades y su coste
Si la búsqueda del método llega hasta la clase superior sin encontrar el IMP, un puntero de función que apunta a la implementación real del método, el runtime pregunta en el siguiente orden.
| Etapa | Método | Pregunta | Coste |
|---|---|---|---|
| 1. Resolución dinámica de métodos | resolveInstanceMethod: |
«¿Quieres añadir el método ahora?» | Bajo |
| 2. Receptor alternativo | forwardingTargetForSelector: |
«¿Hay algún objeto que pueda recibirlo en su lugar?» | Bajo (Fast) |
| 3. Reenvío completo | methodSignatureForSelector: + forwardInvocation: |
«Te paso el mensaje entero; ¿quieres encargarte?» | Alto |
El orden también es el orden del coste. El runtime prueba primero el método más barato; si todos son rechazados, se llama a doesNotRecognizeSelector: y aparece el famoso fallo.
Etapa 1: resolveInstanceMethod: — Añadir el método sobre la marcha
La primera pregunta llega como una llamada a un método de clase. Si añades la implementación con class_addMethod y devuelves YES, el envío del mensaje comienza de nuevo y esta vez tiene éxito.
void dynamicIMP(id self, SEL _cmd) {
NSLog(@"Implementación añadida dinámicamente");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
El tercer argumento, "v@:", es la codificación de tipos. El valor devuelto es void (v), el receptor es id (@) y el selector es (:). Aquí se revela que todo método de Objective-C recibe dos argumentos ocultos, self y _cmd.
Core Data es un usuario representativo de esta etapa. Las propiedades declaradas con @dynamic no tienen accesor en tiempo de compilación; se crean dinámicamente en esta etapa cuando se llaman por primera vez.
Hay una advertencia importante: resolveInstanceMethod: puede llamarse aunque no haya reenvío. También se invoca cuando respondsToSelector: o KVC (Key-Value Coding) buscan métodos internamente, por lo que registrar aquí genera muchos más mensajes de los esperados.
Etapa 2: forwardingTargetForSelector: — La verdadera naturaleza de Fast Forwarding
La segunda pregunta es: «Si no puedes procesarlo, ¿puedes indicar un objeto que lo haga en tu lugar?»
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.helper respondsToSelector:aSelector]) {
return self.helper;
}
return [super forwardingTargetForSelector:aSelector];
}
Si devuelves un objeto distinto de nil, el mensaje completo se vuelve a enviar a ese objeto. Comparada con la etapa 3, la razón de llamarla Fast Forwarding es clara: no crea un objeto NSInvocation, solo cambia el receptor, y termina con un coste casi igual al del envío normal de mensajes.
También es una etapa con usos prácticos muy claros.
- Simular herencia múltiple: delegar selectores en varios objetos helper permite combinar capacidades de varias clases sin herencia.
- weak proxy: el objeto intermediario que rompe el retain cycle de NSTimer utiliza este punto, concretamente el reenvío de NSProxy.
- Red de seguridad de API: código defensivo que dirige métodos disponibles solo en sistemas nuevos a un objeto alternativo en sistemas antiguos.
Etapa 3: Reenvío completo — Por qué la firma debe ir primero
Si se rechaza la etapa 2, el runtime usa su último recurso. En esta etapa debes sobrescribir dos métodos como conjunto, no solo uno.
- (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 qué va primero la firma? Para empaquetar el mensaje en un objeto NSInvocation, el runtime debe saber cuántos argumentos hay y cuántos bytes ocupa cada uno. Si methodSignatureForSelector: no devuelve una firma válida, forwardInvocation: ni siquiera se llama y el proceso termina directamente en un fallo.
Con un NSInvocation en tus manos puedes hacer mucho más. Puedes cambiar argumentos o, como en el ejemplo anterior, difundir el mismo mensaje a varios objetos (delegado multicast), y también registrar una respuesta para reproducirla después. El prepareWithInvocationTarget: de NSUndoManager funciona exactamente así: captura la llamada del método que se deshará como NSInvocation y la reproduce al hacer undo.
Es la opción más flexible, pero también la más costosa. Crear NSInvocation y empaquetar argumentos tiene un coste, por lo que la documentación de Apple afirma explícitamente que es «mucho más lento que el envío normal de mensajes». En rutas críticas para el rendimiento, lo correcto es terminar en la etapa 2.
Trampa: respondsToSelector: no conoce el reenvío
Incluso un objeto que procesa correctamente un mensaje mediante reenvío responde NO si se le pregunta mediante respondsToSelector:. La búsqueda del selector solo consulta la lista de métodos; la ruta de reenvío funciona al enviar realmente el mensaje.
Por eso, para crear correctamente un proxy basado en reenvío, también debes sobrescribir respondsToSelector: y hacer que confirme la mentira: «Puedo recibir ese mensaje». En el mundo de Objective-C, donde son habituales las comprobaciones de delegados (if ([delegate respondsToSelector:...])), omitirlo produce el misterio de que el reenvío está listo, pero la llamada nunca llega.
Esto también explica por qué NSProxy es una clase raíz independiente que no hereda de NSObject. Cuantos más métodos heredados haya, más mensajes procesará por sí misma sin llegar al reenvío; por eso se creó una clase aparte con solo la estructura básica. Trataremos este tema en otro artículo sobre NSProxy.
Resumen
- Un fallo por unrecognized selector no es una muerte instantánea, sino el resultado de que fallen las tres etapas de recuperación.
- La etapa 1,
resolveInstanceMethod:, añade el método sobre la marcha — aquí funcionan @dynamic y Core Data. - La etapa 2,
forwardingTargetForSelector:, es Fast Forwarding: solo cambia el receptor y termina con bajo coste, sin NSInvocation. - La etapa 3 es el conjunto de
methodSignatureForSelector:yforwardInvocation:: hace falta una firma para crear NSInvocation. - Los delegados multicast, NSUndoManager y los weak proxy son aplicaciones construidas sobre esta estructura.
- Al crear un proxy de reenvío, no olvides sobrescribir
respondsToSelector:.
Si además entiendes por qué Swift elige la dispatch estática de forma predeterminada en lugar de esta flexibilidad, y por qué aun así puedes volver a este mundo mediante @objc dynamic, las filosofías de diseño de ambos lenguajes se leen de forma mucho más completa.

