IOS Low-level exploration series

  • IOS low-level exploration – alloc & init
  • IOS underlayer exploration – Calloc and Isa
  • IOS Low-level exploration – classes
  • IOS Low-level exploration – cache_t
  • IOS Low-level exploration – Methods
  • IOS Low-level exploration – message lookup
  • IOS Low-level exploration – Message forwarding
  • IOS Low-level exploration – app loading
  • IOS low-level exploration – class loading
  • IOS Low-level exploration – classification loading
  • IOS low-level exploration – class extension and associated objects
  • IOS Low-level exploration – KVC
  • IOS Basics – KVO

IOS leak check and fill series

  • IOS leak fix – PerfromSelector
  • IOS bug fix – Threads
  • – RunLoop for iOS
  • IOS – LLVM & Clang

First, dynamic method analysis process analysis

We analyzed dynamic method parsing in the previous chapter “Message Finding.” In order to better understand the specific process, we will go directly to source tracing.

Let’s start with the _class_resolveMethod method.

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

The general process is as follows:

  • Determine if the parse is a metaclass
  • If it is not a metaclass, then_class_resolveInstanceMethodPerform object method dynamic parsing
  • Call if it is a metaclass_class_resolveClassMethodPerform dynamic class method parsing
  • After the class method is dynamically parsed, query againclsIn theimpIf not, an object method dynamic parsing is performed

1.1 Object method dynamic parsing

Let’s look at the dynamic resolution of object methods first. We’ll go straight to the _class_resolveInstanceMethod method:

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    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() ? '+' : The '-', 
                         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() ? '+' : The '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

The general process is as follows:

  • Check if this is implemented+(BOOL)resolveInstanceMethod:(SEL)selClass method that returns directly if not implemented (throughcls->ISA()Get the metaclass, because class methods are object methods stored on the metaclass.
  • If currently implemented+(BOOL)resolveInstanceMethod:(SEL)selClass method, passesobjc_msgSendCall the class method manually
  • After the call is complete, query againclsIn theimp
  • ifimpIf it does, it logs the success of the dynamic resolution object method
  • ifimpIf not, the output is implemented+(BOOL)resolveInstanceMethod:(SEL)selAnd returnedYES“, but did not find itimpThe log

1.2 Class method dynamic parsing

The _class_resolveClassMethod method is used to dynamically resolve the class method:

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    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() ? '+' : The '-', 
                         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() ? '+' : The '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

The general process is as follows:

  • Assert whether it is a metaclass, if not, exit
  • Check if this is implemented+(BOOL)resolveClassMethod:(SEL)selClass method that returns directly if not implemented (throughcls-Because of the currentclsMetaclass, because class methods are object methods stored on metaclass.)
  • If currently implemented+(BOOL)resolveClassMethod:(SEL)selClass method, passesobjc_msgSendCall the class method manually, and note that unlike the dynamic object resolution method, this is requiredFind classes by metaclass and object, that is,_class_getNonMetaClass
  • After the call is complete, query againclsIn theimp
  • ifimpIf it does, it logs the success of the dynamic resolution object method
  • ifimpIf not, the output is implemented+(BOOL)resolveClassMethod:(SEL)selAnd returnedYES“, but did not find itimpThe log

One thing to note here is that if we try self instead of objc_getMetaClass(“LGPerson”) in the above example, this will cause the +(BOOL)resolveInstanceMethod (SEL) SEL method to be called, The problem actually happens at the class_getMethodImplementation method, which internally calls the _class_resolveMethod method, and our CLS passes self, So it will go again +(BOOL)resolveInstanceMethod:(SEL) SEL

1.3 specialNSObjectObject methods resolve dynamically

If CLS is a metaclass, that is to say, dynamic resolution of class methods is performed, the following source code is available:

_class_resolveClassMethod(cls, sel, inst); // It has been processed
        if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // Object method resolution
            _class_resolveInstanceMethod(cls, sel, inst);
        }
Copy the code

_class_resolveClassMethod isa bitmap that can be used to parse an object’s method if the dynamic resolution fails.

We know from this flow chart that the metaclass ultimately inherits from the root metaclass, which in turn inherits from NSObject, so that means that class methods stored in the root metaclass are equivalent to object methods stored in NSObject. When lookUpImpOrNil is executed, the system recursively looks for a list of methods on the parent of the metaclass. Because the metaclass and root metaclass are automatically generated, we can’t write them directly. For NSObject, we can use categories to achieve uniform dynamic resolution of class methods, but only if the class itself does not implement the resolveClassMethod method:

This explains why _class_resolveClassMethod takes another step in the object method resolution process.

2. Quick process of message forwarding

What happens next in the message lookup process if we don’t have dynamic method resolution?

// 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

The lookUpImpOrForward source code returns _objc_msgForward_impcache when dynamic parsing fails. Objc-msg-arm64.s assembler source objC-msG-arm64.s assembler source objC-msG-arm64

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

We can see that __objc_msgForward_impcache jumps to __objc_msgForward, and we get no useful information from __objc_msgForward. Is that when the trail goes cold? We have a meeting in front of the process, if found imp, cache filling and log printing, we might as well find the printed log file to see if there will be the content we need.

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if(! cacheIt)return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (- 1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = - 1;
            return true; }}// Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : The '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
Copy the code

Here we can clearly see where the log file is stored and how it has been named:

Here’s another caveat:

ObjcMsgLogEnabled will only be logged if objcMsgLogEnabled is true. We will search directly for where this value appears:

Obviously, by calling the instrumentObjcMessageSends to achieve print on and off. Here’s a simple test:

Let’s run it and go to /private/ TMP:

Let’s open this file:

We saw the familiar resolveInstanceMethod, but after that there are two methods: that we don’t have explored before forwardingTargetForSelector and methodSignatureForSelector. Then there will be print of doesNotRecognizeSelector method, at this time Xcode console print as follows:

We can see that ___forwarding___ occurs in the CoreFoundation framework. We are still as usual, will be subject to the official documentation, check the forwardingTargetForSelector and methodSignatureForSelector.

First forwardingTargetForSelector:

ForwardingTargetForSelector official definition is returned to the IMP’s message directed to the first object was not found, the said person is in this way can realize the civet cats in prince, can’t find the IMP, isn’t it, I send this message to other objects to deal with is not just a matter of? Let’s go straight to code:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
Copy the code

Here we return [LGTeacher alloc] directly, let’s run it:

We send “saySomething” to LGStudent, and LGTeacher responds to the message. About forwardingTargetForSelector, apple also gives some hints:

If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously If you return self from this method, the code would just fall into an infinite loop. If an object implements or inherits this method and then returns a result that is not null (not self), the return value is treated as the new message recipient object and the message is forwarded to that object. If you return self in this method, then obviously an infinite loop will occur.

If you implement this method in a non-root class, Invoking Super’s if your class has nothing to return for the given selector then you should return the result of invoking Super’s implementation. If you implement this method in a non-base class that has nothing to return, then you need to return the implementation of the parent class. Is the return [super forwardingTargetForSelector: aSelector]; .

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding. This method gives the object a chance to redirect unknown messages sent to it before the more expensive forwardInvocation: mechanical invocation. This is useful when you just want to redirect a message to another object and do it one order of magnitude faster than normal forwarding. This feature is useless in cases where the goal of the forwarding is to capture the NSInvocation or manipulate the parameters or return values during the forwarding.

From the above official document definition, we can clarify the thinking:

  • forwardingTargetForSelectorIs a fast message forwarding process that directly causes other objects to respond to unknown messages.
  • forwardingTargetForSelectorCan’t returnselfOtherwise it will fall into an infinite loop because of returnselfGo back to the current instance object and go through the message lookup process, and obviously come backforwardingTargetForSelector.
  • forwardingTargetForSelectorIf the message is forwarded to another object that can respond to the unknown message, the final return must be the same as the parameter and return value of the message to be searched. Otherwise, you need to go through another process.

Slow process of message forwarding

Above said if you want to eventually return must find the news and the content of the parameters and return values are not consistent, need to go to other processes, so what is the process, we then have a look at just another way to methodSignatureForSelector official documentation:

Official methodSignatureForSelector returns a NSMethodSignature method signature is the definition of object, the object contains the by a given selector identifies the description of the method.

This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature. This method is used for protocol implementation. This method is also used when the NSInvocation object must be created for message forwarding. If your object maintains a delegate or can handle messages that it does not implement directly, override this method to return the appropriate method signature.

We see at the end of the document that there is a method called forwardInvocation:

We go to the documentation for this method:

To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one. “To the response object itself can’t identify method, in addition to forwardInvocation: outside, still must be rewritten methodSignatureForSelector:. Forwarding message mechanism using from methodSignatureForSelector: access to the information to create the NSInvocation object is forwarding. Your override method must provide an appropriate method signature for a given selector, either by specifying a formula beforehand or by asking another object to provide a method signature.

Obviously, methodSignatureForSelector and forwardInvocation do not exist in isolation, need to appear together. Let’s go straight to the code:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}


- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);

   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}
Copy the code

Then view the print:

As you can see, first came to the methodSignatureForSelector, then came to the forwardInvocation, saySomething message is looked up at last.

A few more points to note about the forwardInvocation:

  • forwardInvocationThe method has two tasks:
    • Lookup can respondinInvocationObject of the encoded message in. This object need not be the same for all messages.
    • useanInvocationThe message is sent to the object.anInvocationThe results are saved, and the run-time system extracts the results and passes them to the original sender.
  • forwardInvocationMethod implementations can do more than forward messages.forwardInvocationIt can also, for example, be used to combine code that responds to a variety of different messages, avoiding the hassle of having to write separate methods for each selector.forwardInvocationMethods may also refer to several other objects in response to a given message, rather than forwarding it to just one object.
  • NSObjectforwardInvocationImplementation: only callsDosNotRecognizeSelector: method that does not forward any messages. Therefore, if you choose not to implementThe forwardInvocation, where an unrecognized message to the object will raise an exception.

At this point, we have explored the slow process of message forwarding.

4. Message forwarding flow chart

From dynamic message parsing to fast forwarding process to slow forwarding process, we can summarize the following flow chart:

Five, the summary

We started with objc_msgSend to explore what happens after a message is sent, which is a great help in understanding the underlying iOS. Of course, due to the author’s level, the process of exploration may have some flaws. Here’s a quick summary:

  • Dynamic method analysis is divided intoObject methods resolve dynamicallyandClass method dynamic resolution
    • Object methods resolve dynamicallyA message sender implementation is required+(BOOL)resolveInstanceMethod:(SEL)selmethods
    • Class method dynamic resolutionA message sender implementation is required+(BOOL)resolveClassMethod:(SEL)selmethods
  • Dynamic method resolution failure leads to message forwarding
  • Message forwarding is divided into two processes: fast forwarding and slow forwarding
  • The implementation of fast forwarding isforwardingTargetForSelectorAnd let other objects that can respond to the lookup message do the work
  • The implementation of slow forwarding ismethodSignatureForSelectorforwardInvocationThe combination provides finer – grained control by first returning method signatures toRuntimeAnd then letanInvocationTo send the message to the supplied object, and finally to theRuntimeThe results are extracted and passed to the original message sender.

We’re in the seventh installment of iOS Basics, which starts with app loading, exploring cold and hot launches, and how DyLD works.