JSPatch分析(一):使用js调用oc方法

1,139 阅读6分钟

JSPatch链接 https://github.com/bang590/JSPatch

这篇文章的目标 : 通过js语法调用,来创建一个OC原生对象,并且调用这个对象的实例方法打印一行js传过来的值,代码如下。

(function(){
     require("TestObject");
     TestObject.alloc().init().test("---Test_JSPatch---");
 })();

执行效果

有js基础的可以直接跳转到 "正文"

js调试方式

js的调试比较简单,直接打开浏览器,调出开发者工具。可以直接在控制台写js代码如下图所示。

图1

首先明确一点:我们写的所有的js代码全部是符合js语法的,能够正常的被解析和执行,这是js和原生交互的根本基础.

js基本语法规则

  • this : 关键字,在不同的位置有不同的含义,假如这个关键字在最外面,就指的是一个全局的对象;假如是在函数,那么指的就是调用这个函数的对象。
  • Object : 关键字,表示的是js对象,类似于OC里面的NSDictionary。Array表示的是js的数组对象,类似于OC的NSArray.
  • function : 关键字, 定义一个js函数。一般有两种定义方式,如上图。
    1 : var func1 = function(){}
    2:  function func2(){}
    
  • 我们可以使用this关键字来创建一些全局的变量,也可以用来定义一些全局的函数,来让我们写的js语法符合规则。

  • Object.defineProperty 这个可以对象扩展一些额外的属性,有点类似于OC的Category,如果第一个参数传Object.prototype,表示为所有的Object对象添加扩展
var obj1 = {}     //定义一个obj1对象
obj1.name         //由于obj1没有name属性,访问就会输出undefined (其实直接写obj1.name = "syx"也是给obj1对象添加了一个那么属性,并且值为syx,但是这个地方我们主要是分析defineProperty这个函数的作用)
Object.defineProperty(obj1, "name", {value : "syx"});    //给obj1添加了一个那么属性,并且值为syx.
obj1.name        / / 由于上面已经给obj1添加了name属性,所以能够输出syx       
var obj2 = {}     //定义了一个obj2对象
obj2.name        //obj2没有name属性,同样输出undefined
Object.defineProperty(Object.prototype, "age", {value : "18"});   //给所有的Object对象都扩展了age属性
obj1.age    //18    obj1有age属性
obj2.age    //18    obj2也有age属性
  • (function(){})() : 这种写法表示这个function里面的代码是自执行的,就是说当js加载的时候,这个函数里面的代码会直接执行而不用在调用一次。

正文

js部分

TestObject.alloc().init().test("TestJPush");

1):要想这一部分代码不出错,首先必须要有一个全局的对象TestObject,这个也就是上面一句require的作用,require函数根据传过来的参数创建了一个全局对象,这个地方处理了一下为 TestObject = {"__clasName": "TestObject"}.

var global = this;   //这个赋值让global和this的效果一样,就是为了读起来更明白而已
global.require = function(clsName){     //定义一个全局的require函数,函数的实现为 = 右边,接受一个参数,创建一个全局的对象
    global[clsName] = {__clsName: clsName};
}

2):要想后面的这个TestObject.alloc不出错,那么这个对象就必须有一个属性叫alloc,我们使用Object.defineProperty给所有的对象扩展alloc方法。

Object.defineProperty(Object.prototype, "alloc", {value : function(){  
    //给所有的Object对象都扩展了一个alloc属性,属性的实现为一个function
    return __OC_callC(this.__clsName, "alloc");      //这个__OC_callC是使用JSContext注册的一个方法,用来交互的,可以先放一放
}});   

3):TestObject.alloc得到一个函数,函数后面加一个()表示调用这个函数,调用函数就会执行函数体里面的__OC_callC函数,函数是TestObject调用的,那么alloc函数体里面的this值的是TestObject这个对象,即 {__clsName: "TestObject"}. 4):后面的init和alloc方法是一样的实现,完整的js代码如下

var global = this;       
global.require = function(clsName){       //定义一个全局的require函数
    global[clsName] = {__clsName: clsName};
}
Object.defineProperty(Object.prototype, "alloc", {value : function(){   //添加扩展方法
    return __OC_callC(this.__clsName, "alloc")     //调用OC的方法,callC表示调用的类方法
}});
Object.defineProperty(Object.prototype, "init", {value : function(){
    return __OC_callI(this, "init", null);   //callI表示调用的是实例方法
}});
Object.defineProperty(Object.prototype, "test", {value : function(){
    return __OC_callI(this, "test", "Test_JSPatch")    //同样调用了一个实例方法,然后带了一个字符串参数
}});

(function(){
     require("TestObject");
     TestObject.alloc().init().test("TestJPush");
 })();

OC部分

可以对这个留个心眼:TestObject的代码为TestObject.alloc().init(),这个时候,OC调用了alloc方法之后生成了一个对象(OC的对象)返回到js,然后js接着调用init(),这个init里面的this对象其实是OC返回的对象,然后js通过init将这个对象传到了OC.

1:TestObject的代码如下。

@interface TestObject : NSObject
- (void)test:(NSString *)para;
@end
@implementation TestObject
- (void)test:(NSString *)para{
    NSLog(@"%@",para);
}
@end

2: js调用__OC_callC/__OC_callI 要想不出错,那么就要注册这两个方法,我们使用JSContext注册了这两个方法如下

self.context = [[JSContext alloc] init];
_context[@"__OC_callC"] = ^( NSString *clsName, NSString *selector){  //注册一个__OC_callC的方法,接受两个参数,一个就是类名(本例中是TestObject),里一个参数是selector(本例中是alloc)
    Class cls = NSClassFromString(clsName);
    SEL sel = NSSelectorFromString(selector);   
    void *result = invokeMethod(cls, sel, nil , false);  //通过cls和sel调用生成我们的TestObject对象,对象为result。
    return OCObjectToJsObject((__bridge id)result, clsName);  //对生成的OC对象做一个简单的包装,形如@{@"_obj":OCObject, clsName:"TestObject"}
};

_context[@"__OC_callI"] = ^(id obj, NSString *selector, id arg){  //调用实例方法,js里面传过来的第一个参数就是上面的__OC_callC返回去的
    TestObject *test = obj[@"_obj"];   //我们直接取到我们需要的对象
    if (arg != nil && ![arg isKindOfClass:NSNull.class]){   //第三个三叔是否为nil,test()调用是有一个参数的
        selector = [NSString stringWithFormat:@"%@:",selector];
    }
    SEL sel = NSSelectorFromString(selector);
    void *result = invokeMethod(test, sel, arg, true);   //执行方法
    if (result){
        return OCObjectToJsObject((__bridge id)result, NSStringFromClass(test.class));
    } 
    return OCObjectToJsObject(test, NSStringFromClass(test.class));   //返回结果
};

//invokeMethod的实现如下,就是通过cls和selector来调用执行方法,这里使用的是NSInvocation这种调用方式.performSelector只能带一个参数,多参数没发处理
void* invokeMethod(id cls ,SEL selector, NSString *arg, bool isInstace){
    NSMethodSignature *signature;
    if (isInstace){  //实例方法
        signature = [[cls class] instanceMethodSignatureForSelector:selector];
    }else{   //类方法
        signature = [cls methodSignatureForSelector:selector];
    }
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:cls];
    [invocation setSelector:selector];
    if (arg && ![arg isKindOfClass:NSNull.class]){   //调用的参数是从下标为2开始的,第0个是Target,第一个是selector
        [invocation setArgument:&arg atIndex:2];
    }
    [invocation invoke];
    const char *returnType = [signature methodReturnType];
    NSString *type = [NSString stringWithCString:returnType encoding:NSUTF8StringEncoding];
    if ([type isEqualToString:@"@"]){
        void *result;
        [invocation getReturnValue:&result];
        return result;
    }else{
        return (__bridge void *)(cls);
    }
}

//对Object对象做了一个简单的包装。
NSDictionary *OCObjectToJsObject(id object, NSString *clsName){
    return @{@"_obj":object, @"clsName":clsName};
}

3:整个OC的代码是很基础的,这里不做过多的说明,注释在代码里面都有

思考

1:我们在js里面为我们需要调用的方法形如alloc手动定义了一个全局的方法,假如我们的方法很多我们岂不是要注册很多的方法?

2:如果单纯是为了保证js调用不出错的话,那么能不能遍历TestObject对象的所有的方法,然后返回给js,来让TestObject.alloc()不出错,这样做看起来是可以的啊。这种方法的缺点原作者已经指明了,并且也给了具体的优化方案,被作者称为写这个库最爽的地方就是做了这个优化