[iOS] Mecanismo de mensajería y reenvío de mensajes

mecanismo de paso de mensajes

En el lenguaje OC, llamar al método de un objeto se llama paso de mensajes. Un mensaje tiene un nombre y un selector, puede aceptar parámetros y puede tener un valor de retorno.

En Objective-C, si se pasa un mensaje a un objeto, se utiliza el mecanismo de enlace dinámico para determinar el método que debe llamarse. En el nivel inferior, todos los métodos son funciones ordinarias del lenguaje C. Sin embargo, después de que el objeto recibe el mensaje, el método que se debe llamar se determina completamente durante el tiempo de ejecución e incluso se puede cambiar mientras el programa se está ejecutando. Estas características hacen que Objective- C un lenguaje de programación real, lenguaje dinámico.

Ejemplo: expresión de mensaje OC:

id returnValue = [someObject messageName:parameter];

El compilador procesará esta sección en:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

someObject se llama receptor, messageName se llama "selector", y el selector y los parámetros juntos se llaman "mensaje". Cuando el compilador ve este mensaje, lo convertirá en una llamada de función de lenguaje C estándar.

SELECCIONAR SEL

Durante la compilación, OC generará una ID única para distinguir este método según el nombre del método (incluida la secuencia de parámetros). Esta ID es de tipo SEL. Lo que debemos tener en cuenta es que siempre que los nombres de los métodos (incluidas las secuencias de parámetros) sean los mismos, sus ID serán los mismos. Entonces, ya sea una clase principal o una subclase, si el nombre es el mismo, la ID será la misma.

	SEL sell1 = @selector(eat:);
    NSLog(@"sell1:%p", sell1);
    SEL sell2 = @selector(eat);
    NSLog(@"sell2:%p", sell2);
    //sell1:0x100000f63
	//sell2:0x100000f68

Este mecanismo aumenta enormemente la flexibilidad de nuestro programa. Podemos pasar parámetros SEL a un método y dejar que el método ejecute dinámicamente un determinado método, también podemos especificar el método que debe ejecutarse a través del archivo de configuración y el programa lee la configuración. Luego, el archivo traduce la cadena del método en una variable SEL y luego envía el mensaje al objeto correspondiente.

Desde el punto de vista de la eficiencia, durante la ejecución, el método no se busca por el nombre del método sino por el ID del método, que es un número entero. Dado que la búsqueda y coincidencia de números enteros es mucho más rápida que la de cadenas, esto puede mejorar la ejecución para hasta cierto punto eficiencia

Debemos tener en cuenta que @selector equivale a traducir el nombre del método al nombre del método SEL. Solo le importa el nombre del método y la cantidad de parámetros, y no el valor de retorno ni el tipo de parámetro.

El proceso de generación de SEL es fijo, porque es solo una ID que indica el método. No importa en qué clase esté escrito el método dayin, el valor SEL es fijo.

En Runtime se mantiene una tabla SEL. Esta tabla almacena los SEL no según clases. Siempre que los mismos SEL se consideren uno, se almacenarán en la tabla. Cuando se carga el proyecto, todos los métodos se cargarán en esta tabla y los métodos generados dinámicamente también se cargarán en la tabla.

Entonces diferentes clases pueden tener el mismo método. Cuando los objetos de instancia de diferentes clases ejecutan el mismo selector, encontrarán el IMP correspondiente a su propia clase según SEL en sus respectivas listas de métodos.

IMP es esencialmente un puntero de función. La función señalada contiene una ID de objeto que recibe el mensaje, el SEL para llamar al método y algunos parámetros del método, y devuelve una ID. Por lo tanto, podemos obtener su IMP correspondiente a través de SEL. Después de obtener el puntero de función, significa que hemos obtenido la entrada de código que necesita para ejecutar el método, de modo que podamos usar este puntero de función como una llamada de función normal en lenguaje C.

objc_msgEnviar

Podemos ver que en la conversión se utiliza la función objc_msgSend, esta función toma el receptor del mensaje y el nombre del método como parámetros principales, como se muestra a continuación:

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

objc_msgSend implementa el mecanismo de enlace dinámico mediante los siguientes pasos:

  • Primero, obtenga la implementación del método señalada por el selector. Dado que el mismo método puede tener diferentes implementaciones en diferentes clases, el juicio se basa en la clase a la que pertenece el receptor.
  • En segundo lugar, pase el objeto receptor y los parámetros especificados por el método para llamar a la implementación del método.
  • Finalmente, se devuelve el valor de retorno de la implementación del método.

La clave para el paso de mensajes reside en la estructura objc_class, que tiene tres campos clave:

  • isa: puntero a la clase.
  • superclase: puntero a la clase padre.
  • MethodLists: la tabla de distribución de métodos de la clase (tabla de envío).

Cuando se crea un nuevo objeto, se le asigna memoria y se inicializan sus variables miembro. El puntero isa también se inicializará, lo que permitirá que el objeto acceda a la clase y a la cadena de herencia de la clase.

La siguiente figura muestra un diagrama esquemático del proceso de paso de mensajes:

Insertar descripción de la imagen aquí

  • Cuando se entrega un mensaje a un objeto, primero se busca en la memoria caché del sistema de ejecución objc_cache. Si lo encuentra, ejecútelo. De lo contrario, continúe con los siguientes pasos.
  • objc_msgSend obtiene la estructura de clases a través del puntero isa del objeto y luego busca el selector de métodos en la tabla de distribución de métodos MethodLists. Si no se encuentra, su clase principal se encontrará junto con la superclase de la clase y la búsqueda continuará en las listas de métodos de la tabla de distribución de la clase principal.
  • Por analogía, la cadena de herencia de la clase se remonta a la clase NSObject. Una vez que encuentre el selector, pase los parámetros correspondientes para ejecutar la implementación específica del método y agregue el método al caché objc_cache. Si al final aún no se encuentra el selector, se ingresará al proceso de reenvío de mensajes.

Análisis de código fuente

búsqueda rápida

objc_msgSend se implementa en diferentes arquitecturas: tomando arm64 como ejemplo, la implementación del código es ensamblador. ¿Por qué elegir el montaje para implementar? Es más rápido, utiliza parámetros directamente y evita la sobrecarga de copiar una gran cantidad de parámetros. Se agregará un guión bajo "_" delante de las funciones y variables globales para evitar conflictos de símbolos.

Proceso de ensamblaje:

	//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
	//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
  • Primero, comience con cmp p0,#0, donde p0 es un registro que almacena el destinatario del mensaje. Al ingresar la entrada de envío del mensaje, primero determine si el receptor del mensaje existe, de lo contrario, vuelva a ejecutar objc_msgSend.
  • b.le LNilOrTagged, b significa saltar a. le significa que si p0 es menor o igual a 0, el significado general es que si p0 es menor o igual a 0, salte a LNilOrTagged, ejecute b.eq LReturnZero para salir de esta función directamente
  • Si el receptor del mensaje no es nulo, el ensamblado continúa ejecutándose, va a CacheLookup NORMAL y busca imp en el caché.

Echemos un vistazo a la implementación específica:

//在cache中通过sel查找imp的核心流程
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

    //从x16中取出class移到x15中
	mov	x15, x16			// stash the original isa
//开始查找
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //ldr表示将一个值存入到p10寄存器中
    //x16表示p16寄存器存储的值,当前是Class
    //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
    //#define CACHE (2 * __SIZEOF_POINTER__)
    //经计算,p10就是cache
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
//真机64位看这个
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
//获取buckets
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //and表示与运算,将与上mask后的buckets值保存到p10寄存器
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
	tbnz	p11, #0, LLookupPreopt\Function
#endif
    //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
    //LSR表示逻辑向右偏移
    //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
    //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
    //整句表示_cmd & mask并保存到p12
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
	and	p12, p1, p11			// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    //去除掩码后bucket的内存平移
    //PTRSHIFT经全局搜索发现是3
    //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
    
    
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
	cmp	p9, p1				//     if (sel != _cmd) {
    
    
    //b.ne表示如果不相同则跳转到3f
	b.ne	3f				//         scan more
						//     } else {
    
    
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
    //通过p13和p10来判断是否是第一个bucket
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
    
    
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
	// keep the remaining 38 bits for the IMP offset, which may need to reach
	// across the shared cache. This offset needs to be shifted << 2. We did this
	// to give it even more reach, given the alignment of source (the class data)
	// and destination (the IMP)
	ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs << 38) | imp_offs
	cmp	x12, x17, LSR #38

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub	x0, x16, x17        		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				        // cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub x17, x16, x17               // imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

1. Proceso:

  1. Obtener punteros al caché y _bucketsAndMaybeMask;
  2. Tome los depósitos y la máscara respectivamente de _bucketsAndMaybeMask y use la máscara para calcular el subíndice hash de acuerdo con el algoritmo hash;
  3. De acuerdo con el inicio del subíndice hash obtenido y la primera dirección del depósito, extraiga el depósito correspondiente al subíndice hash;
  4. Ingrese al ciclo do- while y busque según sel en el depósito;

El caché y _bucketsAndMaybeMask se obtienen mediante traducción de memoria. Los 16 bits altos de _bucketsAndMaybeMask almacenan la máscara y los 48 bits bajos almacenan depósitos (16 bits altos | 48 bits bajos = máscara | cubos), es decir, _bucketsAndMaybeMask = máscara (16 altos ) + puntero de cubos (bajo 48 bits).

Usando el parámetro p1 (es decir, el segundo parámetro _sel) y máscara de objc_msgSend, a través del algoritmo hash, obtenemos el subíndice del depósito que debe encontrarse para almacenar sel-imp, es decir, p12 = comenzar = _sel y máscara Porque cuando se almacena sel-imp Cuando, el subíndice hash también se calcula y almacena mediante el mismo algoritmo hash.

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    
    
    return (mask_t)(uintptr_t)sel & mask;
}

De acuerdo con el inicio del subíndice hash calculado multiplicado por el tamaño de la memoria ocupada por un solo depósito, se obtiene el desplazamiento en la memoria real de la primera dirección del depósito desde el depósito señalado por el subíndice de inicio. Obtenga el depósito correspondiente al subíndice hash que comienza hasta la primera dirección + desplazamiento real. El depósito se compone de dos atributos: sel e imp. Cada atributo tiene un tamaño de 8 bytes, por lo que el tamaño del depósito es 16

En el ciclo do- while:

  • En el primer ciclo do- while, busque desde el principio -> 0. Si no hay resultados, p9 no es nulo y se inicia el segundo ciclo do- while;
  • En el segundo bucle do- while, busque nuevamente desde máscara —> 0;
  • Si este sigue siendo el caso, ejecute __objc_msgSend_uncached —> MethodTableLookup —> _lookUpImpOrForward para comenzar a buscar la lista de métodos.

Si encuentra un método en el caché, llámelo directamente, si encuentra sel, ingresará CacheHit y devolverá o llamará a imp: Devuelve o llama a la implementación del método (imp).

2. Contenido de CacheHit: El modo en la imagen de arriba representa el siguiente proceso NORMAL, autenticar y llamar a imp significa verificación e implementación del método de llamada.

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp//调用imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP//返回imp
.elseif $0 == LOOKUP// 执行__objc_msgSend_uncached,开始方法列表查找
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

3. Método no encontrado en caché

Si no se encuentra el caché, busque el siguiente depósito y realice un bucle hasta encontrar el método correspondiente. Si no se encuentra el método después del bucle, llame a __objc_msgSend_uncached.

El siguiente es el código de salto de juicio anterior:

//LGetIsaDone是一个入口
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
    //进入到缓存查找或者没有缓存查找方法的流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

__objc_msgSend_uncached código fuente ensamblado:

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

Se llama a la macro MethodTableLookup: para buscar un método en la lista de métodos.

Fíjate en su estructura:

	.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

Entre ellos, bl significa que se llama al método _lookUpImpOrForward. _lookUpImpOrForward no se puede encontrar en el ensamblado, porque la función ensambladora tiene un guión bajo más que el de C++. Es necesario eliminar el guión bajo para encontrar la implementación del método lookUpImpOrForward.

En este punto, la parte del ensamblaje del diablillo de búsqueda rápida ha terminado y luego viene el proceso de búsqueda lenta: enlace c/c++.

Mensaje de resumen enviado imp de búsqueda rápida (ensamblado):

objc_msgSend(receptor, selección,…)

  1. Compruebe si el receptor del mensaje existe, si es nulo no se realizará ningún procesamiento.
  2. Encuentre el objeto de clase correspondiente a través del puntero isa del receptor
  3. Encuentre el objeto de clase para la traducción de memoria y encuentre el caché
  4. Obtener depósitos del caché
  5. Compare el parámetro sel de los depósitos para ver si hay un método con el mismo nombre en la memoria caché.
  6. Si hay un sel correspondiente en los depósitos --> cacheHit --> llamar imp
  7. Si no hay un sel correspondiente en los depósitos --> _objc_msgSend_uncached -> _lookUpImpOrForward (búsqueda lenta en c/c++)

búsqueda lenta

búfer de método

Apple cree que si se llama a un método, ese método tiene una mayor probabilidad de ser llamado nuevamente. En este caso, mantiene directamente una lista de caché, carga el método llamado en la lista de caché y, cuando vuelve a llamar al método, lo almacena en caché primero. Busque en la lista y, si no puede encontrarlo, vaya a la lista de métodos para realizar la consulta. Esto evita tener que ir a la lista de métodos para consultar cada vez que se llama a un método, lo que mejora enormemente la velocidad.

Proceso de búsqueda

Primero veamos la implementación de la función lookUpImpOrForward:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    
    
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
    
    
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    // 检查当前类是个已知类
    checkIsKnownClass(cls);
    // 确定当前类的继承关系
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); 
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel); //cache中找IMP
            if (imp) goto done_unlock; //找到就直接返回了
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
     //如果不是常量优化缓存
            // 当前类的方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
    
    
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
    
    
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

    // 没有实现,尝试一次方法解析器。
	// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    
    
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    
    
        return nil;
    }
    return imp;
}

El método consiste en definir primero un reenvío de mensajes forward_imp; luego determinar la inicialización de la clase, bloquearla, comprobar si es una clase conocida... etc., ignorarlos por ahora. La atención se centra en el siguiente bucle for:

// unreasonableClassCount()表示循环的上限;
    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
    
            // curClass方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // 没有找到实现,方法解析器没有帮助。
                // 使用转发。
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
    
    
            _objc_fatal("Memory corruption in class list.");
        }

        // 超类缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
    
    
            // 在超类中找到forward::条目。
            // 停止搜索,但不要缓存;调用方法
            // 首先为这个类解析器。
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

Entrando en una lógica circular:

  • Busque imp en la lista de métodos de esta clase (el método de búsqueda es getMethodNoSuper_nolock, que se analizará más adelante);
  • Encuentre imp en el caché de la clase principal de esta clase (escrito por el ensamblado cache_getImp)
  • Encuentre imp en la lista de métodos de la clase principal de esta clase
  • …recorrido de la cadena de herencia…(clase principal->…->clase principal raíz)
  • Si se encuentra imp en cualquiera de los enlaces anteriores, se saltará el bucle y el método de caché se almacenará en el caché de esta clase (log_and_fill_cache); hasta que se encuentre
    nil, se designará a imp como reenvío de mensajes y el Se saltará el bucle.

Método de búsqueda

Eche un vistazo a cómo encontrar imp en la cadena de herencia de clases y clases principales (getMethodNoSuper_nolock):

/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    
    
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

	// 找到方法列表
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
    
    
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

Saltar search_method_list_inline()

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    
    
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    // 已排序的二分查找
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    
    
        return findMethodInSortedMethodList(sel, mlist);
    } else {
    
    
        // Linear search of unsorted method list
      	// 未排序的线性查找
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
    
    
        for (auto& meth : *mlist) {
    
    
            if (meth.name() == sel) {
    
    
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

fastpath() representa la ruta aproximada que se tomará. Las siguientes son búsquedas en dos situaciones.

  1. findMethodInSortedMethodList: de Sorted, se puede ver que la búsqueda en la lista de métodos ordenados utiliza la búsqueda binaria.
  2. findMethodInUnsortedMethodList: Se puede ver en Sin clasificar que se utiliza una búsqueda lineal en la lista de métodos sin clasificar, y método_t se elimina atravesando el sel de comparación uno por uno a través de un bucle for :.

Eche un vistazo a la función findMethodInSortedMethodList, salte a findMethodInSortedMethodList, ALWAYS_INLINE significa que siempre está en línea.

// 方法内联
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    
    
    if (list->isSmallList()) {
    
    
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
    
    
            return findMethodInSortedMethodList(key, list, [](method_t &m) {
    
     return m.getSmallNameAsSEL(); });
        } else {
    
    
            return findMethodInSortedMethodList(key, list, [](method_t &m) {
    
     return m.getSmallNameAsSELRef(); });
        }
    } else {
    
    
        return findMethodInSortedMethodList(key, list, [](method_t &m) {
    
     return m.big().name; });
    }
}

Después de la compilación, se sigue el siguiente proceso, que es la búsqueda de métodos mediante búsqueda binaria.

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    
    
    ASSERT(list);
		// 二分查找
  	// auto 代表自动匹配类型;
    auto first = list->begin();
    auto base = first;
  	// decltype: declare type,译为声明类型。这里获取表达式类型;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
    
    
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
    
    
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
    
    
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
    
    
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

La función obtiene el nombre del método de la sonda y lo compara con la clave pasada. Si son iguales, significa que se encontró un método coincidente. Luego, la función retrocede para encontrar la primera aparición del nombre del método en la lista de métodos para manejar la anulación del método por parte de la categoría.

Después de saltar del circuito

done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    
    
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    
    
        return nil;
    }
    return imp;

Si se encuentra un imp, se almacenará en caché en este tipo de caché (log_and_fill_cache). (Tenga en cuenta que ya sea que esta clase o la clase principal de esta clase encuentre imp, se almacenará en caché en esta clase)

Saltar a log_and_fill_cache:


/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled. 
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
    
    
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
    
    
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

Este código se utiliza para registrar registros de mensajes cuando se llaman a métodos y almacenar en caché los métodos llamados con frecuencia para acelerar las llamadas posteriores al método.

Mensaje de resumen enviado imp de búsqueda lenta (c/c++): IMP lookUpImpOrForward (id inst, SEL sel, Class cls, int comportamiento)

  1. Encuentre imp en la lista de métodos (búsqueda binaria/búsqueda transversal) de esta clase;
  2. Busque imp (ensamblaje) del caché de la clase principal de esta clase;
  3. Busque imp en la lista de métodos (búsqueda binaria/búsqueda transversal) de la clase principal de esta clase;
  4. ...Recorrido de la cadena de herencia...(clase principal->...->clase principal raíz) busca caché y lista de métodos imp;
  5. Si se encuentra imp en cualquiera de los enlaces anteriores, salga del bucle, guarde el método en el caché de esta clase y devuelva imp;
  6. Hasta que se encuentre nil, especifique imp como reenvío de mensajes, salga del bucle y realice la resolución del método dinámico resolveMethod_locked.

reenvío de mensajes

resolución dinámica

Como se explicó anteriormente, la esencia de la llamada a un método es el envío de mensajes. Si no se encuentra ningún método después de la búsqueda, ¿qué hará el sistema? Este es el método de resolución dinámica y reenvío de mensajes que se presenta a continuación.

proceso de decisión dinámico

Cuando no se puede encontrar esta clase ni cachela suma bajo la cadena de herencia de esta clase , y se asigna pero no se llama, ingresará al proceso de resolución del método dinámico y solo se ejecutará una vez.method listimpimp_objc_msgForward_impcache

resolveMethod_lockedDeclaración del código fuente:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    
    
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //判断是不是元类
    if (! cls->isMetaClass()) {
    
    
        // 不是元类,则是实例方法的动态方法解析
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
    
    
        // 是元类,则是类方法的动态方法解析
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls); // inst:类对象   cls: 元类
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    
    
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

Este código es la función utilizada para resolver el método resolveMethod_locked. Realiza una resolución dinámica de métodos basada en el tipo de clase (metaclase o no metaclase) e intenta encontrar la implementación del método en la memoria caché.

La lógica principal de la función es la siguiente:

  1. Primero, la función afirma que el bloqueo de tiempo de ejecución runtimeLockya está retenido para evitar conflictos durante la llamada.
  2. A continuación, la función comprueba si se ha creado una instancia de la clase (realized). Si no se ha creado una instancia de la clase, no tendrá ningún método para analizar y devolver directamente.
  3. Luego, la función libera el bloqueo del tiempo de ejecución, permitiendo que otros subprocesos accedan al tiempo de ejecución.
  4. Según el tipo de clase se realiza análisis dinámico de diferentes tipos de métodos:
    • Si la clase no es una metaclase, significa resolver métodos de instancia. Llame [cls resolveInstanceMethod:sel]al método de resolución de prueba.
    • Si la clase es una metaclase, significa analizar el método de la clase. Primero se llama al método try-resolve [nonMetaClass resolveClassMethod:sel]y, si no se encuentra, continúa llamando [cls resolveInstanceMethod:sel]al método de instancia try-resolve.
  5. Después de analizar el método, la función intenta encontrar la implementación del método en la memoria caché. Si la implementación del método se puede encontrar en la memoria caché, la implementación se devuelve directamente. De lo contrario, la función se procesará aún más de acuerdo con el comportamiento dado behavior, posiblemente reenviando el método o buscando la implementación del método de la clase principal.

Dos métodos: resolveInstanceMethody resolveClassMethod. También llamada resolución dinámica de un método.

Después de ejecutar el código anterior, regrese lookUpImpOrForwardTryCache:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

Este método llama _lookUpImpTryCacheal método:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    
    
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
    
    
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
    
    
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
    
    
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
    
    
        return nil;
    }
    return imp;
}

Ingresando _lookUpImpTryCacheel código fuente, lo puedes ver aquí cache_getImp, es decir, luego de una resolución dinámica, se buscará el sel del método desde el caché a través de cache_getImp.

¿ Si aún no lo encuentras (imp == NULL)? Es decir, si el método no se puede agregar dinámicamente, se ejecutará una vez lookUpImpOrForward. Cuando se ingresa el método , el valor pasado lookUpImpOrForwardaquí cambiará.behavior

Después de ingresar lookUpImpOrForwardel método por segunda vez, cuando se ejecuta este juicio if (slowpath(behavior & LOOKUP_RESOLVER)):

// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

Según el behaviorvalor modificado y LOOKUP_RESOLVERla relación entre los valores, la declaración if solo se puede ingresar por primera vez, por lo que este juicio es equivalente a un singleton. Explica por qué el análisis dinámico mencionado al principio resolveMethod_lockedsólo se ejecuta una vez.

Prueba de análisis dinámico

resolveClassMethod: el valor de retorno predeterminado es NO. Si desea agregar una implementación de método a esta función, debe usar class_addMethod

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串

Para implementar una clase, la clase declara un método en el archivo .h, pero este método no se implementa en el archivo .m. Llamar a este método externamente hará que el programa se bloquee.

razón:

  1. En el primer paso del método de búsqueda, la implementación de este método no se encontró ni en el objeto de clase propia ni en el objeto de clase de la clase principal.
  2. Entonces, al pasar al análisis de métodos dinámicos, no hicimos nada.
  3. Así que procedemos al tercer paso, pasando al reenvío de mensajes. No hicimos nada en el reenvío de mensajes y finalmente fallamos.

En este punto lo solucionamos en el paso de análisis del método dinámico:

  • Cuando se llama a un método de objeto, la resolución del método dinámico se resolveInstanceMethodimplementa en el método.
  • Cuando se llama a un método de clase, la resolución del método dinámico se resolveClassMethodimplementa en

Utilizando el análisis y la suma de métodos dinámicos runtime, podemos agregar la implementación de un método a un método no implementado.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface person : NSObject
- (void)test;
@end

NS_ASSUME_NONNULL_END

#import "person.h"
#import <objc/message.h>
#import <objc/runtime.h>

@implementation person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));
    return  [super resolveInstanceMethod:sel];
}
@end

Ejecute de la siguiente manera:
Insertar descripción de la imagen aquí

¿Puedes ver por qué hay 2 ejecuciones? Dejémoslo para el final. Lo mismo ocurre con los métodos de clase.

Dado que falló porque no se pudo encontrar , podemos pasarlo impcon este método y generarlo dinámicamente . El cuarto parámetro es el tipo de valor de retorno, descrito por una cadena:runtimeclass_addMethodselimpvoid“v@:”

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    
    
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

Modificación del método:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    
    NSLog(@"%s, self = %@", __func__, NSStringFromSelector(sel));
    if (sel == @selector(test)) {
    
    
        IMP imp = class_getMethodImplementation(self.class, @selector(addMethod));
        class_addMethod(self.class, sel, imp, "v@:");
    }
    return  [super resolveInstanceMethod:sel];
}

- (void)addMethod {
    
    
    NSLog(@"%s", __func__);
}

Puedes ver que se está ejecutando normalmente:
Insertar descripción de la imagen aquí

reenvío de mensajes

Si el sistema no encuentra una implementación durante la fase de resolución dinámica, ingresará a la fase de reenvío de mensajes.

Reenvío rápido de mensajes

Cuando cacheno se encuentra imp, no se encuentra la lista de métodos en la cadena de herencia de la clase impy resolveInstanceMethod / resolveClassMethodse devolverá NO para ingresar el reenvío de mensajes.

Cuando entramos lookUpImpOrForward, vimos impque estaba designado _objc_msgForward_impcache.

	//如果上述在类对象和父类对象中没有查到方法
	//我们就进入动态方法解析
 if (resolver  &&  !triedResolver) {
    
    //triedResolver用来判断是否曾经进行过动态方法解析,如果没有那就进入动态方法解析,如果进行过,就跳过
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst); //动态方法解析函数
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES; //进行过动态方法解析就把这个标识为设置为YES
       goto retry;//retry是前面的发送消息的过程
    }
    	
      //如果动态方法解析失败,就进入消息转发

    imp = (IMP)_objc_msgForward_impcache; //由这一步进入消息转发
    cache_fill(cls, sel, imp, inst);
//如果消息转发失败,程序崩溃
 done:
    runtimeLock.unlock();

Entonces, si esta clase no tiene la capacidad de procesar este mensaje, se reenviará a otras clases y dejará que otras clases lo manejen.

Echemos un vistazo a la implementación específica de la función de reenvío de mensajes __objc_msgForward_impcache. Es el proceso de reenvío de mensajes; es hora de volver a la etapa de ensamblaje del código fuente:

 STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band condition register is NE for stret, EQ otherwise.

	jne	__objc_msgForward_stret
	jmp	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache
	
	
	ENTRY __objc_msgForward
	// Non-stret version

	movq	__objc_forward_handler(%rip), %r11
	jmp	*%r11

	END_ENTRY __objc_msgForward

Pero __objc_forward_handler no es de código abierto.

Prueba de reenvío rápido de mensajes

  1. El método func1 está definido en la clase Persona pero no está implementado. Utilice el método -(id)forwardingTargetForSelector:(SEL)aSelector para un reenvío rápido de mensajes.
  2. Defina el método func1 en la clase Blank e impleméntelo
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)func1;
@end

NS_ASSUME_NONNULL_END

#import "Person.h"
#import "Blank.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Person

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    
    NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));
    
    if (aSelector == @selector(func1)) {
    
    
        return [Blank alloc];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
@end

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Blank : NSObject
- (void)func1;
@end

NS_ASSUME_NONNULL_END

#import "Blank.h"

@implementation Blank
- (void)func1 {
    
    
    NSLog(@"%s", __func__);
}
@end

archivo main.m, cree un nuevo objeto persona y llame al método func1

#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        Person* person = [[Person alloc] init];
        [person func1];
    }
    return 0;
}

Ejecute de la siguiente manera:
Insertar descripción de la imagen aquí

La función del reenvío es que si el objeto actual no puede responder al mensaje, se reenvía a un objeto que pueda responder.

¿Dónde está el caché del método en este momento? Objeto que recibe mensajes reenviados

Escenario de aplicación: cree una clase dedicada para manejar estos mensajes que no responden. La recopilación se bloquea cuando no se puede encontrar el método.

La demostración es un método de instancia, si es un método de clase, simplemente cambie - a +;

Reenvío lento de mensajes

Si no se encuentra ningún método para el reenvío rápido de mensajes, existe otro methodSignatureForSelectormétodo más adelante, que se utiliza para la firma de validez del método. También debe combinarse con otro método:forwardInvocation

forwardInvocationEl método proporciona un parámetro de entrada de tipo NSInvocation; proporciona targety selectorse utiliza para encontrar la implementación del método en el objetivo especificado.

Después de comentar sobre el método de avance rápido que acabamos de usar forwardingTargetForSelector, agregue methodSignatureForSelectorel método y forwardInvocationel método.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    
    NSLog(@"%s, aSelector = %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    
    
}

Operando normalmente:
Insertar descripción de la imagen aquí

Resumir

La esencia de la llamada al método OC es el envío de mensajes, y el envío de mensajes es el proceso de búsqueda de SEL-IMP.

resolución dinámica

  • No hay forma de encontrarlo a través del mecanismo de envío de mensajes, el sistema también realizará una resolución dinámica antes de ingresar al reenvío de mensajes.

Resolución dinámica de métodos de instancia.

+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls) 

Resolución dinámica de métodos de clase.

+ (BOOL)resolveClassMethod:(SEL)sel;

reenvío de mensajes

  • La resolución dinámica no puede encontrar una manera de ingresar realmente a la etapa de reenvío de mensajes.
  • La resolución dinámica, el avance rápido y el avance lento se denominan colectivamente los tres popotes que salvan vidas y se utilizan para evitar fallas del sistema causadas por la búsqueda de métodos.

Reenvío rápido de mensajes

- (id)forwardingTargetForSelector:(SEL)aSelector;

Mensaje reenviado lentamente

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;

El mecanismo de reenvío de mensajes se divide básicamente en tres pasos, también conocidos como los tres rescates de mensajes:

  1. Análisis de métodos dinámicos
  2. receptor de respaldo
  3. Reenvío completo de mensajes

Podemos solucionar este problema controlando uno de estos tres pasos.

Nota especial: si es un mensaje normal, no seguirá estos tres pasos. Por lo tanto, el requisito previo para alcanzar estos tres pasos es determinar que el mensaje es desconocido.

diagrama de flujo

Insertar descripción de la imagen aquí

algunos problemas

¿Cómo encuentra el tiempo de ejecución la dirección IMP correspondiente a través del selector?

Búsqueda de caché–>Búsqueda de clase actual–>Búsqueda de clase principal nivel por nivel

Si una subclase llama a un método de clase principal, ¿en qué clase se almacena en caché?

Cuando una clase secundaria llama a un método de la clase principal, el tiempo de ejecución de Objective-C primero verificará el caché del método de la clase secundaria. Si el método de la clase principal ya existe en el caché del método de la clase secundaria, la implementación del método se obtiene directamente del caché y se llama. Esto mejora la velocidad de búsqueda de métodos y evita la necesidad de realizar una búsqueda de métodos completa en cada llamada.

Si el método de la clase principal no existe en el caché de métodos de la subclase, la implementación del método continuará buscándose hacia arriba. Primero se busca el caché de la clase principal, luego la clase principal de la clase principal, y así sucesivamente hasta que se encuentra la implementación del método o se alcanza la parte superior de la cadena de herencia.

Cabe señalar que si la subclase anula el método de la clase principal y llama a la palabra clave super en la subclase para llamar al método de la clase principal, entonces esta llamada al método no utilizará el caché, sino que se determinará mediante un método completo. búsqueda Implementación del método.

En resumen, cuando una subclase llama a un método de una clase principal, el caché se almacena en el caché del método de la subclase, pero si la subclase anula el método de la clase principal y usa super para llamar al método de la clase principal, el caché no se utilizará.

Razones para dos resoluciones dinámicas

Prueba con puntos de interrupción:
Insertar descripción de la imagen aquí

Después de ejecutar, puede ver la información impresa ingresando el comando bt en lldb.
Cuando ingresa el punto de interrupción por primera vez e ingresa bt, la pantalla es la siguiente:
Insertar descripción de la imagen aquí

La segunda vez que ingresa el punto de interrupción e ingresa bt, la pantalla es la siguiente:
Insertar descripción de la imagen aquí

El símbolo se llama ___forwarding___y methodSignatureForSelectorse utiliza el método familiar de reenvío lento. Se puede ver que la segunda vez es el reenvío de mensajes;

Después de que la primera resolución dinámica y el reenvío rápido del mensaje no lograron encontrar una solución, entró en reenvío lento. Durante el proceso, runtimese llamará una vez lookUpImpOrForward. Este método contiene resolución dinámica, lo que da como resultado una segunda resolución dinámica.

Supongo que te gusta

Origin blog.csdn.net/m0_63852285/article/details/131024005
Recomendado
Clasificación