This chapter content

  1. When does the method start to insert?
  2. Add knowledge of the three ways runtime is initiated to invoke the underlying
  3. Objc_msgSend assembler source analysis, and the search process

View the insert method flow

The reason we look at the method insert process is that a method read must be inserted first. Just like objc_msgSend, which is designed to find that method for the recipient of the message, can we guess whether it must be found from the cache first for performance reasons? But what about the first time? Does it not find it in the cache, so does it do an insert on the first time to make the second search faster?

Objc breakpoint Look at insert’s method call stack

Conclusion: According to this section, we can see that the process of insert is: _objc_msgSend or _objc_msgSendSuper2 –> _objc_msgSend_uncached –> lookUpImpOrForward –> log_and_fill_cache –> INSERT

Note: we are working backwards from the very inside out (i.e., the correct order is 6->1, but we are working backwards). So our ultimate goal is to find objc_msgSend, which is actually where we’re going to cache the methods. Then the assembler looks for the arm64 architecture file

  1. We already know that the stack is in and out process, so we can know from this diagram that the process before insert is in order:_objc_msgSend_uncached –> lookUpImpOrForward –> log_and_fill_cache –> insert

  1. Go to see firstlog_and_fill_cacheSource code, in fact, for us this method is not useful at all

  1. To viewlookUpImpOrForwardSource code, we found it to calllog_and_fill_cachePlace.

  1. Check the method_objc_msgSend_uncachedSource code, we found that he is actually written with assembly. And looking at the assembly alone we don’t see it callinglookUpImpOrForwardMethod, but foundMethodTableLookupmethods

  1. To find theMethodTableLookupMethod and then we find out that it’s calledlookUpImpOrForwardMethod, but who called it_objc_msgSend_uncached?

  1. And then we have to go back to_objc_msgSend_uncachedI look it up and I find the whole file and I call it in two places one is_objc_msgSend, one is_objc_msgSendSuper2Method is called inCacheLookupThe _objc_msgsend_cached parameter is passed. Both of them are pretty much the same so_objc_msgSendSuper2It’s not posted.

There are three ways to tune the underlying runtime

There are two versions of Runtime, the Legacy (earlier) version with objective-C 1.0 programming interface, and the Modern (current) version with Objective-C 2.0 programming interface.

The runtime

Compile time: When the code is being compiled, that is, when it is not loaded into memory. That’s when the compiler does some parsing and some of the usual things like syntax errors will be analyzed and the project won’t compile.

Runtime: The code is already running and loaded into memory. For example, if you call a class that declares a method but doesn’t implement it, you build the project and it builds, but if you run it, it crashes.

The hierarchy of the runtime

  1. OC code layer
  2. NSObject service layer and Runtime interface layer
  3. When the compiler, which is responsible for translation, turns the upper layer of code into the middle layer like Clang
  4. Runtime’s underlying library

Set runtime base mode:

  1. Call directly at the OC level we often use, such as calling methods, etc
  2. Call its API through the NSObject layer, such as isKindOfClass, etc
  3. The underlying API for objc, such as class_getInstanceSize

For example

1. As we can see from the figure below, any process of calling a method is actually the process of sending a message. Objc_msgSend (message receiver, message body).

2. For example, we can call a method using objc_msgSend.

Note: If you want to call objc_msgSend, you need to change to NO in Build Settings -> Enable Strict Checking of objc_msgSend Calls

/ / this method in the class Person - (void) personWithAge: (int) age withName: (nsstrings *) name {NSLog (@ "-- -- -- -- -- - % d % @", the age, name); } // Person *p = [Person alloc]; Objc_msgSend (p, @selector(personWithAge:withName:), 18, @" ha ");Copy the code

The output is —-18—– haha

For example, call objc_msgSendSuper(objc_super *, the body of the message). The Teacher class inherits from the Person class, and the Teacher object calls the methods of the Person class

/ / this method in the class Person - (void) personWithAge: (int) age withName: (nsstrings *) name {NSLog (@ "-- -- -- -- -- - % d % @", the age, name); } // Teacher *t = [Teacher alloc]; // Struct objc_super p_objc_super; p_objc_super.receiver = t; // This is the class that the first method looks for, For example, if you pass Teacher. Class, the method lookup process is Teacher -> Person(if not found) -> NSObject p_objc_super.super_class = person.class; Objc_msgSendSuper (&p_objc_super, @selector(personWithAge:withName:), 16, @" hee hee ")Copy the code

The output is —-16—– hee hee

Objc_msgSend call analysis

We can’t call any method without sending a message, but we find objc_msgSend in the objc source code is written in assembly. Why?

  1. In C, it is not possible to write a function with unknown arguments and jump to an arbitrary function pointer. C does not have the features necessary to do this
  2. We have a lot of code calling methods in our program, so it’s necessary to make sure that objc_msgSend needs to be processed faster

Note: This analysis is in real machine mode, that is, the ARM64 architecture

Objc_msgSend Cache hit

We are analyzing the source code of the ARM64 architecture, but the real machine (said 64 bit) register is X0 -> X28, 29 registers, each can store 8 bytes. X0-x7 is passed as an argument to a function, and x0 is often used as a function return value.

Objc_msgSend source

What does the method do

  1. Check whether P0 (receiver message receiver) exists, if yes, 2 process, if no, check whether it is arm64 architecture, if yes, executeLNilOrTaggedMethods orLReturnZero
  2. Get the isa value of receiver and give it to register P13. Continue process 3
  3. callGetClassFromIsa_p16Then you get a Class with P16 as receiver. Continue 4 Process
  4. callCacheLookupMethod, and then look for the method, and if there’s a method in the cache, the cache hits, and there isn’t, then execute_objc_msgSend_uncached.

The source code

Unwinding _objc_msgSend, NoFrame // Create a window, CMP p0, #0 // check if p0(receiver) is nil; If SUPPORT_TAGGED_POINTERS // Whether the taggedPointer type is supported, if arm64 is 1. #endif LDR p13, [x0] #endif LDR p13, [x0] #endif LDR p13, P13 receives the address data of x0(receiver), and the memory data corresponding to the address of x0 is ISA GetClassFromIsa_p16 P13, 1, x0 // gets the Class of the receiver, and gives the Class address corresponding to isa to P16. Pass in the parameters: p13(isa), 1, x0(receiver). As for why you want to find the class of the object, please see this method I gave an explanation LGetIsaDone: // Calls imp or objc_msgSend_uncached CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached _objc_msgSend_uncached #if SUPPORT_TAGGED_POINTERS LNilOrTagged: b.eq LReturnZero // nil check GetTaggedClass 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

supplement

  1. The CMP comparison instruction is explained in the text
  2. B Jump instruction
  3. The LDR assignment instruction is to give the memory data pointed to in the following memory address to the previous one.
  4. LGetIsaDoneIs nearly finished, then proceed to the following code

Method GetClassFromIsa_p16

This method is actually the process of ISA translation to get the Class. As for why objc_msgSend wants to find the class of an object, the main reason is that it wants to find the cache of the class, the method of caching.

We know from the objc_msgSend source code that it calls this method GetClassFromIsa_p16 p13, 1, x0, and we know that SRC is p13(isa), needs_auth is 1, Auth_address is receiver (message receiver such as object P). Then give the Class value in P16

The source code

/* note: auth_address is not required if ! needs_auth */ .macro GetClassFromIsa_p16 src, needs_auth, #if SUPPORT_INDEXED_ISA = arm64 and not 64-bit = 1 // Indexed ISA mov p16, \ SRC //p16 is isa, P16 = SRC TBZ p16, #ISA_INDEX_IS_NPI_BIT, 1f // isa in p16 is indexed adrp x10, _objc_indexed_classes@PAGE add x10, x10, _objc_indexed_classes@PAGEOFF ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 1: If needs_auth == 0 // _cache_getImp takes an authed class already mov p16, \ src. else // end up here // 64 bit packed isa // p16 = Class ExtractISA p16, \src, \auth_address .endif #else // 32-bit raw isa mov p16, \src #endif .endmacroCopy the code

Analysis of CacheLookup method

  1. We first need to understand the values of the parameters to this method when it is called through objc_msgSend: Mode =NORMAL, the Function =_objc_msgSend, MissLabelDynamic =__objc_msgSend_uncached, MissLabelConstant =MissLabelConstant.
  2. Register values: p13 = isa, p16= Class
  3. CACHE_MASK_STORAGE is a macro defined indefined(__arm64__) && __LP64__(it’s a real machine and it’s an ARM64 architecture and it’s 64-bit) with CACHE_MASK_STORAGE_HIGH_16 or CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS, But if we are in real machine mode, the result should be CACHE_MASK_STORAGE_HIGH_16

What does this method do

  1. x15 = Class
  2. Shift Class by 16 bytes to p11, which is p11 = Class cache with the first value of _bucketAndMaybeMask. So, p11 = _bucketAndMaybeMask
  3. Fetch buckets() according to P11, which is p10 = buckets;
  4. Let’s see if p11 is zero. If it’s not zero, goLLookupPreoptMethod, which is to share the cache, but generally 0 to 5 processes
  5. So I’m going to hash out a hash, and I’m going to put it in p12, and p12 = index
  6. Then p13 = buckets[index] to get a cache bucket_T
  7. P17 = IMP, p9 = sel, give one value of P13 to P17 and one to P9
  8. Start circular lookup, if p9 is sel(method) we are looking for, then cache cached, if not, check whether p9 is empty, if not, execute 7, otherwise execute MissLabelDynamic, which is the __objc_msgSend_uncached method we passed

The source code

/* CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached */

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

mov	x15, x16	// 将 Class的值给 x15。
LLookupStart\Function:
	// p1 = SEL, p16 = isa 
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // 根据上面注意3走下面的宏
	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
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //走这里
	ldr	p11, [x16, #CACHE]  // p11 = mask|buckets(将Class平移16字节的值给p11,也就是 p11 = Class的cache = _bucketAndMaybeMask)
    #if CONFIG_USE_PREOPT_CACHES //如果为真机模式走这里
      #if __has_feature(ptrauth_calls) //是否为A12处理器,它会走共享缓存的流程。这个不看
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
      #else  //走这里
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function // 看p11的0号位置不为0的话走LLookupPreopt, 否则继续往下走(在这里我们不看LLookupPreopt方法了)
      #endif
	eor	p12, p1, p1, LSR #7 //异或算法,最终结果放在p12(LSR按位右移)
        //p11, LSR #48 是代表了取 maybeMask的值,然后 and	p12, p12, mask,代表了将算出来的哈希坐标给p12
	and	p12, p12, p11, LSR #48 //得到p12 = 哈希坐标  x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
        // 这个是模拟器情况,比较简单,原理都是一样的
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	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
        //根据上面架构判断承接下来,我们仅需知道p10为buckets,p12为哈希坐标(注意这个哈希坐标它可不是最后的位置否则就代表他完全遍历了,如果是完全遍历的话就没必要走下面的4流程了)
        //p12, LSL #(1+PTRSHIFT) 左移4位,得到16进制
        // add	p13, p10, p13 = bucket_t,哈希坐标,得到p13为buckets平移哈希坐标位的值,也就是p13成为了一个bucket_t的值
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	//p17为imp,p9为sel	            // do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        // 如果说p1就是我们要查的方法则执行2,否则执行3
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
	//如果说已经找到方法执行CacheHit,缓存命中    //     } else {
2:	CacheHit \Mode				// hit:    call or return imp
	//看p9是否存在,如果为空则MissLabelDynamic  //     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
        //如果说bucket一直在那个边界里面则继续执行1,否则跳出循环
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

#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
        //看这里,p13为最终的bucket位置,也就是buckets[mask]
	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
        // 看这里 p12 先是上面算出来的哈希坐标index,然后p10是上面的buckets
        // 最终p12就是上面第一次遍历时的bucket_t
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

        // 看这里,然后,跟1流程差不多,只是现在从最后的位置再往前遍历,也就是完全遍历,但是是不是重复1流程的遍历呢?
						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        //比较 sel 和 p1的sel,如果命中则继续走2缓存命中
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	//如果 p9(sel)为nil的话走下面流程
        cmp	p9, #0				// } while (sel != 0 &&
        //ccmp 对比两个条件,p12(上面第一次遍历的bucket_t),
        //p13为最终位置bucket_t,然后一直--,如果说p13 > p12的话走下面,否则跳出循环
        //也就是不会重复遍历
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b
// 缓存没找到走objc_msgSend_uncached。下面的条件宏是如果是公用缓存(真机)的情况,才走流程5。
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

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// 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
Copy the code

Objc_msgSend Not cached. _objc_msgSend_uncached

We know that the method for objc_msgSend is looked up in the cache first. If the cache does not find it, we should run _objc_msgSend_uncached. The most important and important part of the process is lookUpImpOrForward. And it’s not written in assembly, it’s written in C++, so the process becomes the slow process of objc_msgSend

Analysis of the _objc_msgSend_uncached method

This method source code is minimal, only two methods are implemented, MethodTableLookup and TailCallFunctionPointer

The source code

        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
Copy the code

MethodTableLookup method analysis

This method is going to walk through the method table to enter the slow lookup process, the primary method is lookUpImpOrForward, and its return value x0 continues to execute TailCallFunctionPointer

The source code

.macro MethodTableLookup // Stores some information, this is not important, // lookUpImpOrForward(obj, sel, CLS, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 mov x2, x16 mov x3, // IMP in x0 mov x17, x0 RESTORE_REGS msgsend.endMacroCopy the code

Analysis of the TailCallFunctionPointer method

We now know that X17 is the return value of lookUpImpOrForward, which is IMP. This method has A12 processor, there is also a normal case, take the normal source code. It’s all the same

The source code

// $0 = x17; Macro TailCallFunctionPointer // $0 = function pointer value br $0. EndmacroCopy the code

supplement

Instruction supplement

  1. CMP comparison instruction, if the comparison is successful go to the following conditions, otherwise skip
  2. B Jump instruction
  3. The LDR assignment instruction is to give the memory data pointed to in the following memory address to the previous one.
  4. TBNZ comparison instructions, for exampleTBNZ p0, #0 functionIs to check whether bit 0 of P0 is 0. If you don’t walk the function for 0, if you walk the function for 0, you continue to walk the following instruction

Shared cache

For example, Apple has a separate memory for each APP, but the system’s UIKit,Foundation, and other things can’t be loaded for every APP, so the system’s libraries are in a shared cache. In general, the 0 bit of the method indicates whether to look up the shared cache