This chapter content
- When does the method start to insert?
- Add knowledge of the three ways runtime is initiated to invoke the underlying
- 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
- 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
- Go to see first
log_and_fill_cache
Source code, in fact, for us this method is not useful at all
- To view
lookUpImpOrForward
Source code, we found it to calllog_and_fill_cache
Place.
- Check the method
_objc_msgSend_uncached
Source code, we found that he is actually written with assembly. And looking at the assembly alone we don’t see it callinglookUpImpOrForward
Method, but foundMethodTableLookup
methods
- To find the
MethodTableLookup
Method and then we find out that it’s calledlookUpImpOrForward
Method, but who called it_objc_msgSend_uncached
?
- And then we have to go back to
_objc_msgSend_uncached
I look it up and I find the whole file and I call it in two places one is_objc_msgSend
, one is_objc_msgSendSuper2
Method is called inCacheLookup
The _objc_msgsend_cached parameter is passed. Both of them are pretty much the same so_objc_msgSendSuper2
It’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
- OC code layer
- NSObject service layer and Runtime interface layer
- When the compiler, which is responsible for translation, turns the upper layer of code into the middle layer like Clang
- Runtime’s underlying library
Set runtime base mode:
- Call directly at the OC level we often use, such as calling methods, etc
- Call its API through the NSObject layer, such as isKindOfClass, etc
- 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?
- 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
- 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
- Check whether P0 (receiver message receiver) exists, if yes, 2 process, if no, check whether it is arm64 architecture, if yes, execute
LNilOrTagged
Methods orLReturnZero
- Get the isa value of receiver and give it to register P13. Continue process 3
- call
GetClassFromIsa_p16
Then you get a Class with P16 as receiver. Continue 4 Process - call
CacheLookup
Method, 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
- The CMP comparison instruction is explained in the text
- B Jump instruction
- The LDR assignment instruction is to give the memory data pointed to in the following memory address to the previous one.
LGetIsaDone
Is 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
- 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
. - Register values: p13 = isa, p16= Class
- CACHE_MASK_STORAGE is a macro defined in
defined(__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
- x15 = Class
- Shift Class by 16 bytes to p11, which is p11 = Class cache with the first value of _bucketAndMaybeMask. So, p11 = _bucketAndMaybeMask
- Fetch buckets() according to P11, which is p10 = buckets;
- Let’s see if p11 is zero. If it’s not zero, go
LLookupPreopt
Method, which is to share the cache, but generally 0 to 5 processes - So I’m going to hash out a hash, and I’m going to put it in p12, and p12 = index
- Then p13 = buckets[index] to get a cache bucket_T
- P17 = IMP, p9 = sel, give one value of P13 to P17 and one to P9
- 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
- CMP comparison instruction, if the comparison is successful go to the following conditions, otherwise skip
- B Jump instruction
- The LDR assignment instruction is to give the memory data pointed to in the following memory address to the previous one.
- TBNZ comparison instructions, for example
TBNZ p0, #0 function
Is 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