Today we continue our exploration of the nature of iOS Runtime.

Writing in the front

OC is a very dynamic programming language, and its dynamic is the Runtime BASED API. Runtime plays an important role in our actual development, and we often encountered runtime-related interview questions in the interview process. In previous installtions of exploration and analysis, we also often looked at the underlying source code of Runtime to see the relevant implementation. Runtime is very important for iOS developers. To learn and master Runtime technologies, you need to start with some common data structures at the bottom of Runtime. Master its underlying structure, we can learn to get twice the result with half the effort. Today we look at the message mechanism of OC.

Message mechanism

OC method calls are implemented through the message mechanism and are converted to objc_msgSend function calls.

OCtheMessage mechanismIt can be divided into three stages:

1. Message sending stage: search method from method cache list and method list of class and parent class;

2. Dynamic parsing stage: if the method is not found in the message sending stage, it will enter the dynamic parsing stage and be responsible for dynamically adding method implementation;

3. Message forwarding stage: if the dynamic parsing method is not implemented, the message forwarding stage will be carried out, and the message will be forwarded to the receiver who can process the message for processing;

If message forwarding is also not implemented, the classic error is reported: unrecognzied selector sent to instance, the error that the method cannot find, and is unable to recognise the message.

Let’s look at how each of the three phases of the messaging mechanism is implemented through source code.

1. Message sending

In the project method call frequency is very high, so in order to improve efficiency, in the underlying code objc_msgSend function is written by assembly language, we find objC-MSG-arm64. s assembly file in the source code, to specific analysis of the implementation of objc_msgSend function.

objc_msgSendThe message receiver is first determined in the functionreceiverWhether it is empty. If the incoming message recipient isnilWill performLNilOrTagged.LNilOrTaggedIt will be executed internallyLReturnZeroAnd theLReturnZeroThe inside is directreturn 0.

If the incoming receiver receiver is not null, the receiver’s class is found through the ISA pointer of the receiver receiver, and CacheLookup is executed to fetch the lookup from the method cache. If found in the method cache list, CacheHit is executed, calling the method or returning the function address; CheckMiss is executed if it is found. Used for __objc_msgSend_uncached. Not cached.

__objc_msgSend_uncached calls MethodTableLookup and looks for it in the method list. __class_lookupMethodAndLoadCache3 MethodTableLookup within the core of the code is the c language function _class_lookupMethodAndLoadCache3 (double underscore into a single underscore).

The above analysis is summarized with a simple flow chart:

So let’s go into_class_lookupMethodAndLoadCache3Function that analyzes how to find a method from a list of methods.

_class_lookupMethodAndLoadCache3 function

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

The function internally calls lookUpImpOrForward, passing in three BOOL arguments.

LookUpImpOrForward function

IMP lookUpImpOrForward(Class CLS, SEL SEL, id INst, bool initialize, bool cache, bool resolver) {  initialize = YES , cache = NO , resolver = YES IMP imp = nil; bool triedResolver = NO; runtimeLock.assertUnlocked(); If (cache) {imp = cache_getImp(CLS, sel); if (imp) return imp; } runtimeLock.read(); if (! cls->isRealized()) { runtimeLock.unlockRead(); runtimeLock.write(); realizeClass(cls); runtimeLock.unlockWrite(); runtimeLock.read(); } if (initialize && ! cls->isInitialized()) { runtimeLock.unlockRead(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.read(); } retry: runtimeLock.assertReading(); // Prevent dynamic method addition, cache will change, look up cache again. imp = cache_getImp(cls, sel); If (imp) goto done; if (imp) goto done; Method meth = getMethodNoSuper_nolock(CLS, sel); // Method meth = getMethodNoSuper_nolock(CLS, sel); If (meth) {// if the method exists, the method is cached, and the internal call is cache_fill. log_and_fill_cache(cls, meth->imp, sel, inst, cls); Meth ->imp = meth->imp; goto done; {unsigned Attempts = unreasonableClassCount(); // If the parent cache list and method list cannot find the method, go to the parent class to find the method. 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."); } imp = cache_getImp(curClass, sel); if (imp) { if (imp ! Log_and_fill_cache (CLS, log_and_fill_cache(CLS, CLS, log_and_fill_cache(CLS, CLS, CLS, CLS, CLS, CLS, CLS) imp, sel, inst, curClass); // execute done, imp goto done; } else {// break the loop and stop searching for break; Method meth = getMethodNoSuper_nolock(curClass, sel); If (meth) {// cache log_and_fill_cache(CLS, meth->imp, sel, inst, curClass); imp = meth->imp; // execute done, imp goto done; }}} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- message phase is completed, didn't find method, into the dynamic phase -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / / first check whether has been marked as the method of dynamic analysis, If (resolver &&! triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); // mark triedResolver as YES, and the next time the dynamic resolver will not enter triedResolver = YES; goto retry; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- dynamic phase is completed, entered the stage of forward -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - imp = (imp) _objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlockRead(); // return method address imp; }Copy the code

GetMethodNoSuper_nolock function

Find a method in the method list

getMethodNoSuper_nolock(Class cls, SEL sel) { runtimeLock.assertLocked(); assert(cls->isRealized()); CLS ->data() returns class_rw_t // class_rw_t->methods returns methods for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists ! = end; ++mlists) {// mlists for method_list_t method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil; }Copy the code

The getMethodNoSuper_nolock function iterates through the list of methods to get method_list_t and finally finds the search_method_list function

static method_t *search_method_list(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); // If the list of methods is already sorted, use binary lookup to find the methods, To save time if (builtin_expect (methodListIsFixedUp && methodListHasExpectedSize, 1)) { return findMethodInSortedMethodList(sel, mlist); } else {// Look for for (auto& meth: *mlist) {if (meth. Name == sel) return &meth; } } return nil; }Copy the code

FindMethodInSortedMethodList function binary search principle

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) { assert(list); const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; // >>1 indicates that each binary bit of variable N is moved one bit to the right, and the highest bit complements binary 0. // count >>= 1 If count is even the value changes to (count / 2). If count is odd, change to (count-1) / 2 for (count = list->count; count ! = 0; Count >>= 1) {// probe = base + (count >> 1); Uintptr_t probeValue = (uintptr_t)probe->name; While (probe > first && keyValue == (Uintptr_t)probe[-1].name) {probe--; } // return (method_t *)probe; If (keyValue > probeValue) {base = probe + 1; count--; } } return nil; }Copy the code

From the above analysis, we understand the first phase of the message sending phase in the messaging mechanism. Let’s use a diagram to summarize the overall process:

2. Dynamic method analysis

A dynamic method resolution phase occurs when a method cannot be found in either the method cache list or the method list of the class or its parent. As we see in the source code for the message sending phase, the dynamic method resolution phase is entered through the function _class_resolveMethod.

_class_resolveMethod function

Functions are internally called depending on whether they are metaclass or class, and dynamic method resolution of class methods and object methods calls different functions:

Is called when an object method is dynamically resolved+(BOOL)resolveInstanceMethod:(SEL)selMethods.

Is called when a class method is dynamically resolved+(BOOL)resolveClassMethod:(SEL)selMethods.

After dynamically parsing the method, triedResolver = YES; The next time, instead of the dynamic parsing phase, it will go back to the message sending phase, retry, and look up the method again.

We can use dynamic method resolution to add methods dynamically. We will beMPersonIn the classtestMethod implementation comment out, useotherMethod insteadtestMethod implementation:

As you can see from the diagram, let’s comment it outtestThe system has reported a warning after the implementation of the method, let’s test the code:

When callingMPersonthetestMethod when printed out[MPerson other]. The dynamic adding method succeeded.

Note here that the class_addMethod function is used to add a new method to a class with a given name and implementation. Class_addMethod adds an override of a method implementation, but does not replace an existing implementation. That is, if the -(void)test method is already implemented in the above code, no more methods will be added dynamically.

3. Message forwarding stage

If the first two phases fail, the third phase, the message forwarding phase, occurs. Since the message mechanism in OC is not open source, the principle of message forwarding is directly explained here.

When the message forwarding phase is entered, it determines whether other objects have been specified to execute the method. Specific check to see if the current class implements forwardingTargetForSelector function, if the return value is not null, it means that specifies the forward goal, then can let forward target process the message.

If there is no specified forwardingTargetForSelector function returns nil, forward the goal, will call methodSignatureForSelector method, used to return a method signature, this is the last chance to jump method.

If methodSignatureForSelector methods return the correct signature is called forwardInvocation method, provides a NSInvocation forwardInvocation method type parameters, NSInvocation encapsulates a method invocation, including the invocation invocation, the method name, and the method parameters. Modify the method invocation object in the forwardInvocation function.

If methodSignatureForSelector returns is nil, would come to doseNotRecognizeSelector: within a method, The program crash reported the classic error unrecognized selector sent to instance.

At this point,OCtheMessage mechanismIs the end of the analysis ofOCMethod calls are actually converted toobjc_msgSendA function call that sends a message (selector method name) to the method caller (receiver). The method invocation process consists of three phases: message sending, dynamic method parsing, and message forwarding.

[original address](www.jianshu.com/p/c8d98d39f…)