This article belongs to “Jane Book — Liu Xiaozhuang” original, please note:

< Jane books – Liu Xiaozhuang > https://www.jianshu.com/p/014af0de67cd



The method call

In OC method calls are implemented through the Runtime, which essentially sends messages through the objc_msgSend() function.

For example, the following OC code is converted to Runtime code.

Objc_msgSend (object,@selector(testMethod));
Copy the code

The second parameter to send the message is an SEL parameter, which is often used in projects where different classes define the same method and therefore have the same SEL. Then the question comes, which is also a question that many people have asked in their blogs. Is SEL of different types the same?

However, the fact is that by our validation, we create two different classes, define two identical methods, get the SEL by @selector() and print it. We find that SEL is the same object and address is the same. Thus, it is proved that the same SEL of different classes is the same object.

@interface TestObject : NSObject
- (void)testMethod;
@end

@interface TestObject2 : NSObject
- (void)testMethod;
@end

The same goes for the TestObject2 implementation file
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p".@selector(testMethod));
}
@end

/ / the result:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
Copy the code

A table of SEL is maintained in Runtime. This table stores SELS not according to class. As long as the same SEL is regarded as one, it is stored in the table. When the project loads, all methods are loaded into the table, and dynamically generated methods are also loaded into the table.

Hidden parameters

We can get the current object through self inside the method, but where does self come from?

The essence of method implementation is also a C function, which has two default parameters in addition to the parameters passed by the method, which are also passed when called through objc_msgSend(). These two parameters are not declared in Runtime, but are generated automatically at compile time.

You can see the existence of these two hidden parameters in the objc_msgSend declaration.

objc_msgSend(void /* id self, SEL op, ... * / )
Copy the code
  • selfThe object that calls the current method.
  • _cmd, of the currently called methodSEL.

Neither of these parameters is explicitly declared in the invocation or implementation method, but we can still use them. The response object is self, and the selector of the method being called is _cmd.

- (void)method {
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
Copy the code

A function call

After an object is created, its own class and its parent class, all the way up to the NSObject class, are contained in the object’s memory, such as instance variables of its parent class. When a method of its parent class is called through [super class], a structure is created.

struct objc_super { id receiver; Class class; };
Copy the code

The call to super is converted to a call to objc_msgSendSuper(), and the objc_msgSend() function is called inside it. It is important to note that the receiver object passed in is still self, and the result returned is self’s class, even though it is called through [super class]. Therefore, receiver is the current object regardless of any method called by the current object.

objc_msgSend(objc_super->receiver, @selector(class))
Copy the code

In objc_msg.s, there are multiple versions of the objc_msgSend function. Internal implementation logic is generally the same, are achieved through assembly, but according to different situations have different calls.

objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret
Copy the code

In the source code above, the object with super is passed an objc_super structure object. Stret means that a struct type is returned, and super2 is an unexposed implementation of objc_msgSendSuper().

struct objc_super {
	id	receiver;
	Class	class;
};
Copy the code

Fp returns a long double, while FP2 returns a complex long double. All other float, double, and normal floating-point types use objc_msgSend. Except in these cases, everything else is called through objc_msgSend().

Message Sending Process

When an object is created, the system allocates memory for it and performs default initialization tasks, such as initialization of instance variables. Object the first variable isa pointer to its class object, -isa. The isa pointer can access its class object, and access all classes in its chain of successors through its class object’s ownership.

The ISA pointer is not part of the language and primarily serves the Runtime mechanism.

When an object receives a message, the message function looks for the method selector in the Method list along with the object ISA pointer to the class structure. If no corresponding selector is found in this class, objc_msgSend looks for a selector in its parent’s method list. If it doesn’t find one, it looks up the inheritance line until it finds NSObject.

The Runtime optimizes the selector lookup process by adding a cache field to the class structure. Each class has a separate cache that is added to the cache when a selector is called. Before searching the method list, the system checks whether the method list is in the cache. If no method list is in the cache, the system calls the method list. This improves the method search efficiency.

If the call through the OC code will go through the stage of message sending, if you do not want the process of message sending, you can get the function pointer to the method directly call. NSObject’s methodForSelector: a method gets a pointer to a function. Once it gets a pointer, it needs to type the pointer to a function that matches the calling function, and then make the call.

void (*setter) (id, SEL, BOOL);
int i;
 
setter = (void(*) (id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);
Copy the code

Realize the principle of

In Runtime, the objc_msgSend function is also open source, but it is implemented in assembly code. The arm64 architecture code can be found in objC-msG-arm64.s. Many of the most frequently executed functions in the Runtime are written in aggregates.

Objc_msgSend is not completely open source, in _class_lookupMethodAndLoadCache3 function has access to the Class parameter. So there must be a procedure for getting ISA_t from the object below, which, from the method names and comments, should be the GetIsaFast assembly command. If so, you can link the message sending to the invocation flow.

ENTRY	_objc_msgSend
	MESSENGER_START

	NilTest	NORMAL

	GetIsaFast NORMAL		// r11 = self->isa
	CacheLookup NORMAL		// calls IMP on success

	NilTestSupport	NORMAL

	GetIsaSupport	   NORMAL

// cache miss: go search the method lists
LCacheMiss:
	// isa still in r11
	MethodTableLookup %a1, %a2	// r11 = IMP
	cmp	%r11, %r11		// set eq (nonstret) for forwarding
	jmp	*%r11			// goto *imp

	END_ENTRY	_objc_msgSend
Copy the code
  • MESSENGER_START: The message starts to execute.
  • NilTest: Checks whether the object receiving the message isnilIf fornilIt goes straight back. That’s rightnilThe cause of sending invalid message.
  • GetIsaFast: Quickly obtainedisaThe object pointing to is a class object or metaclass object.
  • CacheLookupFrom:ache listGet cache fromselectorIf found, call its correspondingIMP.
  • LCacheMiss: If there is no cache hit, the method below this assembly is executed.
  • MethodTableLookup: If it is not found in the cache, themethod listIn the search.

cache_t

It would be time-consuming to look up the list of methods based on the object model every time a method was called. Runtime To optimize call times, add a cache_t cache field to objC_class to optimize call times.

In the message sending process of the objc_msgSend function, the first call of the same method has no cache, but there is a cache after the call, and subsequent calls directly call the cache. Therefore, method calls can be divided into two types: with cache and without cache, and the call stack in these two cases is different.

The IMP is first looked up from the cache, but since lookUpImpOrForward is already looked up in the cache when cache3 calls it, NO is passed in and does not go into the cahCE lookup block.

struct cache_t {
    // Stores the hash table of the cached method
    struct bucket_t *_buckets;
    // Total size occupied
    mask_t _mask;
    // Used size
    mask_t _occupied;
}

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};
Copy the code

When sending a message to an object, the Runtime finds the corresponding class object along isa, but does not immediately look for method_list. Instead, it looks for cache_list first, prioritized if there isa cache, rather than the list of methods.

This is the Runtime’s optimization for finding methods, which should theoretically be accessed more frequently in the cache. Cache_list is defined by cache_t and contains an array of bucket_t containing IMPs and keys. See objc-cache.mm for the source code.

If the class object is not initialized and the initialize parameter of lookUpImpOrForward is YES, the class needs to be created. The inside of the function is basically some basic initialization, and the parent class is checked recursively, and if the parent class is not initialized, the parent class object is initialized first.

	STATIC_ENTRY _cache_getImp

	mov	r9, r0
	CacheLookup NORMAL
	// cache hit, IMP in r12
	mov	r0, r12
	bx	lr			// return imp
	
	CacheLookup2 GETIMP
	// cache miss, return nil
	mov	r0, # 0
	bx	lr

	END_ENTRY _cache_getImp
Copy the code

The code for cache_getImp is shown below. However, this function is not open source, but a portion of the source code is available, written in assembly. It internally calls two functions, CacheLookup and CacheLookup2, which are also written in assembly.

After the first call, there is a cache. After entering objc_msgSend, the CacheLookup command is called, or if the cache is found. But the Runtime is not completely open source, and we still don’t see much of the implementation inside of the Runtime, as well as the CacheLookup command, which executes our method after the command is called.

CacheLookup NORMAL, CALL
Copy the code

Source code analysis

In the objc_msgSend assembly implementation above, there is an assembly call to MethodTableLookup. In this assembly call, the C function that looks up the list of methods is called. Here’s the lite code.

.macro MethodTableLookup
	
  // Call MethodTableLookup and internally execute cache3 (C)
	blx	__class_lookupMethodAndLoadCache3
	mov	r12, r0			// r12 = IMP

.endmacro
Copy the code

By calling in MethodTableLookup _class_lookupMethodAndLoadCache3 function, to find the list of methods. Internally, lookUpImpOrForward is used, and NO is passed in the cache field to indicate that the cache lookup is NO longer needed because it has already been done by assembly in cache3.

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

LookUpImpOrForward is multithreaded, so there are many internal locking operations. It has an internal runtimeLock variable of type rwLOCK_T, and runtimeLock controls read and write locks. There is a lot of logic code inside, here the internal implementation of the function has been simplified, the core code is posted below.

The isRealized function of the class object determines whether the current class is implemented. If not, the class is implemented through the realizeClass function. In the realizeClass function, version, RW, superClass, and so on are set.

// Execute the code to find IMP and forward
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // If cache is YES, IMP is searched from cache. If you are coming in from cache3, cache_getImp() is not executed
    if (cache) {
        // Use cache_getImp to search for IMP. If found, IMP is returned and the call ends
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();

    // Determine if the class is already created, and if not, instantiate the class
    if(! cls->isRealized()) {// Instantiate the class
        realizeClass(cls);
    }

    Initialize is executed the first time the current class is called
    if(initialize && ! cls->isInitialized()) {// Initialize the class and free up memory
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
    
 retry:    
    runtimeLock.assertReading();

    // Try to get the cache of this class
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    
    {
        // If no Method is found in the cache, the Method is retrieved from the list of methods
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // If Method is found, IMP is added to the cache and retrieved from Method
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            gotodone; }} {unsigned attempts = unreasonableClassCount();
        // Loop through the cache IMP or method list IMP for this class
        for(Class curClass = cls->superclass; curClass ! =nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Get the IMP of the parent cache
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if(imp ! = (IMP)_objc_msgForward_impcache) {If a parent method is found and is no longer in the cache, cache the method in the function below
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break; }}// In the parent class's method list, get the method_t object. If found, the IMP found is cached
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                gotodone; }}}// If not, try dynamic method resolution
    if(resolver && ! triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); triedResolver =YES;
        goto retry;
    }

    // If no IMP is found and dynamic method parsing is not processed, the message forwarding phase is entered
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}
Copy the code

The cached IMP is found in the cache_getImp function the first time the method is called. But if it is the first call, the cached IMP is not found and is executed in the getMethodNoSuper_nolock function. Here is the key code for the getMethod function.

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // From the methodList list, following the for loop, iterates from the beginning, moving one address backwards after each iteration.
    for(auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists ! = end; ++mlists) {// Matches sel parameters with method_t, and returns method_t if matches.
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
Copy the code

When an object’s method is called, looking up the object’s method is essentially iterating through the list of methods that isa points to, comparing SEL calls with the name field of the iterated method_t structure, and returning the IMP function pointer if equal.

// Find the corresponding method_t structure based on the SEL passed in
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 (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        for (auto& meth : *mlist) {
            // SEL is essentially a string
            if (meth.name == sel) return&meth; }}if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not"); }}}return nil;
}
Copy the code

In the getMethod function, the main function is to find and match the Class’s list of methods. The list of class_rw_t methods is in the Class class_data_bits_t. The data() function obtains the structure of class_rw_t from bits, then obtains the list of methods (methods), and traverses the list of methods.

If the corresponding IMP is not retrieved from the current class, the loop is entered. The loop starts with the current class and follows the chain of successors all the way to the root class until the corresponding IMP implementation is found.

The search process is the same as above, with cache_getImp looking for the parent’s cache and calling the corresponding implementation if found. If the cache is not found, the method of the parent class is called for the first time, and the getMethodNoSuper_nolock function is called to get the implementation from the list of methods.

for(Class curClass = cls->superclass; curClass ! =nil;
             curClass = curClass->superclass)
{
    imp = cache_getImp(curClass, sel);
    if (imp) {
        if(imp ! = (IMP)_objc_msgForward_impcache) { log_and_fill_cache(cls, imp, sel, inst, curClass);goto done;
        }
    }
    
    Method meth = getMethodNoSuper_nolock(curClass, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
        imp = meth->imp;
        gotodone; }}Copy the code

If no method implementation is found, the dynamic method resolution step is entered. The if statement determines whether the passed resolver parameter is YES, and whether a dynamic resolution has already been made. This code may be executed multiple times because of goto retry.

if(resolver && ! triedResolver) { _class_resolveMethod(cls, sel, inst); triedResolver =YES;
    goto retry;
}
Copy the code

If the conditions are met and it is the first time a dynamic method resolution is made, the _class_resolveMethod function is called in the if statement. There are two kinds of dynamic method resolution, _class_resolveClassMethod class method resolution and _class_resolveInstanceMethod instance method resolution.

BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
Copy the code

In both of these dynamic method resolutions, the function implementation essentially calls the resolveInstanceMethod: and resolveClassMethod: methods defined in NSObject via objc_msgSend.

Methods can be added dynamically to both methods, and after the method implementation is added, goto Retry is performed below, and the method lookup process is resumed. As can be seen from the triedResolver parameter, the dynamic method resolution only has one chance. If it is not found this time, the message forwarding process will be entered.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
Copy the code

If after the above steps, or do not find a way to achieve, then enter the dynamic message forwarding. In dynamic message forwarding, some remedies can be made for methods that are not implemented.

The following is the call stack from top to bottom after sending a message through the objc_msgSend function.

CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
Copy the code

Call summary

After calling the objc_msgSend function, there is a complex set of judgment logic, summarized below.

  1. Determines what is currently calledSELWhether to ignore, for exampleMac OSIf the garbage disposal mechanism is enabled in theretain,releaseAnd return one_objc_ignored_methodtheIMPIs used to flag ignore.
  2. Determines whether the object receiving the message isnilBecause in OC, yesnilSending a message is invalid because it is filtered by judgment criteria at call time.
  3. From the method’s cache list, passcache_getImpThe function searches for the cache and returns it if it finds itIMP.
  4. Of the current classmethod listAnd find if there is a correspondingSELIf yes, the command is obtainedMethodObject and fromMethodFetch from objectIMPAnd returnIMP(The result of this step isMethodObject).
  5. If not found in the current classSEL, then go to the parent class to find. In the first place to findcache listIf not found in the cachemethod listAnd so on until you find itNSObjectSo far.
  6. If the class’s inheritance system never finds the correspondingSEL, enter the dynamic method parsing. Can be found inresolveInstanceMethodandresolveClassMethodDynamically add implementations to both methods.
  7. Dynamic message parsing If there is no response, the dynamic message forwarding phase is entered. At this point, some processing can be done in the dynamic message forwarding phase, otherwise it willCrash.

The overall analysis

The population can be divided into three parts:

  1. Just callobjc_msgSendFunction after some internal processing logic.
  2. Complex searchIMPWill be involved incache listandmethod listAnd so on.
  3. The message forwarding process is displayed.

If no method is found in the cache list, the method is retrieved from the class’s method list using the methodTable ookup macro definition. In MethodTableLookup also call _class_lookupMethodAndLoadCache3 function, essentially just preach and cache field when NO, said don’t find from the cache list.

In the cache3 function, lookUpImpOrForward is called directly. The internal implementation of this function is complex, as can be seen in the Runtime Analyze. Just look for the lookUpImpOrForward function name and take a closer look at the internal implementation logic.


Due to typesetting problems, the reading experience is not good, such as layout, picture display, code and many other problems. So go to Github and download the Runtime PDF collection. Put all the Runtime articles together in this PDF, with a table of contents on the left for easy reading.

Please give me a thumbs up, thank you! 😁