What is Runtime?

Runtime is the mechanism by which a system is running, the most important of which is the message mechanism. Runtime is written mostly in C, C++, and assembly in order to make dynamic systems more efficient.

High-level programming language to be executable files need to be compiled to first assembly language to assembly language for the machines, machine language is also a computer can recognize the only language, but OC is not directly compiled to assembly language, but need to transfer is pure C language to compile and assembly operations, from OC to the transition of the C language is implemented by the runtime.

What are SEL and IMP? What does SEL have to do with IMP?

Let’s start with code execution:



This is printed because the method compilation compiles to a function, which by default takes two arguments (id self, SEL _cmd), self is the message recipient, and _cmd is the method number.

SEL is method number: method number is used to find methods. IMP is a pointer to the implementation of a function. For the relationship between SEL and IMP, for example, we have a book, I can find the beginning of the content I want to find through the page in the book catalog. This is consistent with SEL and IMP. SEL is the directory, IMP is the opening page of the content, and the function implementation is the chapter content.

How did SEL find IMP?

Start by creating a Father class and one of its subclasses Son,Father that implements an instanceMethod instanceMethod

// Father
@interface Father : NSObject
- (void)instanceMethod;
@end
#import "Father.h"

@implementation Father
- (void)instanceMethod {
    NSLog(@"%s", __func__);
}
@end

// Son
@interface Son : Father

@end
#import "Son.h"

@implementation Son

@end
Copy the code

When we enable Debug assembly mode, look at the following code: callinstanceMethodThere will be aobjc_msgSendSo thisobjc_msgSendWhat is it? We can add a sign breakpoint and see thatClose the breakpoint and let the program execute to[a instanceMethod];The breakpoint for this line, and then open the symbolic breakpoint to continue runningThis suggests thatobjc_msgSendObjc is in the source code, which can be accessed fromopensourceDownload to, directly search objc, this time is used



Look in the source code

A quick lookup of methods

Since objc_msgSend is written in sinks, we look for ENTRY _objc_msgSend under arm64

	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
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

Copy the code

As you can see, the CacheLookup that goes first under _objc_msgSend is roughly understood by its name and comment, and there is a method caching mechanism that can be found through CacheLookup, Ached: objc_msgSend_uncached

.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 .endmacroCopy the code

As you can see in the source code, lookups can be found in three different ways: first, from finding CacheHit, which is called or returned directly

1: cmp p9, p1 // if (bucket->sel ! = _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp // CacheHit: x17 = cached IMP, x12 = address of cached IMP .macro CacheHit .if $0 == NORMAL TailCallCachedImp x17, x12 // authenticate and call imp .elseif $0 == GETIMP mov p0, p17 AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP ret // return IMP .elseif $0 == LOOKUP AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP ret // return imp via x17 .else .abort oops .endif .endmacroCopy the code

B: No CheckMiss

2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	
.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

Third, add it to the cache for next search

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

We should use __objc_msgSend_uncached, as we get it from NORMAL

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

So this MethodTableLookup is called a list of methods lookup, so let’s go in and look at that

.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, x0Copy the code

From which we found __class_lookupMethodAndLoadCache3, method name, said it is to find the method to add cache, but we won’t be able to search. However, this source is mixed in assembly, C, C + +, we can have a look at whether to find in the C or C + +, method is to remove the front of a “_”, search _class_lookupMethodAndLoadCache3, very good, found it. Let’s not go into the source code. This is the quick lookup part of method lookup. When we go to _objc_msgSend, we use CacheLookup NORMAL to do a quick lookup cache by assembly. Assembly is faster and more efficient, so this is the quick lookup phase. But in CacheHit, when nothing is found, Through MethodTableLookup call _class_lookupMethodAndLoadCache3 entered slowly find.

Slow lookup of method

We continue to see _class_lookupMethodAndLoadCache3 part of source code, this in objc runtime – new. Mm, there are new to this name, because the runtime actually had 2 version, now we use are all new.

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
Copy the code

As you can see, the lookUpImpOrForward method is called. Remembering the parameters passed in, let’s look inside the method

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) return imp;
    }
Copy the code

Although we pass the cache as NO this time, we don’t need to go through the cache, but we can understand this by looking at cache_getImp

/********************************************************************
 * IMP cache_getImp(Class cls, SEL sel)
 *
 * On entry:	a1 = class whose cache is to be searched
 *		a2 = selector to search for
 *
 * If found, returns method implementation.
 * If not found, returns NULL.
 ********************************************************************/

	STATIC_ENTRY _cache_getImp

// do lookup
	movq	%a1, %r10		// move class to r10 for CacheLookup
	CacheLookup NORMAL, GETIMP	// returns IMP on success

LCacheMiss:
// cache miss, return nil
	xorl	%eax, %eax
	ret

	END_ENTRY _cache_getImp
Copy the code

Visibility is quick lookup, that part of the process. Well, continue to look at the method to find the source

retry: runtimeLock.assertLocked(); Imp = cache_getImp(CLS, sel); if (imp) goto done;Copy the code

Why would I want to get it from cache again? If you do not check the cache, you will not be able to find the method this time, so you need to check the cache first. It then looks up the list of methods for the class.

Lookup from the class’s list of methods

// Try this class's method lists. {// Get method from class's method list, if not found, return 0x0, that is, nil method meth = getNosuper_nolock (CLS, sel); If (meth) {// if (meth) {// if (meth) {// if (meth) {// If (meth) {return log_and_fill_cache(CLS, meth->imp, sel, inst, CLS); imp = meth->imp; goto done; }}Copy the code

Let’s look at getMethodNoSuper_nolock

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

You can see that it looks up methodList from the class’s data(). I’m going to interrupt here, but there’s a basic thing to know,Example methods live in class objects, and class methods live in metaclass objects

Lookup from the list of methods of the parent class

Going back to the code for getMethodNoSuper_nolock, if it doesn’t have a method in the current class, it will try to find it in the parent class

// Try superclass caches and method lists. { unsigned attempts = unreasonableClassCount(); NSObject for (Class curClass = CLS ->superclass; 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."); Imp = cache_getImp(curClass, sel); If (imp) {// interpret is not a message forward method, this will explain 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; Method meth = getMethodNoSuper_nolock(curClass, sel); If (meth) {return log_and_fill_cache(CLS, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; }}}Copy the code

So what if I keep looking for NSObject and I can’t find a method? Then you enter the dynamic parsing process

Dynamic method parsing

// No implementation found. Try method resolver once. 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; }Copy the code

_class_resolveMethod(CLS, SEL, INST); To view

Void _class_resolveMethod(Class CLS, SEL SEL, id inst) {// Check whether the metaclass 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 (!) the root metaclass does not find the class method. lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }}}Copy the code

Non-metaclases execute _class_resolveInstanceMethod

static void _class_resolveInstanceMethod(Class cls, SEL sel, + (BOOL)resolveInstanceMethod:(SEL)name if (! LookUpImpOrNil (CLS ->ISA(), SEL_resolveInstanceMethod, CLS, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { Implemented // Resolver not implemented. Return; } // Send a message, that is, call the [CLS resolveInstanceMethod] method. BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveInstanceMethod adds to self a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

The metaclass executes _class_resolveClassMethod,

static void _class_resolveClassMethod(Class cls, SEL sel, id inst) { assert(cls->isMetaClass()); + (BOOL)resolveClassMethod:(SEL)name if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) {// Resolver not implemented. } // Send a message, that is, call the [CLS resolveClassMethod] method. BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; bool resolved = msg(_class_getNonMetaClass(cls, inst), SEL_resolveClassMethod, sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveClassMethod adds to self->ISA() a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

Dynamic parsing part of the source analysis is over, let’s take a look at how to use dynamic parsing.

Dynamic parsing uses simple examples

We can dynamically add unimplemented methods to a class based on runtime, taking missMethod as an example

#include <objc/runtime.h> void dynamicMethodIMP(id self, SEL _cmd) { NSLog(@" >> dynamicMethodIMP"); } + (BOOL)resolveInstanceMethod:(SEL)name { NSLog(@" >> Instance resolving %@", NSStringFromSelector(name)); If (name == @selector(missMethod)) {/ If missMethod binds the SEL to the corresponding IMP: dynamicMethodIMP class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:name]; }Copy the code

About “v@:” : this is actually the return value and parameter mark. The first character represents the return value,” v” represents no return value, and each subsequent character represents an argument, since each function has two default arguments,”@” represents an object, and “:” represents SEL. More characters contain comments Type Encodings

forward

If dynamic method resolution is not added, then message forwarding is entered

/ 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

_objc_msgForward_impcache corresponds to __objc_msgForward_impcache written in assembly

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

I can’t find the code that goes on, so I’ll leave it at that. Message forwarding includes fast message forwarding and slow message forwarding. Here is a simple example of message forwarding.

Fast Message forwarding

ForwardingTargetForSelector can convey method to a can deal with instances of the class of the method to deal with the method

- (id) forwardingTargetForSelector: (SEL) aSelector {the if (aSelector = = @ the selector (missMethod)) {/ / let can handle missMethod method instance to deal with return [Other new]; } return [super forwardingTargetForSelector:aSelector]; }Copy the code

If not implemented forwardingTargetForSelector, or no forwarding method to other instances will slowly forward into the news.

The message is forwarded slowly

Divided into two steps, first to achieve methodSignatureForSelector method, return a method signature

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(missMethod1)) {
        NSMethodSignature *ms = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return ms;
    }else if (aSelector == @selector(missMethod2)) {
        Method method = class_getInstanceMethod([self class], @selector(xxxFunc));
        const char *type - method_getTypeEncoding(method);
        return [NSMethodSignature signatureWithObjCTypes:type];
    }
    return [super methodSignatureForSelector:aSelector];
}
Copy the code

Also implement the forwardInvocation method

+ (void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"%s-%@",__func__, NSStringFromSelector(anInvocation.selector)); NSString *sto = @" parameter "; // Invocation invocation. Target = [OtherClass class]; // Add parameters from index=2, 0 and 1 are used by ID and SEL [anInvocation setArgument:& STO INDEX :2]; NSLog(@"%@",anInvocation.methodSignature); // which method to call anInvocation. Selector = @selector(run:); [anInvocation invoke]; }Copy the code

Attached is a message sending flow chart compiled by others: