(Summary of articles on underlying principles of iOS)

(iOS)

The main purpose of this article is to understand the method lookup process of objc_msgSend

In the previous article iOS- Underlying Principles: Cache in Objc_class (11), we analyzed the cache write process. Before the write process, there is a cache read process, namely objc_msgSend and cache_getImp

Before analyzing, first know what is Runtime

The Runtime is introduced

Runtime is called runtime, which is different from compile time

  • The runtime 是The code runs and is loaded into memoryIf there is an error at this point, the program will crashdynamicphase
  • Compile time 是The source code is translated into machine-readable codeThe process, mainly for the language for the most basic check error, namely lexical analysis, grammar analysis, is astaticThe stage of

Runtime can be used in the following three ways, and the relationship between the three implementations and the compiler layer and the underlying layer is shown in the figure

  • throughOC code, e.g.[person sayNB]
  • throughNSObject methods, e.g.isKindOfClass
  • throughRuntime API, e.g.class_getInstanceSize

One of thecompilerThat’s what we knowThe compiler, i.e.,LLVMFor example OCallocCorresponding to the underlyingobjc_alloc.runtime system libararyisThe underlying library

Explore the nature of the method

Nature of method

In the ios-Underlying Principles 07: Isa and Class Association article, we understand the nature of the OC object by using the source code compiled by clang. Similarly, we use clang to compile the main. CPP file by looking at the implementation of method calls in the main function, as shown below

LGPerson *person = [LGPerson alloc]; [person sayNB]; [person sayHello]; LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello")); Copy the codeCopy the code

As you can see from the above code, the essence of the method is objc_msgSend message sending

To verify, make the call to [Person sayNB] with the objc_msgSend method and see if it prints consistently

Note: 1, call objc_msgSend directly, #import

#import

#import

#import

#import

#import

LGPerson *person = [LGPerson alloc]; objc_msgSend(person,sel_registerName("sayNB")); [person sayNB]; Copy the codeCopy the code

The print-out results are as follows, found to be consistent, so[person sayNB]Is equivalent toobjc_msgSend(person,sel_registerName("sayNB")) 

Object method calls – the actual execution is the superclass implementation In addition to validation, we can also try to have the person call execute the superclass implementation, implemented by objc_msgSendSuper

  • Define two classes: LGPerson and LGTeacher, which implement the sayHello method in the parent class

  • Calls in main
LGPerson *person = [LGPerson alloc]; LGTeacher *teacher = [LGTeacher alloc]; [person sayHello]; struct objc_super lgsuper; lgsuper.receiver = person; // The recipient of the message is still person lgsuper.super_class = [LGTeacher class]; Objc_msgSendSuper (& lgSuper, sel_registerName("sayHello")); Copy the codeCopy the code

The objc_msgSendSuper method has two parameters (struct, sel). The struct type is the struct object defined by objc_super, and the two properties (receiver and super_class) need to be specified

  • objc_msgSendSuperThe method parameters

  • objc_superThe source code to define

The print result is as follows

It turns out that both [Person sayHello] and objc_msgSendSuper execute an implementation of sayHello in the parent class, so here, we can make a guess: method calls, first look in the class, if not found in the class, will look in the parent class.

With that in mind, let’s explore the source implementation of objc_msgSend

Objc_msgSend Quick search process analysis

In objC4-781 source code, searchobjc_msgSendBecause our daily development is the architecture is ARM64, so need inarm64.sFile with the suffixobjc_msgSendSource code implementation, discovery isAssembly implementation, the flowchart for the overall implementation of its compilation is as follows

Objc_msgSend assembly source code

Objc_msgSend is the entry point for the source code for message sending, which is implemented in assembly. The _objc_msgSend source code is implemented as follows

ENTRY objc_msgSend //---- No window UNWIND _objc_msgSend, NoFrame //---- p0 compares with null, that is, determines whether the recipient exists, Where p0 is the first parameter of objc_msgSend - message receiver CMP p0, #0 // nil check and taggedpointer check //---- le < / if support_tagged_pointer LNilOrTagged // (MSB tagged looks negative) #else B q returnZero #endif //---- p0 = receiver must exist //---- [x0] // p13 = isa //---- / p16 = isa (p13) & ISA_MASK GetClassFromIsa_p16 p13 // p16 = class LGetIsaDone: // calls IMP or objc_msgsend_cached //---- if there is isa, go to CacheLookup CacheLookup NORMAL, _objc_msgSend #if support_tagged_int LNilOrTagged //---- = null, B q LReturnZero // nil check // tagged adrp x10, _objc_debug_taggedpointer_classes@PAGE add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF ubfx x11, x0, #60, #4 ldr x16, [x10, x11, LSL #3] adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF cmp x10, x16 b.ne LGetIsaDone // ext tagged adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF ubfx x11, x0, #52, #8 ldr x16, [x10, x11, LSL #3] b LGetIsaDone // SUPPORT_TAGGED_POINTERS #endif LReturnZero: X0 is already zero mov x1, #0 movi d0, #0 movi d1, #0 movi D2, #0 movi D3, #0 ret END_ENTRY _objc_msgSendCopy the code

There are mainly the following steps

  • [Step 1] Determine whether receiver, the first parameter of the objc_msgSend method, is null

    • If tagged Pointer is supported, skip to LNilOrTagged,

      • ifSmall objectsIs null, null is directly returned, i.eLReturnZero
      • ifSmall objectsIf not, small objects are processedisaTo the second step
    • If it is not a small object and receiver is not empty, the following two steps are performed

      • fromreceiverRemove theisadepositp13Register,
      • throughGetClassFromIsa_p16,arm64Through architectureisa & ISA_MASKTo obtainshiftclsBitfield class information, i.eclass.GetClassFromIsa_p16Then go to [Step 2]
.macro GetClassFromIsa_p16 /* SRC */ //---- watchOS #if SUPPORT_INDEXED_ISA // Indexed ISA //---- save isa value to p16 register mov  p16, $0 // optimistically set dst = src tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer ISA -- Determine if it is nonapointer ISA // isa in p16 is indexed //---- Read register ADRP X10, _objc_indexed_classes@PAGE //---- x10 = x10 + _objc_indexed_classes(page offset) --x10 base address _objc_indexed_classes@PAGEOFF //---- Start from the ISA_INDEX_SHIFT bit of p16 and extract the ISA_INDEX_BITS to register p16. Ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index LDR p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 1: //-- used for 64-bit systems #elif __LP64__ // 64-bit packed isa //---- p16 = class = isa & ISA_MASK and p16, $0, #ISA_MASK #else // 32-bit raw isa ---- mov p16, $0 #endif. Endmacro copy codeCopy the code
  • The second step is to obtain the ISA and enter the slow search processCacheLookup NORMAL

The CacheLookup cache looks for the assembly source code

//!!!!!!!!!重点!!!!!!!!!!!!
.macro CacheLookup 
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 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$1,
	//   then our PC will be reset to LLookupRecover$1 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
	//
LLookupStart$1:

//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)
	ldr	p11, [x16, #CACHE]				
//---- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets 
	
//--- p11(cache)右移48位,得到mask(即p11 存储mask),mask & p1(msgSend的第二个参数 cmd-sel) ,得到sel-imp的下标index(即搜索下标) 存入p12(cache insert写入时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式)
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask 

//--- 非64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 
	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

//--- p12是下标 p10是buckets数组首地址,下标 * 1<<4(即16) 得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数
	add	p12, p10, p12, LSL #(1+PTRSHIFT)   
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT是3
		             
//--- 从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//--- 比较 sel 与 p1(传入的参数cmd)
1:	cmp	p9, p1			// if (bucket->sel != _cmd) 
//--- 如果不相等,即没有找到,请跳转至 2f
	b.ne	2f			//     scan more 
//--- 如果相等 即cacheHit 缓存命中,直接返回imp
	CacheHit $0			// call or return imp 
	
2:	// not hit: p12 = not-hit bucket
//--- 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
	CheckMiss $0			// miss if bucket->sel == 0 
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素,),如果等于,则跳转至第3步
	cmp	p12, p10		// wrap if bucket == buckets 
//--- 定位到最后一个元素(即第一个bucket)
	b.eq	3f 
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//--- 跳转至第1步,继续对比 sel 与 cmd
	b	1b			// loop 

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置到最后一个元素
//--- p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) 
					// p12 = buckets + (mask << 1+PTRSHIFT) 
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
//--- 再查找一遍缓存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//--- 比较 sel 与 p1(传入的参数cmd)
1:	cmp	p9, p1			// if (bucket->sel != _cmd) 
//--- 如果不相等,即走到第二步
	b.ne	2f			//     scan more 
//--- 如果相等 即命中,直接返回imp
	CacheHit $0			// call or return imp  
	
2:	// not hit: p12 = not-hit bucket
//--- 如果一直找不到,则CheckMiss
	CheckMiss $0			// miss if bucket->sel == 0 
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
	cmp	p12, p10		// wrap if bucket == buckets 
	b.eq	3f //如果等于,跳转至第3步
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//--- 跳转至第1步,继续对比 sel 与 cmd
	b	1b			// loop 

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
//--- 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached

	JumpMiss $0 
.endmacro

//以下是最后跳转的汇编函数
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// 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, x12, x1, x16	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP 
//--- 如果为GETIMP ,则跳转至 LGetImpMiss
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL 
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
复制代码
Copy the code

Mainly divided into the following steps

  • The first step is to translate the first address of the cache by 16 bytes (because in objc_class, the first address is just 16 bytes away from the cache, that is, the first address of isa is 8 bytes, and the first address of superClass is 8 bytes) to obtain the CAhCE, the high 16 bits of the cache store the mask, and the low 48 bits of the cache store the buckets. The p11 = cache

  • [Step 2] The buckets and mask are extracted from the cache respectively, and the hash subscript is calculated by the mask according to the hash algorithm

    • throughcacheandMask (i.e. 0x0000FFFFFFFFFFFF)the&Operation,High 16-bit mask nullsTo get the address of the buckets pointer, i.ep10 = buckets
    • willcacheMoves to the right48A,mask, i.e.,p11 = mask
    • willobjc_msgSendThe parameters of thep1(the second argument, _cmd)& msakThrough theThe hash algorithmTo find the storage SEL-IMPThe bucket subscript index, i.e.,p12 = index = _cmd & maskWhy this way? Because in theStore the sel - impWhen, also through the sameThe hash algorithm calculates the hash subscript for storage, soreadIt needs to go throughRead it the same way, as shown below

  • [Step 3] Extract the bucket corresponding to the hash index and buckets according to the obtained addresses of the hash index and buckets

    • Among themPTRSHIFTIs equal to 3, and the purpose of moving 4 bits to the left (i.e. 2^4 = 16 bytes) is to compute onebucketThe actual size of the occupied structurebucket_tIn theselAccount for8Bytes,impAccount for8byte
    • Based on the calculated hash indexThe index is multipliedA singleMemory size used by the bucket,bucketsThe first address inReal memoryIn theThe offset
    • throughInitial address + actual offset, get the corresponding hash indexbucket
  • [Step 4] According to the obtained bucket, remove imp and store it in P17, that is, P17 = IMP; remove SEL and store it in P9, that is, P9 = SEL

  • [Step 5] The first recursive loop

    • Compare sel in the obtained bucket to _cmd(that is, p1) in the second argument of objc_msgSend

    • If so, it jumps directly to CacheHit and returns IMP

    • If not, there are two cases

      • If you can’t find it, jump toCheckMissBecause the$0isnormal, will jump to__objc_msgSend_uncached, that is, to enterSlow search process
      • ifBucket obtained based on indexIs equal to theFirst element of Buckets,artificialThe will ofThe bucket is currently set to the last element of buckets(byShift the buckets head address +mask 44 to the right(equivalent to moving 4 to the left) directlyLocate the last element of the bucker) and continue the recursive loop (The first oneRecursion loops nestedThe secondRecursive loop), namely [Step 6]
      • ifThe bucketIs not equal toFirst element of Buckets, then continueLook aheadAnd into theThe first recursive loop
  • [Step 6] The second recursive loop: Repeat step 5, if the bucket is still equal to the first element of buckets, then jump to JumpMiss, where $0 is normal, and jump to __objc_msgSend_uncached, and cached. Enter the slow search process

Here’s the wholeQuickly findprocessThe change of the valueprocess