JSPatch实现分析

JSPatch十分小巧,能让APP具有热更新的能力。最早的一版1000行代码就搞定了,很值得学习探索。

前言

看JSPatch源码前,首先要明白编译型语言,解释性语言,脚本语言,动态语言,静态语言这几个概念。

  • 编译型语言
    这个最好理解,最早学的就是C/C++,由编译器把他编译成二进制文件,就可以直接运行,不需要再编译了,所以性能好。不过很多机器底层的CPU架构都不一样,所以不同的机器上面需要编译器重新编译,所以跨平台较差。

  • 编译-解释型语言
    计算机专业的话应该走的路都差不多,C/C++之后就开始学Java,而Java老师一般都会强调Java最大的优点-跨平台。
    其实Java就是先编译成了字节码文件,然后每个机器都装上JVM。之后运行的时候JVM就会把字节码文件解释成二进制文件并执行。因为多了个字节码解释的过程所以性能要差一点,不过每个平台不需要重新编译,所以跨平台性好。

  • 脚本语言
    正是脚本语言的兴起,所以类似编译解释这些概念的边界都开始模糊起来了。
    比如JavaScript,大学一般不会教脚本语言,这个多数是自学的。他无内存,无类型,不需要编译,只要给他解释器就能直接运行。

解释型语言应该是包含了编译-解释和脚本语言的。

而动态语言和静态语言的边界就更模糊了,随着语言不断地发展,好多语言加了很多动态的特性。
按传统的观念分:

  • 静态语言
    编译的时候就知道每一个变量的类型,因为类型错误而不能做的事情是语法错误。比如C/C++,Java

  • 动态类型
    编译的时候不知道每一个变量的类型,因为类型错误而不能做的事情是运行时错误。比如python,JavaScript

而Object -C应该还是静态语言,毕竟是C的超集嘛,他需要内存,需要类型,需要编译。
他的所有动态特性都是因为runtime,在运行时给他包了一个壳而已。

而JSPatch正是基于两点:

  • JavaScriptCore,可以理解为类似V8一样的js引擎,可以对js进行解析和提供执行环境。
  • oc的runtime,学习JSPatch可以更好地了解runtime的用法。

JSPatch流程分析

//上传的脚本
defineClass('JPViewController', {
  handleBtn: function(sender) {
    var tableViewCtrl = JPTableViewController.alloc().init()
    self.navigationController().pushViewController_animated(tableViewCtrl, YES)
  }
})

首先通过正则把他转换成:

 try{
    defineClass('JPViewController', {
    handleBtn: function(sender) {
        var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
        self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
        }
    })
}catch(e) {_OC_catch(e.message, e.stack)}

在js环境里执行defineClass方法,首先调用var _formatDefineMethod = function(methods, newMethods, isInst){}方法,获得实例方法和类方法数组,存放的function:

 declaration:"JPViewController"
 newInstMethods:Object {handleBtn: function () { … }
 newClsMethods:...

function() {
    var args =      _formatOCToJS(Array.prototype.slice.call(arguments))
    if (isInst) {
        global.self = args[0]
        args.splice(0,1)
    }
    var ret = _formatJSToOC(originMethod.apply(originMethod,   args))
    if (isInst) {
        global.self = null
    }
    return ret
}

之后调用oc环境里的static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods){}
这个方法里做的事情可就多了,遍历该类和需要重写的方法数组,调用static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod){}

1. 将替换的方法指向一个不可能实现的IMP:   
class_replaceMethod(cls, selector, class_getMethodImplementation(cls, @selector(__JPNONImplementSelector)), typeDescription);

2. 将系统的forwardInvocation方法指向重新实现的JPForwardInvocation(id slf, SEL selector, NSInvocation *invocation){},原来的实现则改为ORIGforwardInvocation:  
 IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, newForwardSelector, originalForwardImp, "v@:@");  

SEL newForwardSelector = @selector(ORIGforwardInvocation:);
    if (!class_respondsToSelector(cls, newForwardSelector)) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, newForwardSelector, originalForwardImp, "v@:@");
    }    

3. 将新的实现,也就是js传来的function保存到数组里:  
_JSOverideMethods[clsName][JPSelectorName] = function;  

4. 获得新添加方法的JPImplementation和typeDescription,新的JPImplementation就是从上面数组里取出function来执行:
   static _type JPMETHOD_IMPLEMENTATION_NAME(_typeString) (id slf, SEL selector) {    \
    NSString *selectorName = NSStringFromSelector(selector);    \
    NSString *clsName = NSStringFromClass([slf class]);\
    JSValue *ret = [_JSOverideMethods[clsName][selectorName] callWithArguments:_TMPInvocationArguments]; \
    _ret;    \
}   \
每个函数如果函数名相同,返回值不同,则会根据_typeString来区分。这边都是用的宏定义,避免了大量的重复函数。

5. 将新的实现加入类里:
class_addMethod(cls, JPSelector, JPImplementation, typeDescription);

之后,如果访问不存在的对象方法,会多层转发,层层调用对象的 -resolveInstanceMethod:>-forwardingTargetForSelector:>-methodSignatureForSelector:>-forwardInvocation,而forwardInvocation的实现则是static void JPForwardInvocation(id slf, SEL selector, NSInvocation *invocation) {}
通过NSMethodSignature包装,可以获得参数。如果不是需要替换的方法JPSelector,则分发给ORIGforwardInvocation执行走原来的流程。否则走JPSelector的实现JPImplementation,即_JSOverideMethods里的function。

JSPatch对象转换

流程分析好了,之后就是对象的转换。
var tableViewCtrl = JPTableViewController.alloc().init() -> JPTableViewController *tableViewCtrl = [[JPTableViewController alloc] init];

oc环境里,会传一个字典过去,告诉他是否是oc对象,而obj则是互传的对象本身。

static NSDictionary *toJSObj(id obj)
{
    if (!obj) return nil;
    return @{@"__isObj": @(YES), @"cls": NSStringFromClass([obj class]), @"obj": obj};
}

在js环境里,会把传来的字典转为一个JSClass实例

JSClass = function(obj, className, isSuper) {
          this.__obj = obj
          this.__isSuper = isSuper
          this.__clsName = className
      }    

var _formatOCToJS = function(obj) {
    if (typeof obj == "object" && obj.__isObj) {
        ....
        return new JSClass(meta["obj"], meta["cls"])
     }
    ......
}

最后只要把obj.__obj传给oc环境就可以了。

var _formatJSToOC = function(obj) {
     if (obj instanceof Object && obj.__obj) {
       return obj.__obj
     }
     .......
}

参考

JSPatch实现原理详解
JSPatch实现原理详解<二>
这是作者bang自己写的原理分析,告诉了实现过程中踩的坑。有两张图很好的诠释了精髓,一个是流程,一个对象的转换。

想法

oc的runtime我也懂,各种方法我也知道,JavaScript我也会写。可是我就是写不出JSPatch,想不到竟然还能这样搞。
这就跟以前学习一样,公式我记得,但是题目还是不会做。
所以还要多学习,要知其然,更要知其所以然,路还好长,差距还是好大。

作者:levi
comments powered by Disqus