I. Introduction to Runtime

A set of APIS written in C, C ++, and assembly that provide runtime functionality for Object-C. Source reference

There are actually two versions of Runtime: “Modern” and “Legacy”. Our current Version of Objective-C 2.0 runs on the Modern Runtime system and only runs on 64-bit applications after iOS and macOS 10.5. MaxOS’s older 32-bit programs still use the Runtime system from objective-C 1’s (earlier) Legacy versions.

The biggest difference between the two versions is that when you changed the layout of a class’s instance variables, in earlier versions you had to recompile its subclasses, whereas in current versions you did not.

Second, the nature of message sending

1, objc_msgSend

If you are familiar with object-C, you know that Object method call is actually a message sending process.

Start by defining an object, Son, that contains an instance method. M, clang-rewrite-objc main.m, to generate a.cpp file:

--------------main.m---------------
Son *son = [Son new];
[son son_instanceSelector];

--------------main.cpp---------------
Son *son = ((Son *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Son"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)son, sel_registerName("son_instanceSelector"));
Copy the code

Objc_msgSend (id _Nullable self, SEL _Nonnull op,…)

2, objc_msgSendSuper

Clang (Father, Son); clang (Father, Son);

-----------------------------Son.m-----------------------------
-(void)son_instanceSelector{ [super father_instanceSelector]; } + (void)son_classSelector{
    [super father_classSelector];
}

-----------------------------Son.cpp---------------------------
static void _I_Son_son_instanceSelector(Son * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("father_instanceSelector"));
}

static void _C_Son_son_classSelector(Class self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(&(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getMetaClass("Son"))}, sel_registerName("father_classSelector"));
}
Copy the code

Struct objc_super * _Nonnull super, SEL _Nonnull op… Is to look for a method in the parent class’s method list and call it.

Note: The body of the method call is still a subclass object.

3. Method call summary

So, what does our normal method call look like using the Runtime low-level function? As follows:

// class object instance method call
objc_msgSend(son, sel_registerName("son_instanceSelector"));

// Class method call
objc_msgSend(objc_getClass("Son"), sel_registerName("son_classSelector"));

// Send a message to the parent class (instance method)
struct objc_super kmSuper;
kmSuper.receiver = son;
kmSuper.super_class = [Father class];
objc_msgSendSuper(&kmSuper, @selector(father_instanceSelector));

// Send a message to the parent (class method)
struct objc_super myClassSuper;
myClassSuper.receiver = [son class];
myClassSuper.super_class = class_getSuperclass(objc_getMetaClass("Son"));
objc_msgSendSuper(&myClassSuper, NSSelectorFromString(@"father_classSelector"));
Copy the code

Third, method search process

1. Quick search process

Objc_msgSend’s quick lookup process is implemented in assembly for several main reasons

  • C cannot write a function that preserves unknown parameters and jumps to an arbitrary function pointer. The C language does not have the necessary features to do this.
  • Higher performance, assembly is closer to the system’s underlying language.

Objc_msgSend:

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	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
    // person-isa - class
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
Copy the code
  • 1, normal short judgment processing
  • 2. TAGGED_POINTERS (explore this later)
  • 3, get the class from isa pointer (the method that stores it in class and the method cache)
  • 4, CacheLookup check the method cache

CacheLookup ();

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if! __LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	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
  • Translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate translate

  • 2, check whether the cache hit, retun IMP

    Through assembly search method cache, cache hit, is the method search fast process, not hit, start to go method search slow process.

  • 3. If the cache is not hit, call the CheckMiss function

CheckMiss source code:

.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

Because we called CacheLookup NORMAL earlier, it goes to objc_msgSend_uncached:

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

There is only one function call -MethodTableLookup:

.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, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+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, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro
Copy the code

Did some preparations for memory, and then calls the function _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

From there, you move from assembly to C/C++. The real method is slow search flow.

2, slow search process

The method cache was missed in the quick lookup process. That is, when fast lookup doesn’t work, the bottom layer moves to the slow lookup process, and goes all the way from assembly to lookUpImpOrForward.

LookUpImpOrForward source code:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Determine whether to search from the cache. If yes, go to the method cache first
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // Lock to prevent return errors caused by multithreaded concurrency
    runtimeLock.lock();
    checkIsKnownClass(cls);
    
    // Prepare for the method lookup
    Class, metaclass, and its parent, its metaclass, up to its root, root metaclass
    if(! cls->isRealized()) { realizeClass(cls); }// Make sure the class is initialized
    if(initialize && ! cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); } retry: runtimeLock.assertLocked();// Search the method cache of the class first. The previous call cache may already exist when multiple threads are concurrently called
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Look in the current class method list
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        // goto done (return imp)
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            gotodone; }}// Find the method cache and method list of the parent class
    {
        unsigned attempts = unreasonableClassCount();
        for(Class curClass = cls->superclass; curClass ! = nil; curClass = curClass->superclass) {// Recursive error
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Start by looking in the parent class's method cache
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if(imp ! = (IMP)_objc_msgForward_impcache) {// Find the method in the superclass cache and cache it in the subclass
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break; }}// Look in the list of methods of the parent class
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // Find the method in the list of methods of the parent class and cache the method in the child class
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                gotodone; }}}// No method implementation found, call a method dynamic resolution
    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;
    }

    // When a method implementation cannot be found and dynamic resolution of the method is useless
    // The message will be forwarded (as described in the next article).
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

Copy the code

The slow lookup of a method follows the rule of looking for a method of the class itself, looking for a parent method if you can’t find one, and then looking all the way to NSObject.

_class_resolveMethod:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }}}Copy the code

If we implement the resolveInstanceMethod or resolveClassMethod method in our class and handle sel correctly, we can avoid errors. It returns a method that implements imp and tells the program to look it up again. Ex. :

We call an instance method of the Person object in the main thread that is not implemented

Person *per = [Person alloc];
[per performSelector:@selector(run)];
Copy the code

Then, add the dynamic parsing function to person.m:

----------------------Person.m----------------------
#import "Person.h"
#import <objc/runtime.h>

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        class_addMethod(self, sel, (IMP)methodImp, "v@:");
        return YES;
    }
    return NO;
}

void methodImp(id self,SEL _cmd){
    NSLog(@"Here we go, buddy...");
}

@end
Copy the code

Print result:

Here we go, man…

The same is true for resolveClassMethod.

Four,

The flow chart of method search is as follows: