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: