[iOS] Mecanismo de mensagens e encaminhamento de mensagens

mecanismo de passagem de mensagens

Na linguagem OC, chamar o método de um objeto é chamado de passagem de mensagem. Uma mensagem possui um nome e um seletor, pode aceitar parâmetros e pode ter um valor de retorno.

No Objective-C, se uma mensagem for passada para um objeto, o mecanismo de ligação dinâmica é usado para determinar o método que precisa ser chamado. No nível inferior, todos os métodos são funções comuns da linguagem C. Porém, depois que o objeto recebe a mensagem, qual método deve ser chamado é completamente determinado durante o tempo de execução, e pode até mesmo ser alterado enquanto o programa está em execução. Esses recursos tornam o Objective- C uma linguagem de programação real, linguagem dinâmica.

Exemplo: expressão de mensagem OC:

id returnValue = [someObject messageName:parameter];

Esta seção será processada pelo compilador em:

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

someObject é chamado de receptor, messageName é chamado de "seletor", e o seletor e os parâmetros juntos são chamados de "mensagem".Quando o compilador vir esta mensagem, ela será convertida em uma chamada de função de linguagem C padrão.

SELECIONE SEL

Durante a compilação, o OC irá gerar um ID exclusivo para distinguir este método com base no nome do método (incluindo a sequência de parâmetros).Este ID é do tipo SEL. O que precisamos observar é que, desde que os nomes dos métodos (incluindo sequências de parâmetros) sejam os mesmos, seus IDs serão os mesmos. Portanto, seja uma classe pai ou uma subclasse, se o nome for o mesmo, o ID será o mesmo.

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

Este mecanismo aumenta muito a flexibilidade do nosso programa. Podemos passar parâmetros SEL para um método e deixar o método executar dinamicamente um determinado método; também podemos especificar o método que precisa ser executado através do arquivo de configuração, e o programa lê a configuração O arquivo então converte a string do método em uma variável SEL e então envia a mensagem para o objeto correspondente.

Do ponto de vista da eficiência, ao executar, o método é pesquisado não pelo nome do método, mas pelo ID do método, que é um número inteiro. Como a pesquisa e correspondência de números inteiros é muito mais rápida que a de strings, isso pode melhorar a execução para até certo ponto, a eficiência

Precisamos observar que @selector equivale a traduzir o nome do método no nome do método SEL.Ele se preocupa apenas com o nome do método e o número de parâmetros, e não se preocupa com o valor de retorno e o tipo de parâmetro.

O processo de geração do SEL é fixo, pois é apenas um ID que indica o método.Não importa em qual classe o método dayin esteja escrito, o valor do SEL é fixo.

Uma tabela SEL é mantida em tempo de execução. Esta tabela armazena SELs não de acordo com classes. Contanto que os mesmos SELs sejam considerados como um só, eles serão armazenados na tabela. Quando o projeto for carregado, todos os métodos serão carregados nesta tabela, e os métodos gerados dinamicamente também serão carregados na tabela.

Então classes diferentes podem ter o mesmo método.Quando objetos de instância de classes diferentes executam o mesmo seletor, eles procurarão o IMP correspondente à sua própria classe com base no SEL em suas respectivas listas de métodos.

IMP é essencialmente um ponteiro de função. A função apontada contém um ID de objeto que recebe a mensagem, o SEL para chamar o método e alguns parâmetros do método, e retorna um ID. Portanto, podemos obter seu IMP correspondente através do SEL. Após obter o ponteiro de função, significa que obtivemos a entrada de código necessária para executar o método, para que possamos usar este ponteiro de função como uma chamada de função normal da linguagem C.

objc_msgEnviar

Podemos ver que a função objc_msgSend é utilizada na conversão, esta função leva o receptor da mensagem e o nome do método como parâmetros principais, conforme mostrado abaixo:

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

objc_msgSend implementa o mecanismo de ligação dinâmica por meio das seguintes etapas:

  • Primeiro, obtenha a implementação do método apontada pelo seletor. Como o mesmo método pode ter implementações diferentes em classes diferentes, o julgamento é baseado na classe à qual o receptor pertence.
  • Em segundo lugar, passe o objeto receptor e os parâmetros especificados pelo método para chamar a implementação do método.
  • Finalmente, o valor de retorno da implementação do método é retornado.

A chave para a passagem de mensagens está na estrutura objc_class, que possui três campos principais:

  • isa: Ponteiro para classe.
  • superclasse: ponteiro para a classe pai.
  • methodLists: A tabela de distribuição de métodos da classe (tabela de despacho).

Quando um novo objeto é criado, a memória é alocada para ele e suas variáveis-membro são inicializadas. O ponteiro isa também será inicializado, permitindo que o objeto acesse a classe e a cadeia de herança da classe.

A figura a seguir mostra um diagrama esquemático do processo de passagem de mensagens:

Insira a descrição da imagem aqui

  • Quando uma mensagem é entregue a um objeto, ela é primeiro consultada no cache do sistema de tempo de execução objc_cache. Se encontrado, execute. Caso contrário, continue com as etapas a seguir.
  • objc_msgSend obtém a estrutura da classe por meio do ponteiro isa do objeto e, em seguida, pesquisa o seletor de método na tabela de distribuição de métodos methodLists. Se não for encontrado, sua classe pai será encontrada ao longo da superclasse da classe e a pesquisa continuará na tabela de distribuição MethodLists da classe pai.
  • Por analogia, a cadeia de herança da classe remonta à classe NSObject. Assim que o seletor for encontrado, passe os parâmetros correspondentes para executar a implementação específica do método e adicione o método ao cache objc_cache. Caso o seletor ainda não seja encontrado no final, o processo de encaminhamento de mensagens será iniciado.

Análise de código fonte

pesquisa rápida

objc_msgSend é implementado em diferentes arquiteturas: tomando arm64 como exemplo, a implementação do código é assembly. Por que escolher a montagem para implementar? É mais rápido, usa parâmetros diretamente e evita a sobrecarga de copiar um grande número de parâmetros. Um sublinhado "_" será adicionado na frente das funções e variáveis ​​globais para evitar conflitos de símbolos.

Processo de montagem:

	//进入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
  • Primeiro, comece com cmp p0,#0, onde p0 é um registro que armazena o destinatário da mensagem. Ao inserir a entrada de envio de mensagem, primeiro determine se o receptor da mensagem existe. Caso contrário, execute novamente objc_msgSend.
  • b.le LNilOrTagged, b significa pular para. le significa que se p0 for menor ou igual a 0, o significado geral é que se p0 for menor ou igual a 0, pule para LNilOrTagged, execute b.eq LReturnZero para sair desta função diretamente
  • Se o receptor da mensagem não for nulo, o assembly continua em execução, vai para CacheLookup NORMAL e procura por imp no cache.

Vamos dar uma olhada na implementação 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. Processo:

  1. Obtenha ponteiros para cache e _bucketsAndMaybeMask;
  2. Pegue os buckets e a máscara respectivamente de _bucketsAndMaybeMask e use a máscara para calcular o subscrito de hash de acordo com o algoritmo de hash;
  3. De acordo com o início do subscrito de hash obtido e o primeiro endereço do bucket, extraia o bucket correspondente ao subscrito de hash;
  4. Entre no loop do-while e pesquise com base em sel no bucket;

O cache e _bucketsAndMaybeMask são obtidos por meio de tradução de memória. Os 16 bits altos de _bucketsAndMaybeMask armazenam a máscara, e os 48 bits baixos armazenam buckets (16 bits altos | 48 bits baixos = máscara | baldes), ou seja, _bucketsAndMaybeMask = máscara (16 altos ) + ponteiro de buckets (baixo 48 bits).

Usando o parâmetro p1 (ou seja, o segundo parâmetro _sel) & máscara de objc_msgSend, por meio do algoritmo hash, obtemos o início do subscrito do bucket que precisa ser encontrado para armazenar sel-imp, ou seja, p12=begin=_sel&mascara , porque ao armazenar sel-imp When , o subscrito de hash também é calculado e armazenado por meio do mesmo algoritmo de hash.

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

De acordo com o início do subscrito de hash calculado multiplicado pelo tamanho da memória ocupada por um único balde, é obtido o deslocamento na memória real do primeiro endereço do balde do balde apontado pelo subscrito de início. Obtenha o intervalo correspondente ao subscrito de hash começando pelo primeiro endereço + deslocamento real. O bucket é composto por dois atributos: sel e imp. Cada atributo tem 8 bytes de tamanho, então o tamanho do bucket é 16

No loop do-while:

  • No primeiro loop do-while, pesquise do início -> 0. Se não houver acerto, p9 não será nulo e o segundo loop do-while será iniciado;
  • No segundo loop do-while, pesquise novamente em máscara —> 0;
  • Se este ainda for o caso, execute __objc_msgSend_uncached —> MethodTableLookup —> _lookUpImpOrForward para começar a procurar a lista de métodos.

Se você encontrar um método no cache, chame-o diretamente. Se você encontrar sel, você digitará CacheHit e retornará ou chamará imp: Retornará ou chamará a implementação do método (imp).

2. Conteúdo do CacheHit: O modo na imagem acima representa o seguinte processo NORMAL, autenticar e chamar imp significa verificação e implementação do método de chamada.

#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 não encontrado no cache

Se o cache não for encontrado, procure o próximo bucket e faça um loop até que o método correspondente seja encontrado. Se o método não for encontrado após o loop, chame __objc_msgSend_uncached.

A seguir está o código de salto de julgamento acima:

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

__objc_msgSend_uncached montagem do código-fonte:

	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

A macro MethodTableLookup é chamada: para localizar um método na lista de métodos.

Dê uma olhada em sua estrutura:

	.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 eles, bl significa que o método _lookUpImpOrForward é chamado. _lookUpImpOrForward não pode ser encontrado no assembly, porque a função assembly tem um sublinhado a mais que o C++. Você precisa remover o sublinhado para encontrar a implementação do método lookUpImpOrForward.

Neste ponto, a parte de montagem do imp de pesquisa rápida termina e, em seguida, vem o processo de pesquisa lento: link c/c++.

Mensagem de resumo enviada imp de pesquisa rápida (assembly):

objc_msgSend(receptor, sel,…)

  1. Verifique se o receptor da mensagem existe. Se for nulo, nenhum processamento será feito.
  2. Encontre o objeto de classe correspondente através do ponteiro isa do receptor
  3. Encontre o objeto de classe para tradução de memória e encontre o cache
  4. Obtenha buckets do cache
  5. Compare o parâmetro sel dos buckets para ver se existe um método com o mesmo nome no cache.
  6. Se houver um sel correspondente nos buckets -> cacheHit -> call imp
  7. Se não houver sel correspondente nos buckets --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++ pesquisa lenta)

pesquisa lenta

buffer de método

A Apple acredita que se um método for chamado, esse método terá uma chance maior de ser chamado novamente. Nesse caso, ele mantém diretamente uma lista de cache, carrega o método chamado na lista de cache e, ao chamar o método novamente, armazena-o em cache primeiro . Pesquise na lista e, se não conseguir encontrar, vá até a lista de métodos para consultar. Isso evita ter que ir até a lista de métodos para consultar toda vez que um método é chamado, o que melhora muito a velocidade.

Processo de pesquisa

Vejamos primeiro a implementação da função 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;
}

O método é primeiro definir um encaminhamento de mensagem forward_imp; depois determinar a inicialização da classe, bloqueá-la, verificar se é uma classe conhecida... etc., ignore-os por enquanto. O foco está no seguinte loop 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 em uma lógica circular:

  • Encontre imp na lista de métodos desta classe (o método de busca é getMethodNoSuper_nolock, que será analisado posteriormente);
  • Encontre imp no cache da classe pai desta classe (escrito pelo assembly cache_getImp)
  • Encontre imp na lista de métodos da classe pai desta classe
  • …travessia da cadeia de herança…(classe pai->…->classe pai raiz)
  • Se imp for encontrado em qualquer um dos links acima, o loop será saltado e o método cache será armazenado em cache no cache desta classe (log_and_fill_cache); até que nil seja encontrado, imp será designado
    como encaminhamento de mensagem, e o o loop será saltado.

Método de pesquisa

Dê uma olhada em como encontrar imp na cadeia de herança da classe e da classe pai (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 o caminho aproximado que será percorrido. A seguir estão pesquisas em duas situações.

  1. findMethodInSortedMethodList: Em Sorted, pode-se ver que a pesquisa na lista de métodos classificados usa pesquisa binária.
  2. findMethodInUnsortedMethodList: Pode ser visto em Unsorted que uma pesquisa linear é usada na lista de métodos não classificados, e method_t é retirado percorrendo a comparação sel um por um através de um loop for:.

Dê uma olhada na função findMethodInSortedMethodList, vá para findMethodInSortedMethodList, ALWAYS_INLINE significa que isso está sempre embutido.

// 方法内联
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; });
    }
}

Após a compilação, segue-se o seguinte processo, que é a busca de métodos por meio de busca binária.

/***********************************************************************
 * 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;
}

A função obtém o nome do método do teste e o compara com a chave passada. Se forem iguais, significa que um método correspondente foi encontrado. A função então retrocede para encontrar a primeira ocorrência do nome do método na lista de métodos para lidar com a substituição do método pela categoria.

Depois de sair do loop

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;

Se imp for encontrado, imp será armazenado em cache neste tipo de cache (log_and_fill_cache). (Observe que se esta classe ou a classe pai desta classe encontrar imp, ela será armazenada em cache nesta classe)

Vá para 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 é usado para registrar logs de mensagens quando métodos são chamados e armazenar em cache métodos chamados com frequência para acelerar chamadas subsequentes ao método.

Mensagem de resumo enviada imp de pesquisa lenta (c/c++): IMP lookUpImpOrForward (id inst, SEL sel, Class cls, comportamento int)

  1. Encontre imp na lista de métodos (pesquisa binária/pesquisa transversal) desta classe;
  2. Encontre imp (assembly) no cache da classe pai desta classe;
  3. Encontre imp na lista de métodos (pesquisa binária/pesquisa transversal) da classe pai desta classe;
  4. ...Travessia da cadeia de herança...(classe pai->...->classe pai raiz) procura por cache e lista de métodos imp;
  5. Se imp for encontrado em qualquer um dos links acima, saia do loop, armazene o método em cache no cache desta classe e retorne imp;
  6. Até que nil seja encontrado, especifique imp como encaminhamento de mensagem, saia do loop e execute a resolução do método dinâmico resolveMethod_locked.

encaminhamento de mensagens

resolução dinâmica

Conforme apresentado acima, a essência da chamada de método é o envio de mensagens. Se nenhum método for encontrado após a pesquisa, o que o sistema fará? Este é o método de resolução dinâmica e encaminhamento de mensagens apresentado a seguir.

processo de decisão dinâmico

cacheQuando nem esta classe nem a soma na cadeia de herança desta classe method listpuderem ser encontradas imp, e ela impfor atribuída _objc_msgForward_impcache, mas não for chamada, ela entrará no processo de resolução de método dinâmico e será executada apenas uma vez.

resolveMethod_lockedDeclaração do código-fonte:

/***********************************************************************
* 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 é a função usada para resolver o método resolveMethod_locked. Ele executa a resolução dinâmica do método com base no tipo da classe (metaclasse ou não metaclasse) e tenta encontrar a implementação do método no cache.

A lógica principal da função é a seguinte:

  1. Primeiro, a função afirma que o bloqueio de tempo de execução runtimeLockjá foi mantido para evitar contenção durante a chamada.
  2. A seguir, a função verifica se a classe foi instanciada (realized). Se a classe não tiver sido instanciada, ela não terá nenhum método para analisar e retornar diretamente.
  3. A função então libera o bloqueio de tempo de execução, permitindo que outros threads acessem o tempo de execução.
  4. De acordo com o tipo de aula, é realizada análise dinâmica de diferentes tipos de métodos:
    • Se a classe não for uma metaclasse, significa resolver métodos de instância. Chame [cls resolveInstanceMethod:sel]o método de resolução try.
    • Se a classe for uma metaclasse, isso significa analisar o método da classe. O método try-resolve é chamado first [nonMetaClass resolveClassMethod:sel]e, se não for encontrado, continua chamando [cls resolveInstanceMethod:sel]o método de instância try-resolve.
  5. Depois de analisar o método, a função tenta encontrar a implementação do método no cache. Se a implementação do método puder ser encontrada no cache, a implementação será retornada diretamente. Caso contrário, a função será processada posteriormente de acordo com o comportamento fornecido behavior, possivelmente encaminhando o método ou encontrando a implementação do método da classe pai.

Dois métodos: resolveInstanceMethode resolveClassMethod. Também chamada de resolução dinâmica de um método.

Após executar o código acima, retorne lookUpImpOrForwardTryCache:

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

Este método chama _lookUpImpTryCacheo 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;
}

Inserindo _lookUpImpTryCacheo código fonte, você pode ver aqui cache_getImp; ou seja, após uma resolução dinâmica, o sel do método será pesquisado no cache através de cache_getImp.

Se ainda não foi encontrado (imp == NULL)? Ou seja, se o método não puder ser adicionado dinamicamente, ele será executado uma vez.Quando lookUpImpOrForwardo método for inserido , o valor passado lookUpImpOrForwardaqui será alterado.behavior

Depois de entrar lookUpImpOrForwardno método pela segunda vez, quando este julgamento for executado if (slowpath(behavior & LOOKUP_RESOLVER)):

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

De acordo com o behaviorvalor alterado e LOOKUP_RESOLVERa relação entre os valores, a instrução if só pode ser inserida pela primeira vez, portanto este julgamento equivale a um singleton. Explica porque a análise dinâmica mencionada no início resolveMethod_lockedsó é executada uma vez.

Teste de análise dinâmica

resolveClassMethod: O valor de retorno padrão é NÃO. Se você deseja adicionar uma implementação de método a esta função, você precisa 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 uma classe, a classe declara um método no arquivo .h, mas esse método não é implementado no arquivo .m. Chamar este método externamente fará com que o programa trave

razão:

  1. Na primeira etapa do método de busca, a implementação deste método não foi encontrada nem no próprio objeto de classe nem no objeto de classe da classe pai.
  2. Então, passando para a análise de método dinâmico, análise de método dinâmico, não fizemos nada,
  3. Então passamos para a terceira etapa, passando para o encaminhamento de mensagens. Não fizemos nada no encaminhamento de mensagens e finalmente travamos.

Neste ponto, resolvemos isso na etapa de análise do método dinâmico:

  • Quando um método de objeto é chamado, a resolução dinâmica do método é resolveInstanceMethodimplementada no método
  • Quando um método de classe é chamado, a resolução dinâmica do método é resolveClassMethodimplementada em

Usando análise e soma de métodos dinâmicos runtime, podemos adicionar implementação de método a um método não 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

Execute da seguinte forma:
Insira a descrição da imagem aqui

Você pode ver por que existem 2 execuções? Vamos deixar isso para o fim. O mesmo vale para métodos de classe.

Como ele travou porque não foi encontrado , podemos passá imp-lo neste método e gerá-lo dinamicamente . O quarto parâmetro é o tipo de valor de retorno, descrito por uma string: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);
}

Modificação do 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__);
}

Você pode ver que ele está funcionando normalmente:
Insira a descrição da imagem aqui

encaminhamento de mensagens

Caso o sistema não encontre uma implementação durante a fase de resolução dinâmica, ele entrará na fase de encaminhamento de mensagens.

Encaminhamento rápido de mensagens

Quando cachenão encontrado imp, a lista de métodos na cadeia de herança da classe não foi encontrada impe resolveInstanceMethod / resolveClassMethodNÃO será retornado para entrar no encaminhamento de mensagens.

Quando entramos , lookUpImpOrForwardvimos impque estava 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();

Portanto, se esta classe não tiver a capacidade de processar esta mensagem, ela será encaminhada para outras classes e deixará que outras classes cuidem dela.

Vamos dar uma olhada na implementação específica da função de encaminhamento de mensagens __objc_msgForward_impcache. É o processo de encaminhamento de mensagens; é hora de nosso estágio de montagem do código-fonte novamente:

 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

Mas __objc_forward_handler não é de código aberto.

Teste de encaminhamento rápido de mensagens

  1. O método func1 é definido na classe Person, mas não é implementado. Use o método -(id)forwardingTargetForSelector:(SEL)aSelector para encaminhamento rápido de mensagens.
  2. Defina o método func1 na classe Blank e implemente-o
#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

main.m, crie um novo objeto person e chame o 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;
}

Execute da seguinte forma:
Insira a descrição da imagem aqui

A função do encaminhamento é que se o objeto atual não puder responder à mensagem, ele será encaminhado para um objeto que possa responder.

Onde está o cache do método neste momento? Objeto que recebe mensagens encaminhadas

Cenário de aplicação: Crie uma classe dedicada para lidar com essas mensagens que não respondem. Coleta de falha quando o método não pode ser encontrado.

A demonstração é um método de instância.Se for um método de classe, basta alterar - para +;

Encaminhamento lento de mensagens

Se nenhum método for encontrado para encaminhamento rápido de mensagens, existe outro methodSignatureForSelectormétodo posteriormente, que é usado para assinatura de validade do método. Também precisa ser emparelhado com outro método:forwardInvocation

forwardInvocationO método fornece um parâmetro de entrada do tipo NSInvocation; ele fornece targete selectoré usado para encontrar a implementação do método no destino especificado.

Depois de comentar sobre o método de avanço rápido que acabamos de usar forwardingTargetForSelector, adicione methodSignatureForSelectoro método e forwardInvocationo método.

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

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

Operando normalmente:
Insira a descrição da imagem aqui

Resumir

A essência da chamada do método OC é o envio de mensagens, e o envio de mensagens é o processo de pesquisa do SEL-IMP.

resolução dinâmica

  • Não há como encontrá-lo através do mecanismo de envio de mensagens, o sistema também realizará a resolução dinâmica antes de entrar no encaminhamento de mensagens.

Resolução dinâmica de métodos de instância

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

Resolução dinâmica de métodos de classe

+ (BOOL)resolveClassMethod:(SEL)sel;

encaminhamento de mensagens

  • A resolução dinâmica não consegue encontrar uma maneira de realmente entrar no estágio de encaminhamento de mensagens.
  • Resolução dinâmica, avanço rápido e avanço lento são chamados coletivamente de três canudos que salvam vidas, usados ​​para evitar falhas no sistema causadas pela pesquisa de método.

Encaminhamento rápido de mensagens

- (id)forwardingTargetForSelector:(SEL)aSelector;

Mensagem encaminhada lentamente

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

O mecanismo de encaminhamento de mensagens é basicamente dividido em três etapas, também conhecidas como os três resgates de mensagens:

  1. Análise de método dinâmico
  2. receptor de backup
  3. Encaminhamento completo de mensagens

Podemos resolver este problema controlando uma destas três etapas

Nota especial: Se for uma mensagem normal, não passará por essas três etapas. Portanto, o pré-requisito para alcançar essas três etapas é determinar se a mensagem é desconhecida.

fluxograma

Insira a descrição da imagem aqui

alguns problemas

Como o tempo de execução encontra o endereço IMP correspondente por meio do seletor?

Pesquisa de cache–>Pesquisa de classe atual–>Pesquisa de classe pai nível por nível

Se uma subclasse chama um método de classe pai, em qual classe é armazenada em cache?

Quando uma classe filha chama um método da classe pai, o tempo de execução do Objective-C verificará primeiro o cache do método da classe filha. Se o método da classe pai já existir no cache de métodos da classe filha, a implementação do método será obtida diretamente do cache e chamada. Isso melhora a velocidade da pesquisa de métodos e evita a necessidade de uma pesquisa completa de métodos em cada chamada.

Se o método da classe pai não existir no cache de métodos da subclasse, a implementação do método continuará a ser pesquisada para cima. Primeiro, o cache da classe pai é consultado, depois a classe pai da classe pai e assim por diante, até que a implementação do método seja encontrada ou o topo da cadeia de herança seja alcançado.

Deve-se notar que se a subclasse substituir o método da classe pai e chamar a palavra-chave super na subclasse para chamar o método da classe pai, então esta chamada de método não usará o cache, mas será determinada por meio de um método completo pesquisa.implementação do método.

Resumindo, quando uma subclasse chama um método de uma classe pai, o cache é armazenado no cache de método da subclasse, mas se a subclasse substituir o método da classe pai e usar super para chamar o método da classe pai, o cache não será usado.

Razões para duas resoluções dinâmicas

Teste com pontos de interrupção:
Insira a descrição da imagem aqui

Após a execução, você pode ver as informações impressas digitando o comando bt em lldb.
Quando você insere o ponto de interrupção pela primeira vez e insere bt, a exibição é a seguinte:
Insira a descrição da imagem aqui

Na segunda vez que você insere o ponto de interrupção e bt, a exibição é a seguinte:
Insira a descrição da imagem aqui

O símbolo é chamado ___forwarding___e o conhecido methodSignatureForSelectormétodo de encaminhamento lento é usado.Pode-se observar que a segunda vez é o encaminhamento de mensagens;

Depois que a primeira resolução dinâmica e o encaminhamento rápido da mensagem não conseguiram encontrar uma solução, ela entrou no encaminhamento lento. Durante o processo, runtimeele será chamado uma vez.Este lookUpImpOrForwardmétodo contém resolução dinâmica, o que resulta em uma segunda resolução dinâmica.

Acho que você gosta

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