JavaScriptCore 到 JSPatch , ReactNative

·苹果在iOS 7中推出了JavaScriptCore,主要目的是来解决OC与JS的交互。JavaScriptCore提供了JS的执行环境,可以通过这个环境来执行JS代码。JSPatch、ReactNative都是利用了这点来开发的。以来来讲解三者。

  • JavaScriptCore
  • JSPatch
  • ReactNative

JavaScriptCore

JS如同OC一样运行过程中会有自己的作用域,该作用域能获取所有作用域内变量和函数,JavaScriptCore提供了一个执行JS代码的“作用域”

JSContext,JSValue
OC call JS
JSContext *jsContext = [[JSContext alloc] init];
  //define var
  [jsContext evaluateScript:@"var jsCode = 2"];
  JSValue *jscode = jsContext[@"jsCode"];
  
  //define method
  [jsContext evaluateScript:@"function min(a,b) { if (a > b) {return b;} return a}"];
  
  //way 1
  [jsContext evaluateScript:@"min(5,4)"];
  //way 2
  JSValue *min = jsContext[@"min"];
  [min callWithArguments:@[@5,@4]];

OC通过创建"作用域"JSContext来搭建一个独立的JS执行环境,执行方法和变量定义代码,还可以通过下标的方式获取方法和变量,在调用方法时可以选择执行字符串语句("min(5,4)"),或者使用JSValue的call方法。

JS call OC

由于JS的代码始终需要OC创建的JSContext来执行,所以OC如果明确调用JS代码长相,就可以直接调用,反过来JS无法知道OC的代码具体长相,即使知道也无法明确调用,所以苹果使用了注册的方式,可以把OC的block 转化为一个JSValue,注册到JSContext中,然后JS代码可以获取并调用改block

    JSContext *jsContext = [[JSContext alloc] init];
    jsContext[@"ocMin"]= ^(int a, int b) {
        if (a > b) {
            return b;
        }
        return a;
    };
    [jsContext evaluateScript:@"ocMin(5, 4)"];
JSExport
@protocol JSExport
@end
#define JSExportAs(PropertyName, Selector) \
    @optional Selector __JS_EXPORT_AS__##PropertyName:(id)argument; @required Selector
#endif
#import 
#import 
@protocol JSExportProtocol 
- (void)text;
@end

@interface JSExportText : NSObject 
- (void)text;
@end

@implementation JSExportText
- (void)text
{
    NSLog(@"text");
}
@end

//test code
    JSContext *jsContext = [[JSContext alloc] init];
    //需要先将对象注册jsContext中,不然调用JSExportText是找不到的
   jsContext[@"JSExportText"] = [JSExportText class];
   [jsContext evaluateScript:@"var jsExportText = JSExportText.getInstance(); jsExportText.text()"];

JSExportProtocol 遵循了JSExport ,这样JavaScriptCore会自动配置这个类的方法和属性,使得JS调用可以获取到当前的方法。

JSPatch

jsPatch是目前iOS中用到最频繁的热更新方法,虽然纯Swift代码不支持。

过程

1.JS代码(定义了重写的方法和属性) -> 2.转化后的JS代码 -> 3.方法替换存表 ->4.方法调用转发查找
这四步就是JSPatch的整个过程,我们通过一个例子来说明整个过程

defineClass("JPTableViewController", {
  viewDidLoad: function() {
     self.ORIGviewDidLoad();
  },
})

以上是官方的例子,目的是重写JPTableViewController 的viewDidLoad ,以上代码,其实是调用了defineClass函数,第一个参数为类名称,第二个一个数组,为实例方法数组,第三个参数没有加,如果加上就是类方法数组
这种写法很明确,从上节中我们知道,JS需要context执行,但是上面代码中的defineClass这函数根本没有定义,如何执行?其实JSPatch在执行代码前做了很多工作,下面是JSPatch的初始化部分代码,中间省略了很多

[JPEngine startEngine]; //初始化工作
[JPEngine evaluateScript:string]; //执行

//初始化过程详情
+ (void)startEngine
{
    if (_context) {
        return;
    }
    
    JSContext *context = [[JSContext alloc] init];
    
    context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };
    
////省略部分代码
    context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
        NSLog(@"%@", exception);
        NSAssert(NO, @"js exception: %@", exception);
    };
    
    _context = context;
    
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"WatchDog" ofType:@"js"];
    NSAssert(path, @"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];
    [_context evaluateScript:jsCore];
}

从上面明显看出JSPatch 有一个context,来执行js代码,但是依然没有看到defineClass的定义,我们留意上端代码最后一个部分 [_context evaluateScript:jsCore]; 这个代码是执行的是 JSPatch.js中的代码,这里截取一段JSPatch.js的代码

var global = this
 global.defineClass = function(declaration, instMethods, clsMethods) {
    var newInstMethods = {}, newClsMethods = {}
    _formatDefineMethods(instMethods, newInstMethods)
    _formatDefineMethods(clsMethods, newClsMethods)

    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)

    return require(ret["cls"])
  }

ok,两端代码结合起来
defineclass -> _OC_defineClass -> defineClass( ) (oc内部的代码defineclass)
第二步如何将js方法定义为内部方法

static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{
    NSString *className;
    NSString *superClassName;
    NSString *protocolNames;
    
    NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];
    [scanner scanUpToString:@":" intoString:&className];
    if (!scanner.isAtEnd) {
        scanner.scanLocation = scanner.scanLocation + 1;
        [scanner scanUpToString:@"<" intoString:&superClassName];
        if (!scanner.isAtEnd) {
            scanner.scanLocation = scanner.scanLocation + 1;
            [scanner scanUpToString:@">" intoString:&protocolNames];
        }
    }
    NSArray *protocols = [protocolNames componentsSeparatedByString:@","];
    if (!superClassName) superClassName = @"NSObject";
    className = trim(className);
    superClassName = trim(superClassName);
    
    Class cls = NSClassFromString(className);
    if (!cls) {
        Class superCls = NSClassFromString(superClassName);
        cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
        objc_registerClassPair(cls);
    }
    
//下面代码比较重要
    for (int i = 0; i < 2; i ++) {
        BOOL isInstance = i == 0;
        JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
        
        Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
        NSDictionary *methodDict = [jsMethods toDictionary];
        for (NSString *jsMethodName in methodDict.allKeys) {
            if ([jsMethodName isEqualToString:@"__c"]) {
                continue;
            }
            JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
            int numberOfArg = [jsMethodArr[0] toInt32];
            NSString *tmpJSMethodName = [jsMethodName stringByReplacingOccurrencesOfString:@"__" withString:@"-"];
            NSString *selectorName = [tmpJSMethodName stringByReplacingOccurrencesOfString:@"_" withString:@":"];
            selectorName = [selectorName stringByReplacingOccurrencesOfString:@"-" withString:@"_"];
            
            if (!countArgRegex) {
                countArgRegex = [NSRegularExpression regularExpressionWithPattern:@":" options:NSRegularExpressionCaseInsensitive error:nil];
            }
            NSUInteger numberOfMatches = [countArgRegex numberOfMatchesInString:selectorName options:0 range:NSMakeRange(0, [selectorName length])];
            if (numberOfMatches < numberOfArg) {
                selectorName = [selectorName stringByAppendingString:@":"];
            }
            
            JSValue *jsMethod = jsMethodArr[1];
            if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
                overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
            } else {
                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);
                        overrided = YES;
                        break;
                    }
                }
                if (!overrided) {
                    NSMutableString *typeDescStr = [@"@@:" mutableCopy];
                    for (int i = 0; i < numberOfArg; i ++) {
                        [typeDescStr appendString:@"@"];
                    }
                    overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
                }
            }
        }
    }
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
    class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
#pragma clang diagnostic pop

    return @{@"cls": className};
}


\\实际替换过程
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
//准备过程
    SEL selector = NSSelectorFromString(selectorName);
    NSMethodSignature *methodSignature;
    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
//将原始方法替换为转发消息的方法
    IMP msgForwardIMP = _objc_msgForward;
    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);

//替换转发函数
    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
    }

//保存原来的方法
    if (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];
    SEL JPSelector = NSSelectorFromString(JPSelectorName);
    NSString *clsName = NSStringFromClass(cls);
//保存js的方法
    if (!_JSOverideMethods[clsName][JPSelectorName]) {
        _initJPOverideMethods(clsName);
        _JSOverideMethods[clsName][JPSelectorName] = function;
        class_addMethod(cls, JPSelector, msgForwardIMP, typeDescription);
    }
}

上面的简化了部分代码。这个过程如下
1.oc获取到类和要替换的方法,还有JS提供的jS方法 ,将oc原来的方法的selector替换为 _objc_msgForward ,这样所有的调用原来的方法都会调用_objc_msgForward,这个时候就会调用forwardInvocation
2.将forwardInvocation 替换为就是JPForwardInvocation,这样上一步的方法最终会调用 JPForwardInvocation
3.将原来的方法替换以ORIG开头的方法,这样依然能找到原来的方法
4.将实际JS的代码function保存到一个字典里面,字典是, 以类名为为key,值为一个字典,这个字典保存了以方法为key,值为js 的function类型,jSValue

总结一下整个过程
1.初始化过程
definclass(开发者自己写的js代码调用defineclass) -> definclass(转化三个参数,类型,方法,类方法)
-> _OC_defineClass(将js function 保存到一个全局字典,并且将原来方法保存,并且原来方法调用替换为方法转发)
2.执行过程
callMethod(OC中的方法) -> JPForwardInvocation(初始化参数,查找到 js function) -> JSfunction(执行)

整个过程,JSPatch利用了

  • OC预先给Context中预置了 _OC_defineClass,这样来接受了实际的替换过程
  • 其次直接使用JSValue 的 call 方法,来执行JS方法
    除了以上这些,JSPatch还做了很多转化过程,比如undefined变成nil。还有OC实例查找等等

ReactNative

待续。。。

你可能感兴趣的:(JavaScriptCore 到 JSPatch , ReactNative)