iOS-Runtime message sending and forwarding mechanism

You can download the sample source code Demo on GitHub  , welcome to like and star, thank you!

 

Runtime message forwarding mechanism

1. Runtime ; official document Objective-C Runtime Programming Guide

Runtime is short for runtime, which makes OC language have dynamic characteristics.

The operating mechanism of Runtime enables OC to dynamically create classes and objects at runtime, perform message transmission and forwarding, etc.

run time, compile time difference


Compile time: In fact, when compiling, the compiler helps you translate the source code into machine code.

It is mainly to check and report the most basic language errors (lexical analysis, grammatical analysis, etc.). For example: If there are error and warning information when compiling the code, they are all checked by the compiler. This kind of error is called a compile-time error. This process is called compile-time type checking or static type checking.

Note: When compiling, the code is only scanned as text, and memory is not allocated to run

Runtime: It is the process in which the code runs and is loaded into memory (judgment in memory), which is a dynamic stage.

Runtime is a set of C language API (introduced <objc/runtime.h>or <objc/message.h>) at the bottom of OC, <objc/runtime.h> //Includes operations on classes, member variables, attributes, and methods;

<objc/message.h> //contains the message mechanism

The compiler will eventually convert the OC code into runtime code, and compile the .m file through the terminal command:

clang -rewrite-objc xxx.m

This command will report an error:

In file included from ViewController.m:8:
./ViewController.h:8:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
1 error generated.

 The 'UIKit/UIKit.h' file cannot be found, so we need to execute a new command

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxx.m

You can see the compiled xxx.cpp (C++ file), open it to view the code:

NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

We can see that the essence of the calling method is objc_msgSend to send a message . [[NSObject alloc]init]The statement sends a message twice, the first time an alloc message is sent, and the second time an init message is sent. 

We can also verify

1. To call objc_msgSend, you need to import the header file #import <objc/message.h>

2. TARGET → Build Settings → Search msg → Enable Strict Checking of objc_msgSend Calls from YES to NO

Turn off the strict checking mechanism. If Enable Strict Checking of objc_msgSend Calls is still YES, the parameter of objc_msgSend will report an error

Too many arguments to function call, expected 0, have 2

 
 Test code:

//obc_msgSend 方法
- (void)testObcMsgSend {
    Person *person = [Person new];
    [person eat];
    
    objc_msgSend(person,sel_registerName("eat"));
}

Same output; [person eat]; is equivalent to objc_msgSend(person, sel_registerName("eat")); 

2. The message sending and forwarding process can be summarized as:

Message sending (Messaging) is the process that Runtime quickly finds the IMP through the selector. With the function pointer, the corresponding method can be executed;

Message forwarding (Message Forwarding) is a slow channel that executes a series of forwarding processes after the failure to find the IMP. If no forwarding process is performed, logs will be displayed and exceptions will be thrown.

Message forwarding is divided into three steps, as follows:

1. Forwarding for the first time: dynamic method analysis Method resolution

After the object receives an undecipherable message, that is, a method that cannot be found, it will call the following two methods:

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); //类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); //实例方法

The specific writing method can refer to the following code;

In the runtime, the general user will import the objc/message.h file, which is used for message distribution, but the file loaded by the class at runtime is in the objc/runtime.h file

The Man class inherits from the Person class, and neither the Man class nor the Person class declares and implements the instance method drinkPear and the class method smoke

Man.m file

#import "Man.h"
#import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作
#import "Son.h"

@implementation Man

- (instancetype)init {
    self = [super init];
    if (self) {
//        [self performSelector:@selector(sel) withObject:nil];
    }

    return self;
}

#pragma mark - 第一次转发:方法解析  Method resolution
//实例方法IMP方法名
id dynamicInstanceMethodIMP(id self, SEL _cmd) {
    NSLog(@"第一次转发:方法解析----%s:实例方法",__FUNCTION__);
    return @"1";
}

//类方法IMP方法名
id dynamicClassMethodIMP(id self, SEL _cmd) {
    NSLog(@"第一次转发:方法解析----%s:类方法",__FUNCTION__);
    return @"2";
}

/*  class_addMethod方法
 class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
                 const char * _Nullable types)
 Class cls:添加新方法的那个类名(实例方法,传入CLass;类方法,传入MetaClss;可以这样理解,OC里的Class里的加号方法,相当于该类的MetalClas的实例方法,类调用类方法,和对象调用实例方法,其实底层实现都是一样的。类也是对象。)
 SEL name:要添加的方法名
 IMP imp:实现这个方法的函数
 const char *types:要添加的方法的返回值和参数;如:"v@:@":v:是添加方法无返回值     @表示是id(也就是要添加的类) :表示添加的方法类型   @表示:参数类型
 */

/**实例方法
 对象:在接受到无法解读的消息的时候 首先会调用所属类的类方法

 @param sel 传递进入的方法
 @return 如果YES则能接受消息 NO不能接受消息 进入第二步
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(drinkPear)) {
        //实例方法,传入CLass
        //对类进行对象方法 需要把方法添加进入类内
        class_addMethod([self class], sel, (IMP)(dynamicInstanceMethodIMP), "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

/**类方法
 类:如果是类方法的调用,首先会触发该类方法
 
 @param sel 传递进入的方法
 @return 如果YES则能接受消息 NO不能接受消息 进入第二步
 */
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(smoke)) {
        //对类进行添加类方法 需要将方法添加进入元类内;将[self class]或self.class替换为object_getClass/objc_getMetaClass
        //C语言函数写法:(IMP)(dynamicClassMethodIMP);类方法,Class cls传MetaClass
        class_addMethod(object_getClass(self)/*[self getMetaClassWithChildClass:self]*/,sel,(IMP)(dynamicClassMethodIMP), "@@:");
        //OC语言写法:class_getMethodImplementation([self class], @selector(findSmokeMethod))
//        class_addMethod(object_getClass(self),@selector(smoke),class_getMethodImplementation([self class], @selector(findSmokeMethod)),"@@:");
        
        //检测元类
        [self isMetaClass];
        
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/**
 判断是否是元类
 */
+ (void)isMetaClass {
    /**class_isMetaClass 方法
     通过 class_isMetaClass 方法可以验证判断是否是元类
     */
    Class c1 = object_getClass(self);
    Class c2 = [self getMetaClassWithChildClass:self];
    BOOL object_getClass = class_isMetaClass(c1);
    BOOL objc_getMetaClass = class_isMetaClass(c2);
    NSLog(@"object_getClass是否是元类:%@",object_getClass?@"YES":@"NO");
    NSLog(@"objc_getMetaClass是否是元类:%@",objc_getMetaClass?@"YES":@"NO");
}

/**
 获取类的元类

 @param childClass 目标类别
 @return 返回元类
 */
+ (Class)getMetaClassWithChildClass:(Class)childClass{
    //转换字符串类别
    const  char * classChar = [NSStringFromClass(childClass) UTF8String];
    //需要char的字符串 获取元类
    return objc_getMetaClass(classChar);
}

Separately implemented the class method resolveClassMethod reference (only the way of writing is different)

#import "SuperMan.h"
#import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作

//C语言函数
void eat(id self,SEL sel){
    NSLog(@"第一次转发:方法解析----类方法:eat");
}

@implementation SuperMan

+ (BOOL)resolveClassMethod:(SEL)sel {
    /**
     class: 给哪个类添加方法
     SEL: 添加哪个方法
     IMP: 方法实现 => 函数 => 函数入口 => 函数名
     type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
     */

//    Method exchangeM = class_getInstanceMethod([self class], @selector(eatWithPersonName:));
//    class_addMethod([self class], sel, class_getMethodImplementation(self, @selector(eatWithPersonName:)),method_getTypeEncoding(exchangeM));

    if (sel == NSSelectorFromString(@"eat")) {
        //C语言函数写法:(IMP)eat
        class_addMethod(self, @selector(eat), (IMP)eat, "v@:");
        return YES;
    } else if (sel == NSSelectorFromString(@"writeCode")) {
        NSLog(@"我在写代码");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

@end

The class_addMethod  method can refer to the article class_addMethod of iOS-Runtime to dynamically add methods to classes

There is one thing that needs special attention here. The class method needs to be added to the metaclass. All classes in OC are objects in essence. The isa of the object points to this class, the isa of the class points to the metaclass, and the isa of the metaclass points to the root element. Class, the isa of the root metaclass points to itself, so a closed loop is formed. The metaclass method and the method of judging whether it is a metaclass are as follows:

/**
 判断是否是元类
 */
+ (void)isMetaClass {
    /**class_isMetaClass 方法
     通过 class_isMetaClass 方法可以验证判断是否是元类
     */
    Class c1 = object_getClass(self);
    Class c2 = [self getMetaClassWithChildClass:self];
    BOOL object_getClass = class_isMetaClass(c1);
    BOOL objc_getMetaClass = class_isMetaClass(c2);
    NSLog(@"object_getClass是否是元类:%@",object_getClass?@"YES":@"NO");
    NSLog(@"objc_getMetaClass是否是元类:%@",objc_getMetaClass?@"YES":@"NO");
}

/**
 获取类的元类

 @param childClass 目标类别
 @return 返回元类
 */
+ (Class)getMetaClassWithChildClass:(Class)childClass{
    //转换字符串类别
    const  char * classChar = [NSStringFromClass(childClass) UTF8String];
    //需要char的字符串 获取元类
    return objc_getMetaClass(classChar);
}

Output result:

Create a Man object in MainViewController, and call the instance method drinkPear and class method smoke that do not exist in the Man class and Person class

Man *man = [[Man alloc]init];
//找不到实例方法
[man performSelector:@selector(drinkPear)];

//找不到类方法
[Man performSelector:@selector(smoke)];

The output is as follows:

 Of course, it is also possible for us to implement the instance method of the Man class in MainViewController + (BOOL)resolveInstanceMethod:(SEL)sel, but we need to change the Class class name in the class_addMethod method to [Man class], and the IMP imp parameter is changed to class_getMethodImplementation([MainViewController class], and implement the instance method of forwarding Man in MainViewController

#import "MainViewController.h"
#import "Man.h"
#import "SuperMan.h"
#import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作
//#import <objc/message.h> 包含消息机制

@interface MainViewController ()

@end

@implementation MainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //第一次转发:方法解析 Method resolution
    [self FirsForward];
}

#pragma mark - 第一次转发:方法解析 Method resolution
- (void)FirsForward {
    Man *man = [[Man alloc]init];
    //找不到实例方法
    [man performSelector:@selector(drinkPear)];

    //找不到类方法
    [Man performSelector:@selector(smoke)];
    
    //找不到类方法
    SuperMan *superMan = [[SuperMan alloc] init];
    SEL select = NSSelectorFromString(@"eat");
    [SuperMan resolveClassMethod:select];
    [superMan performSelector:@selector(eat)];
}

- (void)findDrinkPearMethod {
    NSLog(@"实例方法:Man drinkPear");
}

//实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([super resolveInstanceMethod:sel]) {
        return YES;
    }else {
        //IMP imp参数 OC写法:class_getMethodImplementation
        class_addMethod([Man class],@selector(drinkPear),class_getMethodImplementation([MainViewController class], @selector(findDrinkPearMethod)),"v@:");
        return YES;
    }
}

@end

Output result: At this time, the resolveInstanceMethod method in MainViewController will be executed, and the resolveInstanceMethod method in the Man class will not be executed. If the method that cannot be found is processed in MainViewController at this time, then the second and third forwarding will not go again up.

The instance method resolveInstanceMethod can be implemented in both MainViewController and Man, but the class method can only be implemented in the Man class

When Man receives an unknown drinkPear (instance method) and smoke (class method) message, the instance method will first call the resolveInstanceMethod: method above, and the class method calls the resolveClassMethod: method, and then class_addMethoddynamically adds a drinkPear (instance method) through the method ) and smoke (class method) to solve this unknown message, and the message forwarding process ends early.
But when Man does not receive the unknown message of drinkPear (instance method) and smoke (class method), the first step returns NO, that is, when there is no dynamic new implementation method, the second forwarding will be called

2. The second forwarding: fast forwarding (message redirection)

(The second and third stages later are all processed for objects, regardless of class methods)

If the implementation of the first forwarding method is not found, the following method will be called: - (id)forwardingTargetForSelector:(SEL)aSelector

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

In this step, transfer the message to other objects replacement receiver; change the  Selectorcalling object

At this time, the fast forwarding of messages begins. If we can find an object to implement the called study method, we can return the object in this method, and then the object will execute the study method we called. The code is as follows:

#pragma mark - 第二次转发:快速转发 Fast forwarding
- (void)SecondForward {
    Man *man = [[Man alloc]init];
    [man performSelector:@selector(study)];
}

Man.m file

#pragma mark - 第二次转发:快速转发 Fast forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"第二次转发:快速转发----forwardingTargetForSelector:  %@", NSStringFromSelector(aSelector));
    Son *son = [[Son alloc] init];
    if ([son respondsToSelector: aSelector]) {
        return son;
    }
    return [super forwardingTargetForSelector: aSelector];
}

At this time, the Son class will be found to call its study method

Son.h文件
@interface Son : NSObject

//学习
- (void)study;

@end


Son.m文件
@implementation Son

//学习
- (void)study {
    NSLog(@"我要好好学习,天天向上!");
}

@end

The output is as follows:

3. The third forwarding: Normal forwarding (message forwarding)

If the second forwarding does not find an object that can handle the method, then the following method will be called:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (void)doesNotRecognizeSelector:(SEL)aSelector;

Message forwarding also changes the calling object so that the message is called on the new object; the difference is that the forwardInvocationmethod has an NSInvocationobject, which stores all the information of the method call, including SELparameter and return value descriptions, etc., JSPatchwhich is implemented based on message forwarding

When the Man class receives a code message, it finds that the first two steps cannot be processed, so it goes to the third step:


At this time , the - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector method of the Man class will be called, and this method will return a method signature of code.

If the method signature of the code is returned, the
- (void)forwardInvocation:(NSInvocation *)anInvocation method of the Man class will be called, and the code method we call is processed in this method.

If it cannot be processed in this method,
the - (void)doesNotRecognizeSelector:(SEL)aSelector method will be executed, causing an unrecognized selector sent to instance abnormal crash.

#pragma mark - 第三次转发:常规转发 Normal forwarding
- (void)ThirdForward {
    Man *man = [[Man alloc]init];
    [man performSelector:@selector(code)];
    //三次转发都找不到的方法
    [man performSelector:@selector(missMethod)];
}

Man.m file

#pragma mark - 第三次转发:常规转发 Normal forwarding
//返回SEL方法的签名,返回的签名是根据方法的参数来封装的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"第三次转发:常规转发----method signature for selector: %@", NSStringFromSelector(aSelector));
    if (aSelector == @selector(code)) {
        return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//拿到方法签名,并且处理(创建备用对象响应传递进来等待响应的SEL)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation: %@", NSStringFromSelector([anInvocation selector]));
    if ([anInvocation selector] == @selector(code)) {
        //创建备用对象
        CodeMan *codeMan = [[CodeMan alloc] init];
        //备用对象响应传递进来等待响应的SEL
        [anInvocation invokeWithTarget:codeMan];
    }
}

// 如果备用对象不能响应 则抛出异常
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"doesNotRecognizeSelector: %@", NSStringFromSelector(aSelector));
    [super doesNotRecognizeSelector:aSelector];
}

The missMethod method can't handle it, so the doesNotRecognizeSelector method is taken, as follows

At this time, you will find the CodeMan class and call its code method

CodeMan.h
@interface CodeMan : NSObject

//编码
- (void)code;

@end


CodeMan.m
@implementation CodeMan

//编码
- (void)code {
    NSLog(@"我要学习编程!");
}

@end

The output is as follows:

At this point the code message has been processed by the codeMan instance

In each of the three forwarding phases, the recipient of the message has an opportunity to process the message. The later the stage, the higher the processing cost. The best situation is to process the message in the first stage, so that the runtime will cache the result after processing, and the next time the same message is sent, the processing efficiency can be improved. The recipient of the transfer message in the second stage is also less expensive than entering the forwarding process. If the last step is forwardInvocation, the complete NSInvocation object needs to be processed.

At this point, the message forwarding process is completely over, and the complete message forwarding process is as follows:

4. Practical use

Excerpted from the principle and practical use of iOS Runtime message forwarding mechanism

1. JSPatch --iOS dynamic update program

The specific implementation of bang god has been explained in detail in the following two blogs, and the message forwarding mechanism is used very delicately to interact with JS and OC, so as to realize the hot update of iOS. Although Apple's vigorous rectification and hot updates in 2017 made the approval rate of JSPatch unable to pass the review for a period of time, but after Bang God obfuscated the source code, it can basically pass the review. In any case, this dynamic solution is an advancement in technology, but it is currently being suppressed by Apple's father. However, if you use the official obfuscated version on the Bang God platform and don't mess around with it, the pass rate is still okay. Interested students can take a look at these two principle articles, and only the part used for message forwarding is extracted here.

http://blog.cnbang.net/tech/2808/
http://blog.cnbang.net/tech/2855/

The specific implementation principle can be viewed on Bang God's blog.

2. Implement methods for @dynamic

Using @synthesize can automatically generate getter and setter methods for @property (in the current Xcode version, it will be automatically generated), while @dynamic tells the compiler not to generate getter and setter methods. When using @dynamic, we can use the message forwarding mechanism to dynamically add getter and setter methods. Of course you can do it in other ways too.

3. Realize multiple agents

Using the message forwarding mechanism, multiple agents can be implemented without code intrusion, so that different objects can simultaneously represent the same callback, and then perform corresponding processing in their respective responsible areas, reducing the degree of code coupling.

https://blog.csdn.net/kingjxust/article/details/49559091

4. Indirect implementation of multiple inheritance

Objective-C itself does not support multiple inheritance. This is because the name lookup of the message mechanism occurs at runtime rather than at compile time. It is difficult to solve the ambiguity problem that may be caused by multiple base classes, but multiple base classes can be created internally through the message forwarding mechanism. Functional objects forward unrealizable functions to other objects, thus creating an illusion of multiple inheritance. Forwarding is similar to inheritance, and can be used to add some multi-inheritance effects to OC programming. An object forwards a message, as if it took over or "inherited" another object. Message forwarding makes up for the fact that objc does not support multiple inheritance, and also avoids a single class becoming bloated and complicated due to multiple inheritance.

You can download the sample source code Demo on GitHub  , welcome to like and star, thank you!

Reference article:

The principle and practical application of iOS Runtime message forwarding mechanism   

iOS understands the message forwarding mechanism in Objective-C with Demo

iOS message forwarding mechanism and solution to avoid crash

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/117793379