JSPatch原理架构分析

背景

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

方法调用

我们希望在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方法的调用
  • 方法的调用
    我们希望是这样的UIView.alloc(),这样必须需要先添加alloc方法。因为没有像OC那样的转发机制。那么js对象大致是这样的:
        __clsName: "UIView",
        alloc: function() {…},
        beginAnimations_context: function() {…},
        setAnimationsEnabled: function(){…},
        ...

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

  • 为了解决上面的问题
    我们能不能不暴露到对象直接调用?作者想到一个方法,就是再OC执行js前,通过正则表达式都调用一个统一的方式去处理。
    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. 方法调用中的问题。
    类的方法调用,对象的方法调用。
  • 类的方法调用相对简单。
    require('UIView')的时候就可以添加一个属性叫__isCls来表示类,这样直接通过反射就可以获取对象的UIView类。
  • 那么实例方法的调用呢?
    由于OC对象,肯定是OC返回的。作者使用了指针进行标识。但是由于需要在js里根据对象做区分,所以在OC将对象返回到js进行了转换,用来标识。
    static NSDictionary *_wrapObj(id obj) {
        return @{@"__obj": obj};
    }
    
    让 OC 对象作为这个 NSDictionary 的一个值,这样在 JS 里这个对象就变成:
    {__obj: [OC Object 对象指针]}
    
    那么在__c中判断如果有__obj,就取出并传回OC即可。
  • 会遇到内存管理的问题
    如果全局保存对象,那么会内存泄漏。如果不全局保存,可以预想到会发生野指针。作者的思路是JS持有,则引用计数+1,JS进行垃圾回收时释放。JS是有类似析构函数呢?还是有其它,还得进一步深挖。
  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. 那么新增方法呢?
    其实新增方法处理比较简单,原来有就替换,没有就新增。其它和替换方法的流程差不多。
  • 协议怎么处理?
    通过objc_getProtocolprotocol_copyMethodDescriptionList接口把 Protocol 对应的方法取出来,若匹配上,则按其方法的定义走方法替换的流程。否则新增协议,并添加到对应的类。
  • 增加的js变量实现
    JSPatch可以通过 -getProp:, -setProp:forKey: 这两个方法给对象动态添加成员变量。实现上用了运行时关联接口 objc_getAssociatedObject() 和 objc_setAssociatedObject() 模拟。
    本来OC有 class_addIvar() 可以为类添加成员,但必须在类注册之前添加完,注册完成后无法添加,这意味着可以为在JS新增的类添加成员,但不能为OC上已存在的类添加,所以只能用上述方法模拟。
    其实作者是这样说的,但是实际上,我的考虑是这样的,直接js获取就可以了,也没必要使用关联列表。其实不影响使用。
  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. 扩展
  • Struct 支持
    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也是类似的,只是先生成整个结构体大小的地址,然后再按上面说的顺序塞入相应位置。

  • C函数支持
    context[@"memcpy"] = ^(JSValue *des, JSValue *src, size_t n) {
        memcpy(des, src, n);
    };
    
    没错,只能这种形式,没法通过反射去调用。由于全部都扩展会大到影响性能,所以作者分成不同的模块,可以按需接入。
  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,几乎完美的处理了所有问题。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容