JSPatch分析(二):根据源码分析执行过程

975 阅读2分钟

阅读JSPatch的代码需要有javascript的知识,可以看我前一篇文章
JSPatch分析(一):使用js调用oc方法

目的

通过使用JSPatch执行js脚本,来调用OC的原生方法。

测试代码

js代码

(function(){
    var obj = require("TestJSPatch");
    var clsObj = obj.alloc();
    clsObj.init().testBlock(block("NSString *",  function(ctn){
        console.log(ctn);
    }));
})()

Objective-c代码

@interface TestJSPatch : NSObject
- (void)testBlock:(void(^)(NSString *))block;
@end

@implementation TestJSPatch
- (void)testBlock:(void (^)(NSString * _Nonnull))block{
    NSLog(@"---%s---", __func__);
    block(@"123");
}
@end

JSPatch 使用的正则表达式

由于JSPacth使用了正则表达式,正则表达式我一直都听过,但是没有系统性的去好好学习一下,心里一直都有一个疙瘩,借此机会好好的学习正则表达式。这一部分对JSPatch使用到的正则表达式做一个解读会正则表达式同学的可以跳过这一部分。

JSPatch使用的正则: (?<!\\)\.\s*(\w+)\s*\(

JSPatch处理的代码

处理前:
(function(){
    var obj = require("TestJSPatch");
    var clsObj = obj.alloc();
    clsObj.init().testBlock(block("NSString *",  function(ctn){
        console.log(ctn);
    }));
})()

处理后: 可以看到将我们写的.alloc()替换成了.__c("alloc")(),为什么这么替换提示是上一篇文章埋下的坑,后面会解释
;(function(){try{
(function(){
    var obj = require("TestJSPatch");
    var clsObj = obj.__c("alloc")();
    clsObj.__c("init")().__c("testBlock")(block("NSString *",  function(ctn){
        console.__c("log")(ctn);
    }));
})()

使用到的正则

  1. \s 表示的是任何空白字符,包括空格,制表符,换行符
  2. \w 表示的是任何字母,数字,下换线(_)
  3. * 表示的是任意数量(可以0个,1个,多个)
  4. + 表示的是至少一个
  5. 由于( 和 . 都是特殊字符,所有要表示其本身就需要转义, \. \c

6.() 这个可以用来表示分组,我举个例子

\w是匹配字母数字下划线,我下面直接简称为字母,我这个例子只有字母

expression: This is is my cat cat
reg: \w+\s  "+"表示至少匹配一个\w,很后面跟着一个空白
result: 
This    is      is     my      cat      cat   

reg: (\w+)\s(\w+)  匹配的是“字母空格字母”
result:This is      is my     cat cat 
这个地方的()的作用是将匹配的表达式“字母空格字母”分组了,前面一个空号是组1,后面一个是组2.不懂的话可接着往下看

reg:(\w+)\s(\w+)\s\1   这个\1就表示取前面()小括号的第一组,那么这个表达式的意思其实是"字母空格字母空格字母"这种形式,并且最后的那个字母是个开头的那个字母的值是一样的,形式如  a  b a  这种形式的匹配
result: 
is is is 

reg: (\w+)\s(\w+)\s\2 就是abb形式的,能够匹配到结果
This is is     my cat cat 
  1. (?: exp) 这个同样是分组,只是括号这个括号不缓存,还是用上面那个例子
expression: This is is my cat cat 
reg: (?:\w+)\s(\w+)\s\1   第一个括号不缓存,所以第一组从后面一个括号开始,这种也是abb形式的
result:
This is is      my cat cat

8 (?<!x)exp 表示匹配exp前面不是x的那些exp

x(?=y) -->  x后面是y       		(?<=y)x --> x前面是y 
x(?!y) -->  x后面不是y			   (?<!y)x --> x前面不是y 

正则的含义

(?<!\\)\.\s*(\w+)\s*\(

(?<!\\)\.   匹配的是"."这个符号,但是这个符号前面不是"\"   
\s*         匹配的是任意数量的空格
(\w+)       匹配的是任意的字母数字下划线,并且这个是第一组
\(          匹配的是小括号.

匹配到的是  .alloc(  这样的  

基本原理

使用JSContext让js和oc交互。

  1. 在oc里面,想调用一个对象的实例方法,无非就是:创建对象,调用方法,jsPatch只是将触发端放在了js端。
  2. js调用funcA, funcA是使用JSContext来注册的,那么我调用funcA的时候,就会进入到oc的原生环境。
  3. funcA里面将oc的类名和selector的名字传递给oc,那么oc有了class,有了selector就可以执行方法了
  4. 举个例子,要调用NSObject的alloc方法,那么js里面调用funcA("NSObject","alloc"),在JSContext里面收到了类名和selector,就可以使用NSInvocation调用了。

源码执行过程分析

js知识补充

我们要写的js代码要遵循js的规则,要保证运行不出错,有如下代码:

  1. 第一行报错,因为没有这个对象,和oc里面是一样,这个全局没有这个对象
  2. this["NSObject"] = {__className:"NSObject"} 这个话在全局注册了这个对象
  3. 因为我已经注册了,所以后面打印的时候不会报错.

JSPatch.js 部分

require函数
我们的代码: require("TestJSPatch") 

var _require = function(clsName) {
    if (!global[clsName]) {
      global[clsName] = {  // 这个就是将我们的TestJSPatch注册为全局对象,后面就直接能够取,
        __clsName: clsName
      }
    } 
    return global[clsName]  //返回了注册的对象,要用这个对象调用方法
  }

  global.require = function() {    //这个global是最顶部赋值的this,这个this是整个js的运行环境 
    var lastRequire
    for (var i = 0; i < arguments.length; i ++) {   //这个arguments是函数的参数,这个和oc不一样,参数可以从参数列表取,也能从arguments取,有点类似于oc方法的$0 $1,是内置的参数.因为这个方法可以直接注册多个全局对象,具体多少个也不知道,所以参数直接从arguments里面取
      arguments[i].split(',').forEach(function(clsName) {
        lastRequire = _require(clsName.trim())
      })
    }
    return lastRequire
  }
___c("alloc")
我们的代码 var obj = require("TestJSPatch");
          var clsObj = obj.__c("alloc");
          
JSPatch代码
    var _customMethods = {
    __c: function(methodName) {
      var slf = this   //这个this表示的是谁调用这个function,那么这个this就是谁
      
      if (slf instanceof Boolean) {    //判断调用者的类型是否是Boolean类型
        return function() {
          return false
        }
      }
      if (slf[methodName]) {           //取调用者的methodName的属性,这个methodName是参数,外面传过来的,我们的例子里面是"alloc"
        return slf[methodName].bind(slf);
      }

      if (!slf.__obj && !slf.__clsName) {  //判断__obj属性是否存在
        throw new Error(slf + '.' + methodName + ' is undefined')
      }
      if (slf.__isSuper && slf.__clsName) {
          slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName);
      }
      var clsName = slf.__clsName    //这个__clsName就是"TestJSPatch"
      if (clsName && _ocCls[clsName]) {
        var methodType = slf.__obj ? 'instMethods': 'clsMethods'
        if (_ocCls[clsName][methodType][methodName]) {
          slf.__isSuper = 0;
          return _ocCls[clsName][methodType][methodName].bind(slf)
        }
      }

      return function(){      //最后返回一个function
        var args = Array.prototype.slice.call(arguments)
        return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
      }
    }
  }
  
  //JSPatch引擎在加载的时候,这一段代码会自动执行,这一段代码会给所有的js对象都加上一个__c的属性。
  for (var method in _customMethods) {
    if (_customMethods.hasOwnProperty(method)) {
      Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})
    }
  }

同样obj.__c这句话执行不报错,那么obj就必有__c这个属性.上面的代码里面的注释已经说的很清楚,JSPatch引擎给所有的js对象都添加了__c的属性,并且返回一个函数,这样就能保证obj.__c这句话不报错。最后面的()就是执行这个函数。执行这个函数就会执行 _methodFunc 这个函数

_methodFunc
我们的代码 var obj = require("TestJSPatch");
          var clsObj = obj.__c("alloc")();   //执行这个方法
          
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
    var selectorName = methodName
    if (!isPerformSelector) {
      methodName = methodName.replace(/__/g, "-")
      selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
      var marchArr = selectorName.match(/:/g)
      var numOfArgs = marchArr ? marchArr.length : 0
      if (args.length > numOfArgs) {
        selectorName += ":"
      }
    }
    //调用方法
    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)
    return _formatOCToJS(ret)
  }
         

可以看到最终调用的是_OC_callI/_OC_callC.这两个方法就是使用JSContext注册的方法,参数就有我们传过去的clsName和selectorName。oc里面就能根据这两个参数生成对象,将结果返回.

OC部分

_OC_callI
注册了两个方法
context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
        return callSelector(nil, selectorName, arguments, obj, isSuper);
    };
    context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
        return callSelector(className, selectorName, arguments, nil, NO);
    };
callSelector

这里就不贴这个方法的代码,可以发现最使用NSInvocation调用方法,将值返回去了。

调用总结

1: [JPEngin startEngin]:执行的时候,会给所有的对象都注册一个__c的方法.值是一个函数. Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false}) }

2: var obj = require("TestJSPatch"): 在全局注册了一个TestJSPatch的变量.值是一个json,为{__clsName:"TestJSPatch"},这个obj={__clsName: "TestJSPatch"}

3:obj.alloc: 调用会转化成 obj.__c("alloc")的调动,就是 {__clsName:"TestJSPatch"}.__c("alloc")的调用 这个__c函数的参数就是alloc,里面的参数slf是函数的调用者{__clsName:"TestJSPatch"},返回一个函数。 function(){ var args = Array.prototype.slice.call(arguments) return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper) }

4:obj.alloc() --> obj.__c("alloc")() 后面的小括号表示对执行第二步返回的函数,开始执行函数。获取到函数的参数args,这个地方我们没有传递参数,这个函数执行函数 _methodFunc,参数是 slf.__obj = {} 这个参数是对象,表示是调用的对象方法还是实例方法, slf.__clsName="TestJSPatch", methodName="alloc", args=[], slf.__isSuper=false

5:_methodFunc 这个方法里面判断是对象方法还是实例方法,来决定调用 __OC_callI(intnstance, selectorName, args, __isSuper) 或者是 _OC_callC(clsName, selectorName, args)

6: 调用oc的NSInvocation,生成对象