1. About the start of OC method call
1.1 Preliminary preparation
1.1.1 Prepare classes and methods for testing
I prepared a class called LCHero in the project, with an object method called throwSkill, inherited from LCPerson. LCPerson has an object method attack and a class method defence, which LCPerson inherits from NSObject. I prepared a test method in a class of NSObject with the following code:
#import <Foundation/Foundation.h>
#import "LCHero.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCHero *hero = [LCHero new];
[hero throwSkill];
}
return 0;
}
/******************************************/
@interface LCPerson : NSObject
- (void)attack;
+ (void)defence;
- (void)revival;
@end
@implementation LCPerson
- (void)attack {
NSLog(@"%s --> Attack!",__func__);
}
+ (void)defence {
NSLog(@% s "| | - start defense! ",__func__);
}
@end
/******************************************/
@interface LCHero : LCPerson
- (void)throwSkill;
@end
@implementation LCHero
- (void)throwSkill {
NSLog(@"%s --> Unleash ultimate skill!",__func__);
}
@end
/******************************************/
@interface NSObject (test)
- (void)test;
@end
@implementation NSObject (test)
- (void)test {
NSLog(@"%s, test it!",__func__);
}
@end
Copy the code
Compile main.m to see the corresponding contents of the. CPP file
The following command is used:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk main.m
Copy the code
The corresponding CPP file we see is the c++ compilation of what we wrote in the main file
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = ((LCHero *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCHero"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)hero, sel_registerName("throwSkill"));
}
return 0;
}
Copy the code
If you remove the method type and cast, it looks like this. We found that the underlying call was an objc_msgSend method, which was converted to SEL.
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = (LCHero *)objc_msgSend(objc_getClass("LCHero"), sel_registerName("new"));
(void *)objc_msgSend(hero, sel_registerName("throwSkill"));
}
return 0;
}
Copy the code
2. First stop of the method journey –> objc_msgSend One-day tour
2.1 objc_msgSend first scenic spot –> objc_msgSend assembly process
Now that we know that the underlying object is objc_msgSend, let’s make a debug breakpoint before the method call, and when the breakpoint comes, let’s hold down the Control key + Step into to see what happens.
2.1.1 Obtaining classes through ISA
When we get to this point, we think that since methods and things are in classes, and classes and objects are linked by ISA, should we look for ISA? Then find the corresponding class through ISA, and the answer is yes.
2.1.2 Finding the cache cache_t
Once you find the class, start looking for the method cache in the class.
Slow to find
2.2 objc_msgSend –> Imp in C, C ++
2.2.1 _class_lookupMethodAndLoadCache3
Through the above attractions we saw _class_lookupMethodAndLoadCache3 method, source code is as follows: Found it tuned up a lower method lookUpImpOrForward directly, _class_lookupMethodAndLoadCache3 just intermediate links
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
Copy the code
2.2.2 lookUpImpOrForward
This method is a bit verbose and important, so I cut out the judgment, assertion, and assignment code, leaving out some key methods and comments. Let’s take a blind look. Let’s go through them one more time.
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
if(! // realizeClass(CLS); // realizeClass(CLS); }if(initialize && ! CLS ->isInitialized()) {initialize _class_initialize (_class_getNonMetaClass(CLS, inst)); } retry: // Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // Try this class'// Try superclass caches and method lists. // Find the method achesin a superclass. Cache it in this class.
// No implementation found. Try method resolver once.
// No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); return imp; }Copy the code
2.2.2.1 Preparation before method search –> Classes on the inheritance chain and classification must be prepared
cls->isRealized()
To determine if the class is loaded,realizeClass(cls)
This is a recursive operation and all classes on the inheritance chain are loaded. Parent class –> root class –> root metaclass, does not exit recursion until CLS is empty.- supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA())); The metaclass is always loaded recursively to facilitate method lookup.
- Concatenate the associated subclass and its parent class
methodizeClass(cls)
Classification method load so let’s see what they do, the main flow that I showed you in the code block, okay
First look at the comments, MY English is not very good but the general idea is understood, the method, protocol, property arrangement, Then put the classification of the outside also added - > < > feeling to see the beauty / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * methodizeClass * Fixes up cls's method list, protocol list, and property list. * Attaches any outstanding categories. * Locking: runtimeLock must be held by the caller **********************************************************************/ Let's break down the main elements of this method. Copy methods, attributes, and protocols from class ro to rW. Method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 2. Check whether it is the root metaclass. If it is the root metaclass, you need to load the method to make sure it is loaded before the class replacement. The following comment also says that if the root class calls a method that it doesn't have, it will look in the root metaclass. If my root metaclass has a related method, I have to add it to the method table of my class, and it can be found before the classification method is added. Why is it before the classification substitution? I can only hazard a guess here, method table design may be a similar stack table, if there are categories in the back after the addition of then I will find the IMP when the first classification of the IMP returned, // Root classes get Bonus method implementations if they don't 't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO); } 3. Add the classification / / Attach categories. Category_list * cats = unattachedCategoriesForClass (CLS,true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
Copy the code
2.2.2.2 Try cache_getImp(CLS, sel) again
We already looked in the cache, so why do we need to look in the cache? The reason has 2:
- May be multithreaded search, in case a thread has returned it can be directly called.
- Improve efficiency and call it as soon as possible to improve performance — after all, Apple’s dad wrote the code
2.2.2.3 If there is no cache, look in the method table of class and recursive parent class
Method meth = getMethodNoSuper_nolock(cls, sel); Log_and_fill_cache (CLS, meth->imp, sel, inst, CLS); Imp = meth->imp; gotodone
for(Class curClass = cls->superclass; curClass ! = nil; curClass = curClass->superclass) imp = cache_getImp(curClass, sel); Log_and_fill_cache (CLS, meth->imp, sel, inst, CLS); Imp = meth->imp; gotodone
Copy the code
2.2.2.4 No parent is found after traversing the class and recursing
If we look for the method table of the class and recursively look for the parent class but fail to find it, since the default value of the passed resolver is YES and the triedResolver is not reassigned or NO, we will go to the next stop: _class_resolveMethod. The source code is as follows:
// No implementation found. Try method resolver once.
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;
}
Copy the code
3. The second leg of the method journey –> Method resolver
After experiencing the class -> parent -> metaclass -> root metaclass ->… ->NSObject, classification, and so on after a series of lookups but not found, and then what? Our journey will continue! Apple dad is still very distressed us, give you a chance to deal with it, I will not directly let it collapse. So we see the scenery of the next station _class_resolveMethod(CLS, SEL, INST)
3.1 _class_resolveMethod
Let’s take a look at what’s in this method. The code is in the following block. The steps are analyzed below
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
3.1.1 If the currently passed class is not a metaclass –> object method
-
Why judge? Object methods are in classes, and class methods are in metaclasses
-
Comments in writing the try [CLS resolveInstanceMethod: sel]? –> There is no try in our class. Whether the system helped us implement this method, I don’t know, keep reading
-
Determine if the method is not implemented, and return if it is not. My own class does not have this method, if we do not implement it is the system for us to implement it?
-
This method is found in objc’s source code and returns NO by default.
-
Now let’s think about it, if I can get this method to work I have to find an IMP to go back to. If we override this method and add an IMP to the class, wouldn’t that solve the problem? We looked it up in the documentation and it proved our idea.
-
LookUpImpOrNil is raised whether we are handling it or not, and then goes back to lookUpImpOrForward because it is retry and the result is saved, Imp = (imp)_objc_msgForward_impcache imp = (imp)_objc_msgForward_impcache
-
If we search, it turns out that this guy is calling __objc_msgForward in assembly.
-
__objc_msgForward, which calls two similar methods, can be found in objc source code. LCHero: (void)revival: LCHero (void)revival: LCHero (void)revival: LCHero (void)revival: LCHero (void)revival: LCHero (void)revival: LCHero
3.1.2 If the currently passed class is a metaclass –> class method
Imp (resolveClassMethod) imp (resolveClassMethod) imp (resolveClassMethod) imp (resolveClassMethod) We wonder why we’re doing this.
- Class methods in the metaclass, we programmers generally can not operate directly metaclass, is very likely to find class methods.
- Whether you find it or not, once you look for the dynamic resolution of the class method
lookUpImpOrNil
Look it up again and if it still doesn’t work, it’ll go onceObject method resolution _class_resolveInstanceMethod
. - Which means that if you call a class method that doesn’t go all the way up the parent class’s inheritance chain up to NSObject, NSObject class we can’t change directly, but we can write a class extension method, and if we do a dynamic resolution of the corresponding class method in the NSObject class we can intercept that crash.
_class_resolveClassMethod(cls, sel, inst);
if(! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }Copy the code
But is this the end of it? Let’s see if there’s something else we haven’t seen about the crash stack, and obviously there is.
log_and_fill_cache
logMessageSend
-
Let’s be bold here, because according to the log_and_fill_cache flow, it has a method that prints the log, and it writes it to a file, and that file, according to its comments, gives us some clues about the method. Let’s use this switch a little bit more broadly in the project that we’re preparing what methods are called before and after the crash?
-
Run it again and check for a similar file in/TMP/msgtimeline
-
Open it up and look, OMG! Printed there are two we didn’t come out the process of an is forwardingTargetForSelector, another is methodSignatureForSelector. Suddenly found that we still want to stroll a lot of, we all want to see behind two stations are what scenery!!
4. The method is the third leg of the journey – > forwardingTargetForSelector
What does it mean to pass an object? Did I pass in an object when I called the method? Let’s see if the source code implements NSObject just like the last site without processing it, right?
- I don’t have it myself, I don’t have a superclass, I don’t have a metaclass. If you hand in an object that has this method, it is OK in principle.
- Let’s create a new class, LCEnemy, that inherits LCPerson and implements it
- (void)revival
@implementation LCEnemy
- (void)revival {
NSLog(@"%s --> Hahaha, Ben is alive again!",__func__);
}
@end
Copy the code
- And then LCHero rewrites
forwardingTargetForSelector
, returns a LCEnemy object.
-(id)forwardingTargetForSelector:(SEL)aSelector {
return [NSClassFromString(@"LCEnemy") alloc];
}
Copy the code
- Run it and see what happens. >< span style = “box-sizing: border-box! Important; word-wrap: break-word! Important;”
But there is a way I have not seen ah, the next stop we are looking forward to ah, nothing wrong –> just get on the bus to the next stop
4. The method is the third leg of the journey – > methodSignatureForSelector
After this process, it means:
- I don’t have my own way of dealing with it
- No one can handle the chain of inheritance
- There is also no dynamic method for resolution processing
- No one else can handle it
At this time I am not to send help on the net -> see which kind person can deal with. But always know what format you this method is, otherwise how can others know whether to deal with it? –> Method calls must be signed to match SEL to be called. What does the official document say
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- We’ll implement these two methods using the project we prepared before, just comment out the previous step.
/ / the method signature - (NSMethodSignature *) methodSignatureForSelector (SEL) aSelector {NSLog (@"%s",__func__);
if (aSelector == @selector(revival)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return[super methodSignatureForSelector:aSelector]; } // Message invocation - (void)forwardInvocation (NSInvocation *)anInvocation{NSLog(@)"%s",__func__);
NSLog(@"% @",anInvocation);
}
Copy the code
- The invocation to see the result, the forwardInvocation method went, but did not crash. –> means that if I send a method signature and no one handles the method call, the process ends, equivalent to an invalid method with no response.
5. DoesNotRecognizeSelector — the fourth stop of the journey of method
If we call the method without implementation, neither dynamic resolution, nor forward to other object processing at the same time also did not write help post 🙏 good samaritans to deal with this time Apple dad can not help you, have to end your method journey, give you a red return ticket doesNotRecognizeSelector –> familiar crash.
6. Write a review and reflection at the end
6.1 Process review of the method
Objc_msgSend assembly process - > quickly from the class cache lookup imp _class_lookupMethodAndLoadCache3 - > began to c, c + + slow find lookUpImpOrForward - > Imp _class_resolveMethod --> start checking for dynamic resolution. If you have to imp to lookUpImpOrForward forwardingTargetForSelector - > no processing, if there is to the other agent processing. MethodSignatureForSelector + forwardInvocation -- > if no agents, please follow the spec faking doesNotRecognizeSelector - > if do nothing, You're too lazy and Apple's father says God can't save you.Copy the code
6.2 Thinking about method journey
I’ve learned a few things from this methodological journey
- Crashes can be prevented and even collected. The original SDK for crash statistics is also based on this idea
- The componentization and routing that I had thought so magical before has been stripped of its mystery after this journey. It is not necessary to import the corresponding class header file to call a method
- Apple has a rich strategy for preventing method calls from crashing. –> Gives the developer three chances to save the process
- The matching method is called underneath the SEL, which is a numeric value rather than a string comparison. In addition, the method has a signature, and only SEL and signature match successfully will be considered as a method that can be called.
- In fact, we pass an ID (the object calling the method) every time the method is called, which is why we can easily get self in any method
- Some control printing can be very helpful for Bug analysis, such as
void instrumentObjcMessageSends(BOOL flag)
, extending the scope at the right time can follow clues about method calls.
Thank you for reading, and if you think it’s good enough, please give me a thumbs up. I’ll be more motivated to share something good with you. An update on class loading is planned next time. Interested in exchange learning can add my QQ:578200388