JSPatch原理架构分析

2019-07-26  本文已影响0人  RasonWu

背景

为了快速认识整体框架,并且学习如何构思这个框架。

方法调用

我们希望在js实现这样的调用:

UIView.alloc().init()
  1. UIView哪里来?(require)
    我们要用UIView,那么当然就必须创建js对象。当然require不是唯一的方式,但是也是比较好理解的方式。
    var UIView = require('UIView');
    var _require = function(clsName) {
      if (!global[clsName]) {
        global[clsName] = {
          __clsName: clsName
        }
      }
      return global[clsName]
    }
  1. JS方法的调用
        __clsName: "UIView",
        alloc: function() {…},
        beginAnimations_context: function() {…},
        setAnimationsEnabled: function(){…},
        ...

这样的话,即使搞继承关系,也大倒难以接受。

    UIView.alloc().init()
        ->
        UIView.__c('alloc')().__c('init')()

这样所有对象都可以经过方法__c方法去调用。

        Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
        if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
        var self = this
        return function(){
            var args = Array.prototype.slice.call(arguments)
            return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
        }
        }})

_methodFunc() 就是把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。至此,内存消耗直降 99%

  1. 消息传递
    我们知道了_methodFunc方法,需要做桥接。那么就需要知道交互原理。
    JSContext *context = [[JSContext alloc] init];
    context[@"hello"] = ^(NSString *msg) {
        NSLog(@"hello %@", msg);
    };
    [_context evaluateScript:@"hello('word')"];
    

_methodFunc也是一样,只是参数多几个。

  1. 方法调用中的问题。
    类的方法调用,对象的方法调用。
  1. 类型转换
    至此,方法调用过程中的坑就差不多了。接下来是OC调起具体方法的坑了。没错,用的就是NSInvocation。这样必然会遇到类型转换的问题。
    5.1 基本思路
    通过defineClass任意替换一个类的方法。实际上这样需要OC替换原方法,并且新增了-ORIGViewDidLoad方法(例子)指向原来的方法。如果没有参数,当然就很简单,如果有参数的话,那么就会遇到转换的问题。
    5.2 va_list实现(32位)
    va_list结合对应的参数签名,获取精确的类型。arm64 下 va_list 的结构改变了,会有问题。具体不展开。
    5.3 ForwardInvocation实现(64位)
    使用这个是为了-methodSignatureForSelector:, -forwardInvocation:中NSInvocation 对象保存了所有参数值。而class_replaceMethod的实现变成了统一的实现方法,都是走_objc_msgForward,并且替换-forwardInvocation:,从而实现走转发机制的目的。这样轻松获取所有参数,但是有一个问题。如果真要用转发机制呢?
    5.4 真用转发机制
    如果JS有替换方法就用替换方法,没有的话,就用原来的。当然替换方法也可以调用原来的逻辑。
    5.5 转换细节
    假设有个方法是tool.hello(0.5),那么怎么转换?答案是JS到OC,会变成NSNumber,假设OC方法hello的参数其实是float,那么实际上就是[value floatValue],从而获取正确的参数,然后传到nsinvocation处理。其实以前自己写的框架是借助YYModel,会简单很多,少很多事儿。
  2. 那么新增方法呢?
    其实新增方法处理比较简单,原来有就替换,没有就新增。其它和替换方法的流程差不多。
  1. self关键字
defineClass("JPViewController: UIViewController", {
  viewDidLoad: function() {
    var view = self.view()
    ...
  },
}

JSPatch支持直接在defineClass里的实例方法里直接使用 self 关键字,跟OC一样 self 是指当前对象,这个 self 关键字是怎样实现的呢?实际上这个self是个全局变量,在 defineClass 里对实例方法进行了包装,在调用实例方法之前,会把全局变量 self 设为当前对象,调用完后设回空,就可以在执行实例方法的过程中使用 self 变量了。

  1. super关键字
    做法是调用 self.super()时,__c函数会做特殊处理,其实是加了个标识,返回新的对象。然后新增调用方法,传到OC的统一方法,再统一处理。
  2. 扩展
    require('JPEngine').defineStruct({
  "name": "JPDemoStruct",
  "types": "FldB",
  "keys": ["a", "b", "c", "d"]
    })
    /* 上面其实相当于下面这样的结构体,结构体是个连续的内存地址
    struct JPDemoStruct {
      CGFloat a;
      long b;
      double c;
      BOOL d;
    }
    */

js通过上面的定义,OC与js两边都保存一份配置表,就可以根据types的顺序去获取对应的变量参数了。

    for (int i = 0; i < types.count; i ++) {
  size_t size = sizeof(types[i]);  //types[i] 是 float double int 等类型
  void *val = malloc(size);
  memcpy(val, structData + position, size);
  position += size;
    }

从JS到OC也是类似的,只是先生成整个结构体大小的地址,然后再按上面说的顺序塞入相应位置。

  1. 关于Special Struct
    如果替换方法的返回值是某些 struct,_objc_msgForward的话,会crash。结论是非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。具体看作者的文档
  2. 关于内存问题
    • i.Double Release
      涉及OC对象和指针的转换,需要明确内存管理内容的。那么Double Release是怎么发生的呢?外面用id result作为变量,而getReturnValue的参数为void *。这就导致result在当前方法结束会自动添加release,而void *并不会对参数进行retain。根据内存管理的约定如果使用new的话,需要release,所以getReturnValue内部是new的调用就没问题。但是如果是其它方法,不需要release,而是由自动释放池管理。但实际上ARC又添加了释放的方法。所以就会造成两次释放问题。
    • ii.内存泄露
      根据内存管理的约定如果使用new的话,需要release。需要[invocation getReturnValue:&result];的内部。如果是用void *obj的方式做变量的话,就会没有释放。所以需要外面释放。当然如果用了id的方式做变量的话,会自动添加release就不会有问题的。但是对于非new等方式,那么实际上已经加入autorelease了,自动添加的release就会有问题,就是上面的二次释放问题。实际上getReturnValue方法内部,并不会对象所有权进行转移。
  3. 关于名字转换
    作者把js的两个下划线_当成OC的,用以转换,存在一定的问题,但一般不影响。
  4. 封箱
    为了处理,OC 返回到 JS 时 JavaScriptCore 把它们转成了 JS 的 Array / Object / String,和原对象的联系脱离了关系。
  5. nil的处理
    14.1 为了区分区分NSNull/nil,所以js搞了个特殊变量nsnull来表示NSNull,其它照常。
  6. 链式调用
    由于js的null会调用链式调用失效,所以最后是用false来代表nil,几乎完美的处理了所有问题。
上一篇 下一篇

猜你喜欢

热点阅读