The nature of a method call

In this article, we explore the nature of a method call by first looking at what the nature of a method call looks like by converting the method call code into c++ code. xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

[person test]; / / -- -- -- -- -- -- -- -- -- the underlying c + + code ((void (id, SEL)) (*) (void *) objc_msgSend) ((id) person, sel_registerName ("test"));
Copy the code

It can be seen from the above source code that method calls in c++ underlying code are actually converted into objc_msgSend function. OC method calls are also called message mechanism, which means to send messages to method callers.

Take the code above, which actually sends a test message to the Person instance object. Recipient: Person Message name: test

The process of method invocation can be divided into three phases.

Message sending phase: Is responsible for looking up methods from the cache list and method list of the class and its parent. Dynamic parsing phase: If no method is found in the message sending phase, it will enter the dynamic parsing phase, which is responsible for dynamically adding method implementation. Message forwarding phase: If the dynamic resolution method is also not implemented, the message forwarding phase is carried out and the message is forwarded to the recipient who can process the message for processing.

If message forwarding is also not implemented, an error is reported that the method couldn’t find the message, and the message is unrecognzied selector sent to instance

Next we explore how the three phases of the message sender are implemented through the source code.

Message is sent

The internal implementation of the _objc_msgSend function can be found in the objC-msG-arm64.s assembler file

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START

	cmp	x0, #0 // nil check and tagged pointer check
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
Copy the code

The value of the message recipient reveiver is first determined in the above compiled source code. If the receiver of the incoming message is nil, LNilOrTagged is executed. Inside LNilOrTagged, LReturnZero is executed. Inside LReturnZero, LReturnZero is directly return0.

If the incoming message recipient is not nILL, CacheLookup is executed, the method cache list is searched internally, and if it is found, CacheHit is executed to invoke the method. Otherwise, CheckMiss is executed, and __objc_msgSend_uncached is called internally.

__objc_msgSend_uncached does MethodTableLookup, which is a method list lookup, Internal core code __class_lookupMethodAndLoadCache3 MethodTableLookup _class_lookupMethodAndLoadCache3 is the c language function

The c language_class_lookupMethodAndLoadCache3Inside the function is the core source code for method lookup.

Let’s take a look at the process of _objc_msgSend in assembly language.

Method to find the core function is _class_lookupMethodAndLoadCache3 function, then analyzed _class_lookupMethodAndLoadCache3 function within the source code.

_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

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(); // Cache lookup, because cache is passed NO, there is NO cache lookup, because in assembly language CacheLookup has already been searchedif (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 is found, call imp directlydone, returns the method addressif (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 to cache_fill is described in detail above. log_and_fill_cache(cls, meth->imp, sel, inst, cls); // After the method is cached, fetch imp, calldoneImp imp = meth->imp; gotodone; {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) { // Haltif 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); / / executiondone, returns IMP Gotodone;
                }
                else{// Break out of the loop and stop the searchbreak; Method meth = getMethodNoSuper_nolock(curClass, sel);if(meth) {// Cache log_and_fill_cache(CLS, meth->imp, sel, inst, curClass); imp = meth->imp; / / executiondone, returns IMP Gotodone; }}} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- message phase completed -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- into the dynamic parsing stage -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / Finding no method implementation in any of the above lists, try parsing the methodif(resolver && ! triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); triedResolver = YES; goto retry; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- dynamic phase completed -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- into the forward stage -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst);done: runtimeLock.unlockRead(); // Return the method addressreturn 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; // CLS ->data() returns class_rw_tfor(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

In the source code above, the getMethodNoSuper_nolock function iterates through the list of methods to get method_list_t and finally finds the method through the search_method_list function

search_method_listfunction

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 ordered, use dichotomy to find methods to save timeif (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else{// Otherwise, traverse the list to findfor (auto& meth : *mlist) {
            if (meth.name == sel) return&meth; }}return nil;
}
Copy the code

findMethodInSortedMethodListThe principle of binary search within a function

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, it changes to count-1 over 2for(count = list->count; count ! = 0; Count >>= 1) {// probe = base + (count >> 1); Uintptr_t probeValue = (uintptr_t)probe->name;if(keyValue == probeValue) {// Retrieve probewhile(probe > first && keyValue == (uintptr_t)probe[-1].name) { probe--; } // return methodreturn(method_t *)probe; } // If keyValue > probeValueif(keyValue > probeValue) { base = probe + 1; count--; }}return nil;
}
Copy the code

At this point, the message sending phase is complete. Look at us through a stations drawing _class_lookupMethodAndLoadCache3 function inside the whole process of message is sent

If no method is found during the message sending phase, the dynamic resolution method phase is entered.

Dynamic resolution stage

When no method can be found in this class, including its parent cache and class_rw_t, the dynamic method parsing phase occurs. Let’s take a look at the dynamic parsing phase of the source code.

Dynamic parsing methods

    if(resolver && ! triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); // 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

Inside the _class_resolveMethod function, different operations are performed on class objects or metaclass objects

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

TriedResolver = YES; triedResolver = YES; The next time, instead of the dynamic parsing phase, the method is retried and looked up again. That is, whether we implement dynamic parsing or not, and whether it is successful or not, dynamic parsing will not be repeated after retry.

How do you parse methods dynamically

When object methods are dynamically resolved, the +(BOOL)resolveInstanceMethod (SEL) SEL method is called. When a class method is dynamically resolved, the +(BOOL)resolveClassMethod (SEL) SEL method is called.

Here is a look at the dynamic parsing process using an instance object as an example

@implementation Person
- (void) other {
    NSLog(@"%s", __func__); } + (BOOL)resolveInstanceMethod:(SEL) SEL {// dynamic add method implementationif (sel == @selector(testOtherMethod = class_getInstanceMethod(self, @selector(other)); // Dynamic addtestMethod implementation class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod)); // Returning YES indicates that there is a dynamic add methodreturn YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}

@end
Copy the code
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return0; } // Print the result // -[Person other]Copy the code

As you can see from the above code, person successfully called the Other method after dynamic parsing while calling the Test method.

The resolveInstanceMethod: method is automatically called when a method cannot be found in the cache or class_rw_t. So we can dynamically add methods inside the resolveInstanceMethod: method using class_addMethod.

Note here that class_addMethod 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 to say if the above code has been implemented -(void)test method, it will not add method dynamically, this can also be reflected in the above source code, because once found method implementation directly return IMP and call method, will not perform dynamic parsing method.

Class_addMethod function

Let’s take a look at what the arguments to class_addMethod represent.

/** first parameter: CLS: add method to which class second parameter: SEL name: add method name third parameter: IMP IMP: Method implementation, function entry, function name may be different from method name (same as method name is recommended) Types: method types (__unsafe_unretained Class CLS, SEL name, IMP IMP, const char *types)Copy the code

The above parameters have been explained in detail and will not be repeated here.

Notice how we get Method via class_getInstanceMethod in the code above

OtherMethod = class_getInstanceMethod(self, @selector(other));Copy the code

As mentioned above, method_T represents a Method structure, which contains SEL, type, and IMP. We customize the Method structure. Force objc_method to method_t to see if the method can be added dynamically.

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

- (void) other {
    NSLog(@"%s", __func__); } + (BOOL)resolveInstanceMethod:(SEL) SEL {// dynamic add method implementationif (sel == @selector(testStruct method_t * Method = (struct method_t *)class_getInstanceMethod(self, @selector(other)); NSLog(@"%s,%p,%s",method->sel,method->imp,method->types); // Dynamic addtestClass_addMethod (self, sel, method->imp, method->types); // Returning YES indicates that there is a dynamic add methodreturn YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}
Copy the code

Viewing print Content

Dynamic parsing [3246:1433553] Other, 0x100000D00,v16@0:8 Dynamic parsing [3246:1433553] -[Person other]Copy the code

You can see that you can actually print out the information, so we can understand that objC_Method has the same internal structure as method_t and can represent methods in the class definition.

Additionally, in the code above we get the IMP and type of the method using the method_getImplementation and method_getTypeEncoding functions. Of course, we can also write their own way to call, here to add parameters to the method as an example.

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)cook, "v@:@");
        return YES;
    }
    return[super resolveInstanceMethod:sel]; } void cook(id self,SEL _cmd,id Num) {// implement content NSLog(@"The %@ method of %@ is implemented dynamically with %@",self,NSStringFromSelector(_cmd),Num);
}
Copy the code

When the eat: method is called in the above code, the cook function is dynamically added as its implementation and an id parameter is added.

Dynamically resolve class methods

ResolveClassMethod (SEL) SEL (BOOL)resolveClassMethod (SEL) SEL is called when a class method is dynamically parsed

void other(id self, SEL _cmd)
{
    NSLog(@"other - %@ - %@", self, NSStringFromSelector(_cmd));
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {// The first argument is object_getClass(self), passing in a metaclass object. class_addMethod(object_getClass(self), sel, (IMP)other,"v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
Copy the code

As mentioned in the above source code analysis, whether or not we have implemented the dynamic parsing method, the system will execute retry to find the method again, so if we have implemented the dynamic parsing method, it will find the method smoothly, and then return to imp to call the method. If we don’t implement dynamic parsing methods. The message will be forwarded.

Next take a look at the dynamic resolution method flow diagram

forward

If we don’t parse the method dynamically ourselves, message forwarding happens

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

When it is unable to process the message itself, the message forwarding phase occurs and the _objc_msgForward_impcache function is called.

The __objc_msgForward_impcache function implementation can be found in the assembly by searching. The __objc_msgForward_impcache function calls __objc_msgForward to find __objc_forward_handler.

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : The '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
Copy the code

We found that this was simply the output of an error message. In fact, the message forwarding mechanism is not open source, but we can guess that it may take the returned object to call objc_msgSend, retrace the message sending, dynamic parsing, message forwarding process. Finally find a way to call it.

The Car class inherits from NSObject, and the Car has a – (void) driving method. When the Person instance loses the ability to drive and doesn’t learn to drive dynamically, it forwards the message to the Car. The Car instance object helps the Person object drive.

#import "Car.h"
@implementation Car
- (void) driving
{
    NSLog(@"car driving");
}
@end

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"@ implementation Person - (id) forwardingTargetForSelector aSelector: (SEL) {/ / return to process the message objectif (aSelector == @selector(driving)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        [person driving];
    }
    return0; } // Print the content // Message forward [3452:1639178] car drivingCopy the code

As you can see from the above code, this is called when the class has no implementation methods and no dynamically parsed methodsforwardingTargetForSelectorFunction for message forwarding, we can achieveforwardingTargetForSelectorFunction, within which the message is forwarded to an object that can implement the method.

If forwardingTargetForSelector function returns nil or not, will call methodSignatureForSelector method, used to return a method signature, this is also our correct method is the last chance to jump.

ifmethodSignatureForSelectorMethod returns the correct method signature is calledforwardInvocationMethod,forwardInvocationProvide one within the methodNSInvocationType parameter,NSInvocationEncapsulates a method call, including the caller, method name, and method parameters. inforwardInvocationModify the method call object within the function.

ifmethodSignatureForSelectorIf it returns nil, it goes todoseNotRecognizeSelector:Inside the method, the program crash prompts that the selector cannot be recognizedunrecognized selector sent to instance.

We verify this with the following code

- (id) forwardingTargetForSelector aSelector: (SEL) {/ / return to process the message objectif(aSelector = = @ the selector (driving)) {/ / it call methodSignatureForSelector method returns nilreturn nil; 
        // return [[Car alloc] init];
    }
    return[super forwardingTargetForSelector:aSelector]; } / / the method signature: return value type and parameter types - (NSMethodSignature *) methodSignatureForSelector aSelector: (SEL) {if (aSelector == @selector(driving)) {
       // return [NSMethodSignature signatureWithObjCTypes: "v@:"];
       // return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"]; / / can also by calling the Car methodSignatureForSelector method to get the method signature, this way to the Car object have aSelector methodreturn [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return[super methodSignatureForSelector:aSelector]; } //NSInvocation encapsulates a method call including: // Invocation invocation. Target invocation. Selector method name // [anInvocation Invocation getArgument: NULL atIndex: 0]; Gain parameters - (void) forwardInvocation: (anInvocation NSInvocation *) {/ / anInvocation encapsulates methodSignatureForSelector function returned by the method. The anInvocation. Target is still the Person object, so we need to change target to the method caller that can execute the method. // anInvocation.target = [[Car alloc] init]; // [anInvocation invoke]; [anInvocation invokeWithTarget: [[Car alloc] init]]; } // Print the content // Message forward [5781:2164454] car drivingCopy the code

You can see in the above code that the method is called normally. Let’s look at the flow chart of the message forwarding phase

NSInvocation

MethodSignatureForSelector method returned in the method signature, being packaged into NSInvocation object, in forwardInvocation NSInvocation provides access and modify the method name and methods of parameters and return values, that is to say, The final modification to the method can be made in the forwardInvocation function.

Similarly, we add the return value and parameters for the driving method and modify the return value and parameters for the method in the forwardInvocation method.

#import "Car.h"
@implementation Car
- (int) driving:(int)time
{
    NSLog(@"car driving %d",time);
    return time * 2;
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"@ implementation Person - (id) forwardingTargetForSelector aSelector: (SEL) {/ / return to process the message objectif (aSelector == @selector(driving)) {
        return nil;
    }
    return[super forwardingTargetForSelector:aSelector]; } / / the method signature: return value type and parameter types - (NSMethodSignature *) methodSignatureForSelector aSelector: (SEL) {if(aSelector == @selector(driving:)) {// add an int argument and an int return valuetypeFor I @ : Ireturn [NSMethodSignature signatureWithObjCTypes: "i@:i"];
    }
    return[super methodSignatureForSelector:aSelector]; } //NSInvocation encapsulates a method invocation, including the caller, method, and method parameters - (void)forwardInvocation:(NSInvocation *)anInvocation {int time; The method invocation has self and CMD by default, so the new parameter is 2 [anInvocation getArgument: &time atIndex: 2]; NSLog(@"Parameter value before modification = %d",time);
    time = time + 10; // time = 110
    NSLog(@"Parameter value before modification = %d",time); // Set method parameters at this time set the parameter to 110 [anInvocation]setArgument: &time atIndex:2]; [anInvocation invokeWithTarget: [[Car alloc] init]]; // Get the return value of the method int result; [anInvocation getReturnValue: &result]; NSLog(@"Get method return value = %d",result); // result = 220, the parameter is successfully modified result = 99; // Set the return value of the method to 99 [anInvocation]setReturnValue: &result]; // Get the return value of the method [anInvocation Invocation getReturnValue: &result]; NSLog(@"Change method return value = %d",result);    // result = 99
}

#import<Foundation/Foundation.h>
#import "Person.h"int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [[Person alloc] init]; // Pass in 100 and print the return value NSLog(@"[person driving: 100] = %d",[person driving: 100]);
    }
    return 0;
}
Copy the code
Message forwarding [6415:2290423] Values before modification = 100 Message forwarding [6415:2290423] Values before modification = 110 Message forwarding [6415:2290423] Car driving 110 Message forwarding [6415:2290423] Obtain method return value = 220 message forward [6415:2290423] Change method return value = 99 message forward [6415:2290423] [person driving: 100] = 99Copy the code

The forwardInvocation method can be used to modify the method parameters and return values.

Also, you can see that the method is called when the tagert is set to the Car instance and the return value is printed after the forwardInvocation method ends.

From this validation we know that once in the forwardInvocation method we have absolute control over the invocation, the invocation and the return value of the invocation parameters.

Class method for message forwarding

The class method message forwarding is the same as the object method, and the message forwarding mechanism will be carried out after the dynamic method resolution. We know that class methods are stored in metaclass objects, which are also special class objects. Note that the message recipient of a class method becomes a class object.

When the class object forwards a message, the corresponding + sign is calledForwardingTargetForSelector, methodSignatureForSelector, forwardInvocationMethod, note that the + sign method simply does not prompt, not that the system does not forward messages to class methods.

Let’s look at the message forwarding mechanism of a class method in a piece of code.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person driving];
    }
    return 0;
}

#import "Car.h"
@implementation Car
+ (void) driving;
{
    NSLog(@"car driving");
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"@ implementation Person + (id) forwardingTargetForSelector aSelector: (SEL) {/ / return to process the message objectif(aSelector == @selector(driving)) {// We need to return the class object herereturn [Car class]; 
    }
    return[super forwardingTargetForSelector:aSelector]; } // if the forwardInvocation returns nil then the following code is invoked: Return value type and parameter types + (NSMethodSignature *) methodSignatureForSelector aSelector: (SEL) {if (aSelector == @selector(driving)) {
        return [NSMethodSignature signatureWithObjCTypes: "v@:"];
    }
    return[super methodSignatureForSelector:aSelector]; } + (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation invokeWithTarget: [Car class]]; } // Print the result // Message forwarding [6935-2415131] car drivingCopy the code

Message forwarding for class object methods can also be done in the code above. It is important to note that the recipients of class methods are class objects. The other same object methods have the same message forwarding pattern.

conclusion

Method calls in OC are actually converted into calls to objc_msgSend, which sends a message (selector method name) to the receiver (method caller). The underlying implementation of the method invocation, objc_msgSend, is divided into three phases: message sending, dynamic method parsing, and message forwarding. This paper mainly explores the relationship between these three stages and the process. The above has been explained in detail, here is not repeated.

Underlying principles article column

Underlying principles article column


Welcome to point out any mistakes in the article. I am XX_CC, a long grown but not enough of a guy.