preface

JSPatch is a great hotfix framework, 10000+ Star !!!!! It was banned by Apple in 2017, but from the limited information I’ve been able to gather, it’s still being used with a sneaky mix-up and a new name. After all, bugs are inevitable, and with JSPatch we can save them in case they go wrong. Bango has actually introduced some of the implementation details, but after reading it, I still can’t get a general impression of the complete process, and there are certainly a lot of things worth digging and learning.

JSPatch profile

JSPatch is an iOS dynamic update framework that allows you to use JavaScript to invoke any native objective-C interface by simply introducing a minimal engine into your project, gaining the advantages of scripting languages: dynamically adding modules to your project, or dynamically fixing bugs by replacing the project’s native code.

I need to know

  • JavaScript– Still need to know a little bit of JS, I am the level of that kind of food, so the JS part is a little difficult to see. What you don’t understand is all by debugging the js code break point, according to the results of the reverse to understand.
  • In the JSPatchJavaScriptIn the writing. Or that sentence, after using there will be some problems, with the problem to read the source code more targeted, better effect.
  • JavaScriptCore– Is used for internal interaction between JS and OCJavaScriptCoreRecommended readingJavaScriptCore overview.
  • Runtime– JSPatch internally dynamically modifs the added methods using Runtime, which involves a lot of Runtime knowledge. So you still need to know something about Runtime. Recommended readingObjective-C Runtime.

Look at the source code with the problem

International practice: Two questions.

1. When adding methods, what is the method encoding

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
                
Copy the code

I know that adding a method to the Runtime is going to require us to pass in the method code. Apple’s runtime library internally utilizes type encoding to help speed message distribution. Method encoding mainly describes the return value and parameters of the method. -(void)test:(id)arg; The final encoding is V@ :@, and the corresponding relationship between each character is Type Encodings. But you might have a problem with the fact that there are only two arguments here with the return value, but there are four characters here. The reason is that [receiver test:arg] is converted by the compiler to objc_msgSend(Receiver, Selector, arg1) and the argument part of the signature we get is added a self and an SEL.

JS methods do not have such encoding, so how do you handle the encoding from JS to OC methods?

2. How to call OC in JS function

If a method does not exist on a JS object then the call will fail. For example, uiview.alloc () this method call will have a problem if the UIView object doesn’t have alloc.

Take a cursory look at the process

Many details have been simplified. The general flow of the code in the demo is as follows. The js side and native side are distinguished by two colors.

From this flowchart, we can see that the last method call of JSPatch is similar to the message forwarding of the Aspects library. JSPatch does not dynamically generate a method when the native method does not exist. So be careful when mixing Aspects with JSPatch.

Look back at the question

Question 1

Regarding the first method coding problem, JSPatch has several cases to deal with

  • The method in the agreement
  • Methods already implemented by native
  • Default case

In the general code logic, you can see that if you add a method that didn’t exist before, you need to know how many parameters to create your own method code. So the previous step in js is to generate a data structure like the number of method parameters, method implementation.

// If this method is already implementedif(class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {// overrideMethod is used to obtain the encoding inside the Method. // Method Method = class_getInstanceMethod(CLS,  selector); //typeDescription = (char *)method_getTypeEncoding(method); overrideMethod(currCls, selectorName, jsMethod, ! isInstance, NULL); }else{// protocol method BOOL overrided = NO;for (NSString *protocolName in protocols) {
                    char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
                    if(! types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);if(types) { overrideMethod(currCls, selectorName, jsMethod, ! isInstance, types); free(types); overrided = YES;break; }} // Defaultif(! overrided) {if(! [[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
                        NSMutableString *typeDescStr = [@"@ @." mutableCopy];
                        for (int i = 0; i < numberOfArg; i ++) {
                            [typeDescStr appendString:@"@"]; } overrideMethod(currCls, selectorName, jsMethod, ! isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]); }}}Copy the code

Question 2

Bango already has some introduction to this problem. Through the re, calls like uiview.alloc ().init() are replaced with uiview.__c (‘alloc’)().__c(‘init’)(). Walk through the uniform __c() meta-function, passing in the method name to get a method, passing in the arguments to call the method.

Implementation details

JSPatch is a bit too detailed. I’m just going to pick some of the main ones, and by default you’ve already seen the source code.

Start with the demo execution entry

    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
Copy the code

StartEngine is equivalent to the initialization runtime environment, and its internal JavaScriptCore defines many JS methods for external JS to interact with native.

+ (void)startEngine
{
.
.
.

    context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };

    context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
        return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
    };
    
    context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
        return callSelector(nil, selectorName, arguments, obj, isSuper);
    };
    context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
        return callSelector(className, selectorName, arguments, nil, NO);
    };
    context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {
        return formatJSToOC(obj);
    };
    
    context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {
        returnformatOCToJS([obj toObject]); }; .Copy the code

Js can call these methods directly and pass arguments, which are automatically converted here. The specific transformation correspondence.

   Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)
Copy the code

Above is the environment initialization, initialization is the js script call. [JPEngine evaluateScript:script]; The evaluateScript method’s main function is to change the regularization of the original JS method’s method calls to __c function calls. That’s the same thing as in problem 2 above.

+ (JSValue *)_evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL
{
    if(! script || ! [JSContext class]) { _exceptionBlock(@"script is nil");
        return nil;
    }
    [self startEngine];
    
    if(! _regex) { _regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil]; } NSString *formatedScript = [NSString stringWithFormat:@"; (function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];
    @try {
        if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
            return [_context evaluateScript:formatedScript withSourceURL:resourceURL];
        } else {
            return [_context evaluateScript:formatedScript];
        }
    }
    @catch (NSException *exception) {
        _exceptionBlock([NSString stringWithFormat:@"% @", exception]);
    }
    return nil;
}
Copy the code

Next, JavaScriptCore executes the JS script.

defineClass('JPViewController', {

  viewDidAppear:function(animate) {
    console.log('lol');
  },
            
  handleBtn: function(sender) {
    var tableViewCtrl = JPTableViewController.alloc().init()
    self.navigationController().pushViewController_animated(tableViewCtrl, YES)
  }
})
Copy the code
global.defineClass = function(declaration, properties, instMethods, clsMethods) {
    var newInstMethods = {}, newClsMethods = {}
    if(! (properties instanceof Array)) {clsMethods = instMethods instMethods = properties properties = null} You need to decide if it's implemented or notsetThe get method, if implemented, is added to the list of instance methodsif (properties) {
      properties.forEach(function(name){
        if(! instMethods[name]) { instMethods[name] = _propertiesGetFun(name); } var nameOfSet ="set"+ name. Substr (0, 1). The toUpperCase () + name. The substr (1);if(! instMethods[nameOfSet]) { instMethods[nameOfSet] = _propertiesSetFun(name); }}); Var realClsName = declaration.split();':')[0].trim()

    _formatDefineMethods(instMethods, newInstMethods, realClsName)
    _formatDefineMethods(clsMethods, newClsMethods, realClsName)

    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
    var className = ret['cls']
    var superCls = ret['superCls']

    _ocCls[className] = {
      instMethods: {},
      clsMethods: {},
    }

    if (superCls.length && _ocCls[superCls]) {
      for (var funcName in _ocCls[superCls]['instMethods']) {
        _ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
      }
      for (var funcName in _ocCls[superCls]['clsMethods']) {
        _ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
      }
    }

    _setupJSMethod(className, instMethods, 1, realClsName)
    _setupJSMethod(className, clsMethods, 0, realClsName)

    return require(className)
  }
Copy the code

We see that the defineClass method does some parsing inside and calls the _OC_defineClass method we registered earlier, passing in the class name, instance method, and class method. The defineClass method in the OC section is as follows

static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) { NSScanner *scanner = [NSScanner scannerWithString:classDeclaration]; NSString *className; NSString *superClassName; NSString *protocolNames; // xxObject: xxSuperObject <xxProtocol> // Parse class [scanner scanUpToString:@":" intoString:&className];
    if(! Scanner. IsAtEnd) {// parse the parent class scanner. ScanLocation = scanner. [scanner scanUpToString:@"<"intoString:&superClassName]; // Parse the protocolif(! scanner.isAtEnd) { scanner.scanLocation = scanner.scanLocation + 1; [scanner scanUpToString:@">"intoString:&protocolNames]; }} // If you have no information about the parent class, you inherit from NSObjectif(! superClassName) superClassName = @"NSObject"; ClassName = trim(className); superClassName = trim(superClassName); NSArray *protocols = [protocolNames Length]? [protocolNames componentsSeparatedByString:@","] : nil;
    
    Class cls = NSClassFromString(className);
    if(! CLS) {Class superCls = NSClassFromString(superClassName) if the current Class does not exist;if(! SuperCls) {// If the superclass does not have direct collapse _exceptionBlock([NSString stringWithFormat:@"can't find the super class %@", superClassName]);
            return@ {@"cls": className}; } CLS = objc_allocateClassPair(superCls, classname.utf8String, 0); objc_registerClassPair(cls); } // Add a protocol to this classif (protocols.count > 0) {
        for (NSString* protocolName inprotocols) { Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]); class_addProtocol (cls, protocol); } // Instance method and class method handle 1 is instance method and 2 is class methodfor(int i = 0; i < 2; i ++) { BOOL isInstance = i == 0; JSValue *jsMethods = isInstance? JSValue *jsMethods = isInstance? instanceMethods: classMethods; Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String); NSDictionary *methodDict = [jsMethods toDictionary];for (NSString *jsMethodName inmethodDict.allKeys) { JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName]; Int numberOfArg = [jsMethodArr[0] toInt32]; NSString *selectorName = convertJPSelectorString(jsMethodName); // For the sake of the js side, you can not write '_' at the end of the parameterif ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
                selectorName = [selectorName stringByAppendingString:@":"]; } JSValue *jsMethod = jsMethodArr[1]; // If this method is already implementedif(class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {// overrideMethod is used to obtain the encoding inside the Method. // Method Method = class_getInstanceMethod(CLS,  selector); //typeDescription = (char *)method_getTypeEncoding(method); overrideMethod(currCls, selectorName, jsMethod, ! isInstance, NULL); }else{// protocol method BOOL overrided = NO;for (NSString *protocolName in protocols) {
                    char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
                    if(! types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);if(types) { overrideMethod(currCls, selectorName, jsMethod, ! isInstance, types); free(types); overrided = YES;break; }} // Defaultif(! overrided) {if(! [[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
                        NSMutableString *typeDescStr = [@"@ @." mutableCopy];
                        for (int i = 0; i < numberOfArg; i ++) {
                            [typeDescStr appendString:@"@"]; } overrideMethod(currCls, selectorName, jsMethod, ! isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
                    }
                }
            }
        }
    }
    
    class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@ @ : @");
    class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");

    return@ {@"cls": className, @"superCls": superClassName};
}
Copy the code

We’re going to focus on the method part of the operation, and you can see that there are different cases where when you get the code for the method you’re going to overrideMethod. Let’s look at the main logic of this approach.

static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription) { SEL selector = NSSelectorFromString(selectorName); // Get the method signatureif (!typeDescription) {
        Method method = class_getInstanceMethod(cls, selector);
        typeDescription = (char *)method_getTypeEncoding(method); } // Get the original method IMP if the method implements IMP originalImp = class_respondsToSelector(CLS, selector)? class_getMethodImplementation(cls, selector) : NULL; IMP msgForwardIMP = _objc_msgForward;#if ! defined(__arm64__)
        if (typeDescription[0] == '{') {
            //In some cases that returns struct, we should use the '_stret' API:
            //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
            //NSMethodSignature knows the detail but has no API to return. we can only get the info from debugDescription. NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
            if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
                msgForwardIMP = (IMP)_objc_msgForward_stret;
            }
        }
    #endif// Replace the implementation of 'forwardInvocation:'if(class_getMethodImplementation(cls, @selector(forwardInvocation:)) ! = (IMP)JPForwardInvocation) { IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation,"v@:@");
        if (originalForwardImp) {
            class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@"); // Save the original IMP}} // Invocationreturn 0 when the return typeis double/float. [cls jp_fixMethodSignature]; // If this method is implemented, make a distinction between IMP that generates a new ORIGSelector -> original method. In order for JS to be able to call the previous methodif (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(! class_respondsToSelector(cls, originalSelector)) { class_addMethod(cls, originalSelector, originalImp,typeDescription);
        }
    }
    
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
    
    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function; // Replace the original selector at last, Preventing threading issus when // The selector gets called during the execution of 'overrideMethod' class_replaceMethod(cls, selector, msgForwardIMP,typeDescription);
}
Copy the code

This approach is similar to the core approach in Aspects. The main operation is to replace the implementation of the forwardInvocation, and replace the SEL of the method to be invoked pointing to _objc_msgForward. In other words, when the method is called, the JPForwardInvocation method is executed. I also save the JS method passed in and use it in the JPForwardInvocation method. The JPForwardInvocation is a bit long, and the main flow is shown below

The code is as follows, with some content removed.

Static void JPForwardInvocation(__unsafe_unretained ID assignSlf, SEL Selector, NSInvocation * Invocation) {// Call stack#ifdef DEBUG
    _JSLastCallStack = [NSThread callStackSymbols];
#endif
    
    BOOL deallocFlag = NO;
    id slf = assignSlf;
    // 方法签名
    NSMethodSignature *methodSignature = [invocation methodSignature];
    // 方法参数
    NSInteger numberOfArguments = [methodSignature numberOfArguments];
    // 方法名字
    NSString *selectorName = NSStringFromSelector(invocation.selector);
    // JS方法名字
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName]; // Get jsfunction
    JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
    if(! JsFunc) {/ / if there is no implementation of JS method is invoked the original message forwarding JPExecuteORIGForwardInvocation (SLF, the selector, invocation);return; NSMutableArray *argList = [[NSMutableArray alloc] init]; NSMutableArray *argList = [[NSMutableArray alloc] init];if([SLF class] == SLF) {// Class method [argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
    } else if ([selectorName isEqualToString:@"dealloc"]) {// dealloc method adds an assign self object [argList addObject:[JPBoxing boxAssignObj: SLF]]; deallocFlag = YES; }else{// Add a weak self object by default [argList addObject:[JPBoxing boxWeakObj: SLF]]; } // The signature of the method is self and 1 is _cmd.for (NSUInteger i = 2; i < numberOfArguments; i++) {
        const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
        switch(argumentType[0] == 'r'? ArgumentType [1] : argumentType[0]) {// Underlying data types#define JP_FWD_ARG_CASE(_typeChar, _type) \
            case _typeChar: {   \
                _type arg;  \
                [invocation getArgument:&arg atIndex:i];    \
                [argList addObject:@(arg)]; \
                break;  \
            }
            JP_FWD_ARG_CASE('c', char)
            JP_FWD_ARG_CASE('C', unsigned char)
            JP_FWD_ARG_CASE('s', short)
            JP_FWD_ARG_CASE('S', unsigned short)
            JP_FWD_ARG_CASE('i', int)
            JP_FWD_ARG_CASE('I', unsigned int)
            JP_FWD_ARG_CASE('l', long)
            JP_FWD_ARG_CASE('L', unsigned long)
            JP_FWD_ARG_CASE('q', long long)
            JP_FWD_ARG_CASE('Q', unsigned long long)
            JP_FWD_ARG_CASE('f'.float)
            JP_FWD_ARG_CASE('d', double)
            JP_FWD_ARG_CASE('B', BOOL) // objectcase The '@': {
                __unsafe_unretained id arg;
                [invocation getArgument:&arg atIndex:i];
                if ([arg isKindOfClass:NSClassFromString(@"NSBlock")]) {
                    [argList addObject:(arg ? [arg copy]: _nilObj)];
                } else {
                    [argList addObject:(arg ? arg: _nilObj)];
                }
                break;
            }
            
            // struct
            case '{': {
                NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
                
                #define JP_FWD_ARG_STRUCT(_type, _transFunc) \
                if ([typeString rangeOfString:@#_type].location ! = NSNotFound) { \
                    _type arg; \
                    [invocation getArgument:&arg atIndex:i];    \
                    [argList addObject:[JSValue _transFunc:arg inContext:_context]];  \
                    break; \
                }
                JP_FWD_ARG_STRUCT(CGRect, valueWithRect)
                JP_FWD_ARG_STRUCT(CGPoint, valueWithPoint)
                JP_FWD_ARG_STRUCT(CGSize, valueWithSize)
                JP_FWD_ARG_STRUCT(NSRange, valueWithRange)
                
                @synchronized (_context) {
                    NSDictionary *structDefine = _registeredStruct[typeString];
                    if (structDefine) {
                        size_t size = sizeOfStructTypes(structDefine[@"types"]);
                        if (size) {
                            void *ret = malloc(size);
                            [invocation getArgument:ret atIndex:i];
                            NSDictionary *dict = getDictOfStruct(ret, structDefine);
                            [argList addObject:[JSValue valueWithObject:dict inContext:_context]];
                            free(ret);
                            break; }}}break;
            }
            case ':': {
                SEL selector;
                [invocation getArgument:&selector atIndex:i];
                NSString *selectorName = NSStringFromSelector(selector);
                [argList addObject:(selectorName ? selectorName: _nilObj)];
                break; } / / a pointercase A '^':
            case The '*': {
                void *arg;
                [invocation getArgument:&arg atIndex:i];
                [argList addObject:[JPBoxing boxPointer:arg]];
                break; } / / classcase The '#': {
                Class arg;
                [invocation getArgument:&arg atIndex:i];
                [argList addObject:[JPBoxing boxClass:arg]];
                break;
            }
            default: {
                NSLog(@"error type %s", argumentType);
                break; }}} // Call the superclass methodif (_currInvokeSuperClsName[selectorName]) {
        Class cls = NSClassFromString(_currInvokeSuperClsName[selectorName]);
        NSString *tmpSelectorName = [[selectorName stringByReplacingOccurrencesOfString:@"_JPSUPER_" withString:@"_JP"] stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"_JP"];
        if(! _JSOverideMethods[cls][tmpSelectorName]) { NSString *ORIGSelectorName = [selectorName stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"ORIG"];
            [argList removeObjectAtIndex:0];
            id retObj = callSelector(_currInvokeSuperClsName[selectorName], ORIGSelectorName, [JSValue valueWithObject:argList inContext:_context], [JSValue valueWithObject:@{@"__obj": slf, @"__realClsName": @""} inContext:_context], NO);
            id __autoreleasing ret = formatJSToOC([JSValue valueWithObject:retObj inContext:_context]);
            [invocation setReturnValue:&ret];
            return; NSArray *params = _formatOCToJSList(argList); NSArray *params = _formatOCToJSList(argList); // Return encode char for datareturnType[255];
    strcpy(returnType, [methodSignature methodReturnType]); // Return double under 7.1float// Restore thereturn type
    if (strcmp(returnType, @encode(JPDouble)) == 0) {
        strcpy(returnType, @encode(double));
    }
    if (strcmp(returnType, @encode(JPFloat)) == 0) {
        strcpy(returnType, @encode(float)); } // Get jsFunc directly call '[jsFunc callWithArguments:params]' and then according toreturnChange Type to OC data switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
        #define JP_FWD_RET_CALL_JS \
            JSValue *jsval; \
            [_JSMethodForwardCallLock lock];   \
            jsval = [jsFunc callWithArguments:params]; \
            [_JSMethodForwardCallLock unlock]; \
            while(! [jsval isNull] && ! [jsval isUndefined] && [jsval hasProperty:@"__isPerformInOC"]) { \
                NSArray *args = nil;  \
                JSValue *cb = jsval[@"cb"]; \
                if ([jsval hasProperty:@"sel"]) { \ id callRet = callSelector(! [jsval[@"clsName"] isUndefined] ? [jsval[@"clsName"] toString] : nil, [jsval[@"sel"] toString], jsval[@"args"], ![jsval[@"obj"] isUndefined] ? jsval[@"obj"] : nil, NO);  \
                    args = @[[_context[@"_formatOCToJS"] callWithArguments:callRet ? @[callRet] : _formatOCToJSList(@[_nilObj])]]; \ } \ [_JSMethodForwardCallLock lock]; \ jsval = [cb callWithArguments:args]; \ [_JSMethodForwardCallLock unlock]; The \}#define JP_FWD_RET_CASE_RET(_typeChar, _type, _retCode) \
            case _typeChar : { \
                JP_FWD_RET_CALL_JS \
                _retCode \
                [invocation setReturnValue:&ret]; \break; The \}#define JP_FWD_RET_CASE(_typeChar, _type, _typeSelector) \
            JP_FWD_RET_CASE_RET(_typeChar, _type, _type ret = [[jsval toObject] _typeSelector];)   \

        #define JP_FWD_RET_CODE_ID \
            id __autoreleasing ret = formatJSToOC(jsval); \
            if (ret == _nilObj ||   \
                ([ret isKindOfClass:[NSNumber class]] && strcmp([ret objCType], "c") == 0 && ![ret boolValue])) ret = nil;  \

        #define JP_FWD_RET_CODE_POINTER \
            void *ret; \
            id obj = formatJSToOC(jsval); \
            if([obj isKindOfClass:[JPBoxing class]]) { \ ret = [((JPBoxing *)obj) unboxPointer]; The \}#define JP_FWD_RET_CODE_CLASS \
            Class ret;   \
            ret = formatJSToOC(jsval);


        #define JP_FWD_RET_CODE_SEL \
            SEL ret;   \
            id obj = formatJSToOC(jsval); \
            if ([obj isKindOfClass:[NSString class]]) { \
                ret = NSSelectorFromString(obj); \
            }

        JP_FWD_RET_CASE_RET(The '@', id, JP_FWD_RET_CODE_ID)
        JP_FWD_RET_CASE_RET(A '^', void*, JP_FWD_RET_CODE_POINTER)
        JP_FWD_RET_CASE_RET(The '*', void*, JP_FWD_RET_CODE_POINTER)
        JP_FWD_RET_CASE_RET(The '#', Class, JP_FWD_RET_CODE_CLASS)
        JP_FWD_RET_CASE_RET(':', SEL, JP_FWD_RET_CODE_SEL)

        JP_FWD_RET_CASE('c', char, charValue)
        JP_FWD_RET_CASE('C', unsigned char, unsignedCharValue)
        JP_FWD_RET_CASE('s', short, shortValue)
        JP_FWD_RET_CASE('S', unsigned short, unsignedShortValue)
        JP_FWD_RET_CASE('i', int, intValue)
        JP_FWD_RET_CASE('I', unsigned int, unsignedIntValue)
        JP_FWD_RET_CASE('l', long, longValue)
        JP_FWD_RET_CASE('L', unsigned long, unsignedLongValue)
        JP_FWD_RET_CASE('q', long long, longLongValue)
        JP_FWD_RET_CASE('Q', unsigned long long, unsignedLongLongValue)
        JP_FWD_RET_CASE('f'.float.floatValue)
        JP_FWD_RET_CASE('d', double, doubleValue)
        JP_FWD_RET_CASE('B', BOOL, boolValue)

        case 'v': {
            JP_FWD_RET_CALL_JS
            break;
        }
        
        case '{': {
            NSString *typeString = extractStructName([NSString stringWithUTF8String:returnType]);
            #define JP_FWD_RET_STRUCT(_type, _funcSuffix) \
            if ([typeString rangeOfString:@#_type].location ! = NSNotFound) { \
                JP_FWD_RET_CALL_JS \
                _type ret = [jsval _funcSuffix]; \
                [invocation setReturnValue:&ret]; \break;  \
            }
            JP_FWD_RET_STRUCT(CGRect, toRect)
            JP_FWD_RET_STRUCT(CGPoint, toPoint)
            JP_FWD_RET_STRUCT(CGSize, toSize)
            JP_FWD_RET_STRUCT(NSRange, toRange)
            
            @synchronized (_context) {
                NSDictionary *structDefine = _registeredStruct[typeString];
                if (structDefine) {
                    size_t size = sizeOfStructTypes(structDefine[@"types"]);
                    JP_FWD_RET_CALL_JS
                    void *ret = malloc(size);
                    NSDictionary *dict = formatJSToOC(jsval);
                    getStructDataWithDict(ret, dict, structDefine);
                    [invocation setReturnValue:ret]; free(ret); }}break;
        }
        default: {
            break; }}... }Copy the code

The above process is mainly to define a JS method and then some operations.

Let’s take a look at how native methods are called inside js methods. The main logic is to add a __c member to the base class of the JS object, so that all objects can call __c. There are also operations inside __c to determine whether it is an object, whether it is calling a method of the parent class, and so on. Finally, _methodFunc is called.

  var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
    var selectorName = methodName
    if(! isPerformSelector) { methodName = methodName.replace(/__/g,"-")
      selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
      var marchArr = selectorName.match(/:/g)
      var numOfArgs = marchArr ? marchArr.length : 0
      if (args.length > numOfArgs) {
        selectorName += ":"
      }
    }
    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)
    return _formatOCToJS(ret)
  }
Copy the code

Then we see that some judgment and parsing is done inside the method. Finally, the object method calls the _OC_callI class method which is registered in the native. Both of these methods go through the callSelector method. This method is also old. Its main flow chart is as follows.

The following code

static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper)
{
    NSString *realClsName = [[instance valueForProperty:@"__realClsName"] toString];
   
    if(instance) {// JSValue -> OC object instance = formatJSToOC(instance);if(class_isMetaClass(object_getClass(instance))) {className = NSStringFromClass(Class)instance); instance = nil; }else if(! The instance | | instance = = _nilObj | | [instance isKindOfClass: [JPBoxing class]]) {/ / null objectsreturn@ {@"__isNil": @(YES)}; } // Argument id argumentsObj = formatJSToOC(arguments);if (instance && [selectorName isEqualToString:@"toJS"]) {
        if ([instance isKindOfClass:[NSString class]] || [instance isKindOfClass:[NSDictionary class]] || [instance isKindOfClass:[NSArray class]] || [instance isKindOfClass:[NSDate class]]) {
            return_unboxOCObjectToJS(instance); }} Class CLS = instance? [instance class] : NSClassFromString(className); SEL selector = NSSelectorFromString(selectorName); NSString *superClassName = nil;if (isSuper) {
        NSString *superSelectorName = [NSString stringWithFormat:@"SUPER_%@", selectorName];
        SEL superSelector = NSSelectorFromString(superSelectorName);
        
        Class superCls;
        if (realClsName.length) {
            Class defineClass = NSClassFromString(realClsName);
            superCls = defineClass ? [defineClass superclass] : [cls superclass];
        } else {
            superCls = [cls superclass];
        }
        
        Method superMethod = class_getInstanceMethod(superCls, selector);
        IMP superIMP = method_getImplementation(superMethod);
        
        class_addMethod(cls, superSelector, superIMP, method_getTypeEncoding(superMethod));
        
        NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
        JSValue *overideFunction = _JSOverideMethods[superCls][JPSelectorName];
        if(overideFunction) { overrideMethod(cls, superSelectorName, overideFunction, NO, NULL); } selector = superSelector; superClassName = NSStringFromClass(superCls); } NSMutableArray *_markArray; // Method encoding NSInvocation *invocation; NSMethodSignature *methodSignature;if(! _JSMethodSignatureCache) { _JSMethodSignatureCache = [[NSMutableDictionary alloc]init]; }if (instance) {
        [_JSMethodSignatureLock lock];
        if(! _JSMethodSignatureCache[cls]) { _JSMethodSignatureCache[(id<NSCopying>)cls] = [[NSMutableDictionary alloc]init]; } methodSignature = _JSMethodSignatureCache[cls][selectorName];if(! methodSignature) { methodSignature = [cls instanceMethodSignatureForSelector:selector]; methodSignature = fixSignature(methodSignature); _JSMethodSignatureCache[cls][selectorName] = methodSignature; } [_JSMethodSignatureLock unlock];if(! methodSignature) { _exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for instance %@", selectorName, instance]);
            return nil;
        }
        invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
        [invocation setTarget:instance];
    } else {
        methodSignature = [cls methodSignatureForSelector:selector];
        methodSignature = fixSignature(methodSignature);
        if(! methodSignature) { _exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for class %@", selectorName, className]);
            return nil;
        }
        invocation= [NSInvocation invocationWithMethodSignature:methodSignature];
        [invocation setTarget:cls];
    }
    [invocation setSelector:selector];
    
    NSUInteger numberOfArguments = methodSignature.numberOfArguments;
    NSInteger inputArguments = [(NSArray *)argumentsObj count];
    if (inputArguments > numberOfArguments - 2) {
        // calling variable argument method, only support parameter type `id` and return type`id` id sender = instance ! = nil ? instance : cls; id result = invokeVariableParameterMethod(argumentsObj, methodSignature, sender, selector);returnformatOCToJS(result); } // Set the method parametersfor (NSUInteger i = 2; i < numberOfArguments; i++) {
        const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
        id valObj = argumentsObj[i-2];
        switch (argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
                
                #define JP_CALL_ARG_CASE(_typeString, _type, _selector) \
                case _typeString: {                              \
                    _type value = [valObj _selector];                     \
                    [invocation setArgument:&value atIndex:i]; \break; \
                }
                
                JP_CALL_ARG_CASE('c', char, charValue)
                JP_CALL_ARG_CASE('C', unsigned char, unsignedCharValue)
                JP_CALL_ARG_CASE('s', short, shortValue)
                JP_CALL_ARG_CASE('S', unsigned short, unsignedShortValue)
                JP_CALL_ARG_CASE('i', int, intValue)
                JP_CALL_ARG_CASE('I', unsigned int, unsignedIntValue)
                JP_CALL_ARG_CASE('l', long, longValue)
                JP_CALL_ARG_CASE('L', unsigned long, unsignedLongValue)
                JP_CALL_ARG_CASE('q', long long, longLongValue)
                JP_CALL_ARG_CASE('Q', unsigned long long, unsignedLongLongValue)
                JP_CALL_ARG_CASE('f'.float.floatValue)
                JP_CALL_ARG_CASE('d', double, doubleValue)
                JP_CALL_ARG_CASE('B', BOOL, boolValue)
                
            case ':': {
                SEL value = nil;
                if(valObj ! = _nilObj) { value = NSSelectorFromString(valObj); } [invocationsetArgument:&value atIndex:i];
                break;
            }
            case '{': {
                NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
                JSValue *val = arguments[i-2];
                #define JP_CALL_ARG_STRUCT(_type, _methodName) \
                if ([typeString rangeOfString:@#_type].location ! = NSNotFound) { \
                    _type value = [val _methodName];  \
                    [invocation setArgument:&value atIndex:i];  \
                    break; \
                }
                JP_CALL_ARG_STRUCT(CGRect, toRect)
                JP_CALL_ARG_STRUCT(CGPoint, toPoint)
                JP_CALL_ARG_STRUCT(CGSize, toSize)
                JP_CALL_ARG_STRUCT(NSRange, toRange)
                @synchronized (_context) {
                    NSDictionary *structDefine = _registeredStruct[typeString];
                    if (structDefine) {
                        size_t size = sizeOfStructTypes(structDefine[@"types"]);
                        void *ret = malloc(size);
                        getStructDataWithDict(ret, valObj, structDefine);
                        [invocation setArgument:ret atIndex:i];
                        free(ret);
                        break; }}break;
            }
            case The '*':
            case A '^': {
                if ([valObj isKindOfClass:[JPBoxing class]]) {
                    void *value = [((JPBoxing *)valObj) unboxPointer];
                    
                    if (argumentType[1] == The '@') {
                        if(! _TMPMemoryPool) { _TMPMemoryPool = [[NSMutableDictionary alloc] init]; }if(! _markArray) { _markArray = [[NSMutableArray alloc] init]; } memset(value, 0, sizeof(id)); [_markArray addObject:valObj]; } [invocationsetArgument:&value atIndex:i];
                    break; }}case The '#': {
                if ([valObj isKindOfClass:[JPBoxing class]]) {
                    Class value = [((JPBoxing *)valObj) unboxClass];
                    [invocation setArgument:&value atIndex:i];
                    break;
                }
            }
            default: {
                if (valObj == _nullObj) {
                    valObj = [NSNull null];
                    [invocation setArgument:&valObj atIndex:i];
                    break;
                }
                if (valObj == _nilObj ||
                    ([valObj isKindOfClass:[NSNumber class]] && strcmp([valObj objCType], "c") == 0 && ![valObj boolValue])) {
                    valObj = nil;
                    [invocation setArgument:&valObj atIndex:i];
                    break;
                }
                if ([(JSValue *)arguments[i-2] hasProperty:@"__isBlock"]) {
                    JSValue *blkJSVal = arguments[i-2];
                    Class JPBlockClass = NSClassFromString(@"JPBlock");
                    if(JPBlockClass && ! [blkJSVal[@"blockObj"] isUndefined]) {
                        __autoreleasing id cb = [JPBlockClass performSelector:@selector(blockWithBlockObj:) withObject:[blkJSVal[@"blockObj"] toObject]];
                        [invocation setArgument:&cb atIndex:i];
                        Block_release((__bridge void *)cb);
                    } else {
                        __autoreleasing id cb = genCallbackBlock(arguments[i-2]);
                        [invocation setArgument:&cb atIndex:i]; }}else {
                    [invocation setArgument:&valObj atIndex:i]; }}}}if (superClassName) _currInvokeSuperClsName[selectorName] = superClassName;
    [invocation invoke];
    if (superClassName) [_currInvokeSuperClsName removeObjectForKey:selectorName];
    if ([_markArray count] > 0) {
        for (JPBoxing *box in _markArray) {
            void *pointer = [box unboxPointer];
            id obj = *((__unsafe_unretained id *)pointer);
            if (obj) {
                @synchronized(_TMPMemoryPool) {
                    [_TMPMemoryPool setObject:obj forKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
                }
            }
        }
    }
    
    char returnType[255];
    strcpy(returnType, [methodSignature methodReturnType]);
    
    // Restore the return type
    if (strcmp(returnType, @encode(JPDouble)) == 0) {
        strcpy(returnType, @encode(double));
    }
    if (strcmp(returnType, @encode(JPFloat)) == 0) {
        strcpy(returnType, @encode(float)); } // Method return value processing IDreturnValue;
    if (strncmp(returnType, "v"1)! = 0) {if (strncmp(returnType, "@", 1) == 0) {
            void *result;
            [invocation getReturnValue:&result];
            
            //For performance, ignore the other methods prefix with alloc/new/copy/mutableCopy
            if ([selectorName isEqualToString:@"alloc"] || [selectorName isEqualToString:@"new"] ||
                [selectorName isEqualToString:@"copy"] || [selectorName isEqualToString:@"mutableCopy"]) {
                returnValue = (__bridge_transfer id)result;
            } else {
                returnValue = (__bridge id)result;
            }
            return formatOCToJS(returnValue);
            
        } else {
            switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
                    
                #define JP_CALL_RET_CASE(_typeString, _type) \
                case_typeString: { \ _type tempResultSet; \ [invocation getReturnValue:&tempResultSet]; \returnValue = @(tempResultSet); \
                    break; \
                }
                    
                JP_CALL_RET_CASE('c', char)
                JP_CALL_RET_CASE('C', unsigned char)
                JP_CALL_RET_CASE('s', short)
                JP_CALL_RET_CASE('S', unsigned short)
                JP_CALL_RET_CASE('i', int)
                JP_CALL_RET_CASE('I', unsigned int)
                JP_CALL_RET_CASE('l', long)
                JP_CALL_RET_CASE('L', unsigned long)
                JP_CALL_RET_CASE('q', long long)
                JP_CALL_RET_CASE('Q', unsigned long long)
                JP_CALL_RET_CASE('f'.float)
                JP_CALL_RET_CASE('d', double)
                JP_CALL_RET_CASE('B', BOOL)

                case '{': {
                    NSString *typeString = extractStructName([NSString stringWithUTF8String:returnType]);
                    #define JP_CALL_RET_STRUCT(_type, _methodName) \
                    if ([typeString rangeOfString:@#_type].location ! = NSNotFound) { \
                        _type result;   \
                        [invocation getReturnValue:&result];    \
                        return [JSValue _methodName:result inContext:_context];    \
                    }
                    JP_CALL_RET_STRUCT(CGRect, valueWithRect)
                    JP_CALL_RET_STRUCT(CGPoint, valueWithPoint)
                    JP_CALL_RET_STRUCT(CGSize, valueWithSize)
                    JP_CALL_RET_STRUCT(NSRange, valueWithRange)
                    @synchronized (_context) {
                        NSDictionary *structDefine = _registeredStruct[typeString];
                        if (structDefine) {
                            size_t size = sizeOfStructTypes(structDefine[@"types"]);
                            void *ret = malloc(size);
                            [invocation getReturnValue:ret];
                            NSDictionary *dict = getDictOfStruct(ret, structDefine);
                            free(ret);
                            returndict; }}break;
                }
                case The '*':
                case A '^': {
                    void *result;
                    [invocation getReturnValue:&result];
                    returnValue = formatOCToJS([JPBoxing boxPointer:result]);
                    if (strncmp(returnType, "^{CG", 4) == 0) {
                        if(! _pointersToRelease) { _pointersToRelease = [[NSMutableArray alloc] init]; } [_pointersToRelease addObject:[NSValue valueWithPointer:result]]; CFRetain(result); }break;
                }
                case The '#': {
                    Class result;
                    [invocation getReturnValue:&result];
                    returnValue = formatOCToJS([JPBoxing boxClass:result]);
                    break; }}return returnValue; }}return nil;
}
Copy the code

Ok, that’s where the basic process is. You can get a sense that a large portion of the code is cast. There are a lot of details that go into the conversion process, the mutable object handling and so on, and I’m not going to expand on that. You can study it if you are interested.

conclusion

  • The js/Native interaction isJavaScriptCore.
  • The substitution of adding methods is a straightforward way to swap methods. Methods that do not exist are not added.
  • Methods are called withNSInvocation.
  • The format of parameters and return values is handled by encoding.

The last

Read JSPatch source code, plus recently has been playing JSBox. Some of the urge to achieve a simple JSBox on your own, set a small goal, get ready to take a good look at JS soon, and give it a try.

Hydrology one, hope can help everybody a little bit.