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_msgSendIs 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 firstobjc_msgSendQuick search, whileobjc_msgSendIt was done in the context of compilation. Enter theobjc_msgSendCheck whether the first memory value is empty or yestaggedPointerIf so, follow the procedure. If it’s not, it’s normal procedure to go throughGetClassFromIsa_p16findisaThrough theisaperformCacheLookupGo to the classcache_tTo 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_uncachedYou can performMethodTableLookupFunction 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. throughlookUpImpOrForwardFunction to search through the list of methods of the class and its parent, and cache them if foundcache_tIn the. If not found and not doneforwardThe operation will eventually be executed_objc_msgForward_impcacheThen go in__objc_msgForwardthe__objc_forward_handlerThe function reported an error.

At this point, the method of finding the underlying principle is introduced.