JSPatch链接 https://github.com/bang590/JSPatch
这篇文章的目标 : 通过js语法调用,来创建一个OC原生对象,并且调用这个对象的实例方法打印一行js传过来的值,代码如下。
(function(){
require("TestObject");
TestObject.alloc().init().test("---Test_JSPatch---");
})();
执行效果
有js基础的可以直接跳转到 "正文"
js调试方式
js的调试比较简单,直接打开浏览器,调出开发者工具。可以直接在控制台写js代码如下图所示。
首先明确一点:我们写的所有的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()不出错,这样做看起来是可以的啊。这种方法的缺点原作者已经指明了,并且也给了具体的优化方案,被作者称为写这个库最爽的地方就是做了这个优化