·苹果在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保存到一个字典里面,字典是
总结一下整个过程
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
待续。。。