preface
The author sorted out a series of low-level articles about OC, hoping to help you. This article mainly explains the method search principle analysis.
1. Alloc principle of OC object creation in iOS
2. Align the OC object memory of iOS
3. The underlying principle of iOS OC ISA
4. Structural analysis of OC source code of iOS
5. Source code analysis of OC method cache of iOS
In iOS development we use a variety of methods in the class, and in OC the call to a method is called sending a message. The use of method functions is familiar to every iOS developer, but how to find method functions in the bottom layer is mainly introduced in this article.
1. The nature of the method
To cover the next steps, create a macOS project that defines a TestObject class and a testMethod method, and implement the following code in the main.m file
#import <Foundation/Foundation.h>
#import "TestObject.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestObject *objc = [[TestObject alloc] init];
[objc testMethod];
}
return 0;
}
Copy the code
Then use the terminal command clang-rewrite-objc main.m directly to compile a main. CPP file in the project directory to see the underlying implementation of the above code, resulting in the following code
TestObject *objc = objc_msgSend(objc_getClass("TestObject"), sel_registerName("alloc")), sel_registerName("init"));
objc_msgSend(objc, sel_registerName("testMethod"));
Copy the code
Sel_registerName is the equivalent of @selector, and alloc,init, and testMethod calls to TestObject are all underlying messages sent by objc_msgSend. You can see that the essence of the method is to send messages via objc_msgSend. Objc_msgSend has two parameters,id is the receiver of the message and SEL method number. Method calls are compiled into objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, and objc_msgSendSuper_stret, respectively. If the method calling the superclass is compiled into a function with a super field, objc_msgSend_stret is the method calling the structure.
2. Quick search of methods
Objc_msgSend is written in assembly, and the following source code is in arm64.
2.1 objc_msgSend assembly
//objc_msgSend has no window UNWIND _objc_msgSend, Nil runs to LReturnZero. // taggedPointer runs to LNilOrTagged CMP P0.#0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif// P13 is isa. If the receiver of the message is an object, the class can be found by ISA. LDR p13, [x0] // p13 = isa GetClassFromIsa_p16 p13 = class LGetIsaDone: CacheLookup NORMAL // calls imp or objc_msgSend_uncachedCopy the code
GetClassFromIsa_p16 if the receiver is null or taggedPointer if the receiver is null or taggedPointer if the receiver is null or taggedPointer if the receiver is null or taggedPointer if the receiver is null or taggedPointer if the receiver is null or taggedPointer
2.2 GetClassFromIsa_p16
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// 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:
#elif __LP64__
// 64-bit packed isa
and p16, $0.#ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
Copy the code
This part of the source code in the ARM64 architecture will only go under #elif __LP64__, pass through the isa&ISA_MASK to get the class, and return as p16, finally return above objc_msgSend. It will continue to execute LGetIsaDone’s CacheLookup.
2.3 CacheLookup
There are three kinds of which CacheLookup find way CacheLookup NORMAL | GETIMP | LOOKUP, NORMAL is NORMAL process, GETIMP find imp, LOOKUP method LOOKUP.
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x12 = address of cached IMP
#define CACHE (2 * __SIZEOF_POINTER__)
#define CLASS __SIZEOF_POINTER__Macro CacheLookup, where x16 is the class found byCache_t = cache_t; cache_t = cache_tCache_t is a 16-byte structure, bucket_t is a 8-byte structure, // p1 = SEL, p16 = isa LDP p10, p11, [x16,#CACHE] // p10 = buckets, p11 = occupied|mask
#if ! __LP64__
and w11, w11, 0xffff // p11 = mask
#endif//x12 is obtainedhashAnd w12, w1, w11 // x12 = _cmd & mask add p12, p10, p12, LSL#(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if(bucket->sel ! = _cmd) b.ne 2f // scan more CacheHit$0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if(bucket->sel ! = _cmd) b.ne 2f // scan more CacheHit$0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
Copy the code
This section is all about finding cache_t and checking to see if the method passed in is there at cache_t. For details about cache_t method caching, see the source code for iOS OC method caching. Determine whether buckect sel is equal to the CMD passed in in part 1, that is, if there is a cached method, return IMP if the cache hits CacheHit, and go to part 2 if there is none. Perform CheckMiss.
2.4 CheckMiss
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
Copy the code
This is the part that needs to be executed based on the $0 argument passed in earlier. $0 is NORMAL. We then execute __objc_msgSend_uncached, where objc_msgSend gets the quick lookup from cache_t and gets the slow lookup from cached.
3. Slow search of methods
Objc_msgSend does a quick lookup with cache_t in Part 2. When no cached method is found in the cache, a slow lookup with no cache is required.
3.1 objc_msgSend_uncached and MethodTableLookup
That’s what this section is
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, # - 16]!mov fp, sp // save parameter registers: x0.. x8, q0.. q7 sub sp, sp,# (10 * 8 + 8 * 16)
stp q0, q1, [sp, # (0 * 16)]
stp q2, q3, [sp, # (2 * 16)]
stp q4, q5, [sp, # (4 * 16)]
stp q6, q7, [sp, # (6 * 16)]
stp x0, x1, [sp, 16 + 0 # (8 * * 8)]
stp x2, x3, [sp, # 16 + 2 * (8 * 8)]
stp x4, x5, [sp, # (8 * 16 + 4 * 8)]
stp x6, x7, [sp, # 16 + 6 (8 * * 8)]
str x8, [sp, # (16 + 8 * 8 * 8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, # (0 * 16)]
ldp q2, q3, [sp, # (2 * 16)]
ldp q4, q5, [sp, # (4 * 16)]
ldp q6, q7, [sp, # (6 * 16)]
ldp x0, x1, [sp, 16 + 0 # (8 * * 8)]
ldp x2, x3, [sp, # 16 + 2 * (8 * 8)]
ldp x4, x5, [sp, # (8 * 16 + 4 * 8)]
ldp x6, x7, [sp, # 16 + 6 (8 * * 8)]
ldr x8, [sp, # (16 + 8 * 8 * 8)]
mov sp, fp
ldp fp, lr, [sp], # 16
AuthenticateLR
.endmacro
Copy the code
MethodList (bits ro) and methodList (RW) in cache_t. If you can’t find a method in cache_t, you need to find a method in methodList. The methodTable Rookup is a preparation for this section. Will perform to the __class_lookupMethodAndLoadCache3 this function.
3.2 class_lookupMethodAndLoadCache3
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
Copy the code
The obj passed in is the object, sel is the method number, and CLS is the class. LookUpImpOrForward is then called directly, with initialize YES,cache NO, and resolver YES as arguments, since the method cannot be found in the cache_t cache.
3.3 lookUpImpOrForward
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) returnimp; } // 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(); checkIsKnownClass(cls);if(! cls->isRealized()) { realizeClass(cls); }if(initialize && ! cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); // If sel == initialize, _class_initialize will send +initialize and //then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172 } retry: runtimeLock.assertLocked(); // Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists. { Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } // Try superclass caches and method lists. { unsigned attempts = unreasonableClassCount(); for (Class curClass = cls->superclass; curClass ! = nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. imp = cache_getImp(curClass, sel); if (imp) { if (imp ! = (IMP)_objc_msgForward_impcache) { // Found the method in a superclass. Cache it in this class. log_and_fill_cache(cls, imp, sel, inst, curClass); goto done; } else { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if(resolver && ! 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;
goto retry;
}
// No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); return imp; }Copy the code
The lookUpImpOrForward function is a bit too much code, so I’ll break it down a bit. RuntimeLock is a lock that prevents concurrent contention between threads.
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
Copy the code
Here is again to determine if there is a cache, directly in the cache to find the IMP back out.
runtimeLock.lock();
checkIsKnownClass(cls);
if(! cls->isRealized()) { realizeClass(cls); }if(initialize && ! cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); // If sel == initialize, _class_initialize will send +initialize and //then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172 }Copy the code
RealizeClass () {realizeClass () {realizeClass () {realizeClass () {realizeClass () {realizeClass () {realizeClass (); This section prepares you for the next class to look up methods in bits.
// Try this class's method lists. { Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { runtimeLock.assertLocked(); assert(cls->isRealized()); // fixme nil cls? // fixme nil sel? for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists ! = end; ++mlists) { method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil; } static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) { #if SUPPORT_MESSAGE_LOGGING if (objcMsgLogEnabled) { bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(), implementer->nameForLogging(), sel); if (! cacheIt) return; } #endif cache_fill (cls, sel, imp, receiver); }Copy the code
This section is looking for methods in the class. The getMethodNoSuper_nolock function loops through the SEL list in the CLS data() class. Returns method_t if found. Log_and_fill_cache is executed, and cache_fill is executed at the end. The method is then cached in cache_T again.
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for(Class curClass = cls->superclass; curClass ! = nil; curClass = curClass->superclass) { // Haltif there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if(imp ! = (IMP)_objc_msgForward_impcache) { // Found the methodin a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method // resolver for this class first. break; } } // Superclass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; }}}Copy the code
You can’t find a method in your class. You need to go to the parent class to find a method. Since all of our previous lookups enabled the objc_msgSend assembly lookups and CLS bits methodList lookups on the current class, and the methods of the parent class may also be cached, the loop through the parent class first looks for IMP via cache_getImp. Where _objc_msgForward_impcache is the function pointer method cache actually stored there. If the log_and_fill_cache function is executed directly, the log_and_fill_cache function is executed directly. If the log_and_fill_cache function is executed directly, the log_and_fill_cache function is executed directly. If an IMP is not found, or if an IMP is found to be forwarded, the cache is not cached, so getMethodNoSuper_nolock is called. If found, the method will still be cached.
4. Method search fails
The above description is method existence, if in the process of method search, can not find the case is reported exception, for example, execute the following code
TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];
Copy the code
// No implementation found. Try method resolver once.
if(resolver && ! 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;
goto retry;
}
// No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst);Copy the code
The _objc_msgForward_impcache function is eventually executed, and _objc_msgForward_impcache is assembled
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
Copy the code
From this, __objc_msgForward_impcache is executed to __objc_msgForward and eventually to __objc_forward_handler. After searching the source code, the objc_defaultForwardHandler function is executed to print out the error message.
#if ! __OBJC2__
// Default forward handler (nil) goes to forward:: dispatch.
void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;
#else
// Default forward handler halts the process.
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : The '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret
objc_defaultForwardStretHandler(id self, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif
Copy the code
whyobjc_msgSend
Is the bottom layer written in assembly?
- For better performance optimization and easier machine recognition, because method calls are very frequent and costly in projects, assembly can be used to optimize performance.
- Sometimes some method calls will have some unknown parameters and unknown types, if using C or C ++ are difficult to achieve, assembly can be very perfect to solve these problems.
5 concludes
Method lookups are performed under the objc_msgSend function, and there are fast lookups and slow lookups.
- Quick search: Pass first
objc_msgSend
Quick search, whileobjc_msgSend
It was done in the context of compilation. Enter theobjc_msgSend
Check whether the first memory value is empty or yestaggedPointer
If so, follow the procedure. If it’s not, it’s normal procedure to go throughGetClassFromIsa_p16
findisa
Through theisa
performCacheLookup
Go to the classcache_t
To find if the method is cached, if not executed__objc_msgSend_uncached
. At this point is equivalent to the fast search method is not found, the need to excessive to slow search. - Slow search: Pass
__objc_msgSend_uncached
You can performMethodTableLookup
Function to prepare for the next method that needs to be looked up in the bits of the class. It will eventually transition to c++ functions in assembly and executeclass_lookupMethodAndLoadCache3
. throughlookUpImpOrForward
Function to search through the list of methods of the class and its parent, and cache them if foundcache_t
In the. If not found and not doneforward
The operation will eventually be executed_objc_msgForward_impcache
Then go in__objc_msgForward
the__objc_forward_handler
The function reported an error.
At this point, the method of finding the underlying principle is introduced.