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 <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSExportProtocol <JSExport>
- (void)text;
@end

@interface JSExportText : NSObject <JSExportProtocol>
- (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保存到一个字典里面,字典是<NString, NSDictionary>, 以类名为为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

待续。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 转载:原文链接 http://blog.cnbang.net/tech/2808/ JSPatch以小巧的体积做到...
    made_China阅读 390评论 0 0
  • JSPatch是一个可以在线修复bug的轻量级框架,项目中嵌入这个框架可以让你的app具有热更新的能力。你可以通过...
    daixunry阅读 5,974评论 5 38
  • 原创文章转载请注明出处,谢谢 相信HotFix大家应该都很熟悉了,今天主要对于最近调研的一些方案做一些总结。iOS...
    北辰明阅读 7,636评论 6 60
  • 本文由我们团队的 纠结伦 童鞋撰写。 写在前面 本篇文章是对我一次组内分享的整理,大部分图片都是直接从keynot...
    知识小集阅读 15,195评论 11 172
  • 写在前面 本篇文章是对我一次组内分享的整理,大部分图片都是直接从keynote上截图下来的,本来有很多炫酷动效的,...
    等开会阅读 14,359评论 6 69