JSPatch原理解析(二)
由于执行了demo.js这个js文件,接下来就要转到这个文件中去查看它的调用过程,demo.js里面写了两个方法调用,第一个就是给那个实现了那个在oc没有实现的按钮点击事件,第二个就是添加了一个JPTableViewController类然后实现了相关的方法,我们先看是如何实现那个点击事件的
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
}
})
defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {...})
这里调用了defineClass这个匿名函数穿进去了2个参数,一个类名字符串'JPViewController',以及一个对象,对象里面有键值对handleBtn并对应了一个方法实现,注意这里的键和oc里面的未实现的方法名相同,defineClass是在JSPatch中定义的
defineClass
1.判断传进来的值
首先在JSPatch中给全局变量global添加了一个defineClass对应一个匿名函数,这个匿名函数干的第一件事就是判断传进来的参数,在参数全部填满的情况下应该是传进来一个declaration=>字符串(刚才调用时传进来的JPViewController),properties=>数组,instMethods(刚才传进来的包含handleBtn方法的对象)、clsMethods=>对象,向刚才这种调用的情况下只传了两个值,一个类名,一个类方法,而js是不支持方法重载的之后的同名方法会覆盖第一个,这里就用了一个聪明的方法,因为创建类,类名是肯定会传的但是可以没有变量于是一进来就判断一下传进来的properties到底是不是变量名(是不是字符串),如果像刚才传进来的是类方法的话其实传进来的第二个变量是一个对象,那么说明没有传进来变量名,那么就让instMethods,clsMethods都等于前一个传进来的变量然后把变量properties置为空就是实现了本来方法重载应该实现的功能
global.defineClass = function(declaration, properties, instMethods, clsMethods) {
var newInstMethods = {}, newClsMethods = {}
if (!(properties instanceof Array)) {
clsMethods = instMethods
instMethods = properties
properties = null
}
...}
2.给要生成的类设置变量
if (properties) {
properties.forEach(function(name){
if (!instMethods[name]) {
instMethods[name] = _propertiesGetFun(name);
}
var nameOfSet = "set"+ name.substr(0,1).toUpperCase() + name.substr(1);
if (!instMethods[nameOfSet]) {
instMethods[nameOfSet] = _propertiesSetFun(name);
}
});
}
首先遍历这个变量名数组,然后去传进来的类方法里面找有没有实现这个变量名对应的get方法和set方法,如果没有就给它生成一个,比如说我一开始传入了‘data’变量名,在这里这个变量名会在这里被处理成一个叫以setData为键,其方法实现为值的数据存在instMethods数组中,设置方法实现的这个过程要看_propertiesSetFun,_propertiesGetFun这两个方法一个是实现set方法一个是实现get方法,两个的实现差不多用_propertiesSetFun来讲一下过程,这个方法因为有传参所以比较有代表性
var _propertiesSetFun = function(name){
return function(jval){
var slf = this;
if (!slf.__ocProps) {
var props = _OC_getCustomProps(slf.__obj)
if (!props) {
props = {}
_OC_setCustomProps(slf.__obj, props)
}
slf.__ocProps = props;
}
slf.__ocProps[name] = jval;
};
}
根据上述代码可以得出其返回了一个匿名函数,放在刚才 instMethods[nameOfSet] = _propertiesSetFun(name);实际上就是给这个变量生成了一个set/get方法。
这个方法中的slf=this请参看基础篇中的this讲解,由于JSPatch对于变量的set和get方法和oc中是一致的,也就是说都是调用的本类的set方法比如self.setData,所以在这里实际指代的是调用set/get方法的类(JPViewController),而__ocProps则是用来存储改类变量的数组,所以这个方法做的事就是先判断该类有没有生成这个属性数组如果有就看看这个调用set或者get方法的属性有没有在oc中和这个类关联如果有就加到属性数组中没有就关联一下在添加到JS模拟oc类的属性数组中。这个方法中属性property关联oc类对象就是维护oc的类,而将属性添加到属性列表就是在维护js中的模拟oc类,通过这样的操作可以保持js中的模拟oc类和oc中的类保持一致。
_OC_setCustomProps传入的参数就是当前调用set方法的js对象,这个对象在_OC_setCustomProps中会通过formatJSToOC这个方法变为OC的对象并把属性值关联到这个对象上,之后通过 slf.__ocProps[name] = jval;给该对象的属性数组中添加一个name属性并赋值。
举例:
defineClass('testViewController',['objctA'],...,...)
在这个例子中slf就是testViewController对象,该对象有一个属性列表__ocProps,_propertiesSetFun的作用就是
3.获取到真实的类名
因为传进来的有可能是例如‘JPTableViewController : UITableViewController <UIAlertViewDelegate>’所以要按冒号分割取第一个值,trim的作用是去除左右空格
var realClsName = declaration.split(':')[0].trim()
4.格式化方法
_formatDefineMethods(instMethods, newInstMethods, realClsName)
_formatDefineMethods(clsMethods, newClsMethods, realClsName)
var _formatDefineMethods = function(methods, newMethods, realClsName) {
for (var methodName in methods) {
if (!(methods[methodName] instanceof Function)) return;
(function(){
var originMethod = methods[methodName]
newMethods[methodName] = [originMethod.length, function() {
try {
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
global.self = args[0]
if (global.self) global.self.__realClsName = realClsName
args.splice(0,1)
var ret = originMethod.apply(originMethod, args)
global.self = lastSelf
return ret
} catch(e) {
_OC_catch(e.message, e.stack)
}
}]
})()
}
}
这个方法针对你写在JS文件中的模拟OCclass中的方法进行格式化,让你每个方法实现变成一个数组,数组中第一个成员是originMethod.length也就是该方法传进来的参数的数量,第二个成员是这个匿名函数,这个匿名函数接受这个方法在运行时传进来的参数(注意是运行时传进来的,你在定义方法时显式声明的入参没有作用,显式声明多少都没用,最后传进来的是实际运行时传进来的值),而在运行时调用的时候传进任何一个方法中的参数都比实际多一个,这个参数就是调用者本身,比如出了handleBtn这个点击事件,它的arguments数组中第一个对象是JPViewController自身,第二个对象是传进来的sender也就是UIButton,所以在把这些参数交给js方法来执行前需要传进去实际需要的参数,调用者本身就不需要了,所以有了 args.splice(0,1)这句话的意思是从下标0开始删除1个数组元素也就是删掉了调用者本身,同时这里设定了self的值为调用者本身
5.把参数交给OC的运行时去动态生成这个class
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
var className = ret['cls']
var superCls = ret['superCls']
这一部分js做的事情很少,就只是把创建类所需要的参数交给oc来处理,然后oc会返回一个字典,第一项是该类的类名,第二项是它的父类的类名,这两项在后面创建对应的oc模拟类的时候会用到,接下来看一下_OC_defineClass具体做了什么
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{
NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];
NSString *className;
NSString *superClassName;
NSString *protocolNames;
[scanner scanUpToString:@":" intoString:&className];
if (!scanner.isAtEnd) {
scanner.scanLocation = scanner.scanLocation + 1;
[scanner scanUpToString:@"<" intoString:&superClassName];
if (!scanner.isAtEnd) {
scanner.scanLocation = scanner.scanLocation + 1;
[scanner scanUpToString:@">" intoString:&protocolNames];
}
}
if (!superClassName) superClassName = @"NSObject";
className = trim(className);
superClassName = trim(superClassName);
NSArray *protocols = [protocolNames length] ? [protocolNames componentsSeparatedByString:@","] : nil;
Class cls = NSClassFromString(className);
if (!cls) {
Class superCls = NSClassFromString(superClassName);
if (!superCls) {
_exceptionBlock([NSString stringWithFormat:@"can't find the super class %@", superClassName]);
return @{@"cls": className};
}
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
}
if (protocols.count > 0) {
for (NSString* protocolName in protocols) {
Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
class_addProtocol (cls, protocol);
}
}
这一大段只做了一件事就是把类名,继承的类名,协议从声明字符串里面取出来然后判断一下该类存不存在不存在就要生成一个,生成前先看一下父类存不存在不存在就报错,然后添加协议
for (int i = 0; i < 2; i ++) {
BOOL isInstance = i == 0;
JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
NSDictionary *methodDict = [jsMethods toDictionary];
for (NSString *jsMethodName in methodDict.allKeys) {
JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
int numberOfArg = [jsMethodArr[0] toInt32];
NSString *selectorName = convertJPSelectorString(jsMethodName);
if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
selectorName = [selectorName stringByAppendingString:@":"];
}
JSValue *jsMethod = jsMethodArr[1];
if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
} else {
BOOL overrided = NO;
for (NSString *protocolName in protocols) {
char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
if (types) {
overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);
free(types);
overrided = YES;
break;
}
}
if (!overrided) {
if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
NSMutableString *typeDescStr = [@"@@:" mutableCopy];
for (int i = 0; i < numberOfArg; i ++) {
[typeDescStr appendString:@"@"];
}
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
}
}
}
这一段也很简单,就是给生成的类分别添加类方法和实例方法,其中做了一个判断,判断一下当前类是不是已经有了这个方法,如果是的话就复写这个方法
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
最后这点就是添加了两个方法,这两个方法用于在生成类以后动态的给该类添加属性,比如说你想在handleBtn里面声明一个类的属性你就要用这两个方法进行存取
6.最后一件事是生成该类对应的JS模拟OC类
var className = ret['cls']
var superCls = ret['superCls']
_ocCls[className] = {
instMethods: {},
clsMethods: {},
}
if (superCls.length && _ocCls[superCls]) {
for (var funcName in _ocCls[superCls]['instMethods']) {
_ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
}
for (var funcName in _ocCls[superCls]['clsMethods']) {
_ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
}
}
这里就用到了刚才oc处理完的返回值中的类名和父类名,然后把这些方法实现都添加到ocCls这个存储方法实现的数组中,上述代码的第二段的是用来判断js的模拟oc类有没有实现该类的父类,如果有就也要把这个父类的方法添加到子类中去,这就实现了方法查找,在oc中的实现是先在该类中查找该方法没有就去父类查找,这里直接把子类没有的父类方法都添加到了子类,实现效果是一样的
_setupJSMethod(className, instMethods, 1, realClsName)
_setupJSMethod(className, clsMethods, 0, realClsName)
最后就是把这些实例方法和类方法都添加到_ocCls数组中去了
返回值是require(classname),这个require在bang大神的官方解释中也说明了,就是给这个类名添加一个js的全局变量,不然js在使用没有定义变量时会报错
总结
最后梳理一下思路,在JS里面定义的类实际上只有方法没有变量,变量是用set和get方法来实现的,所以JS中的模拟OC类只需要一个三维数组就能实现了_ocCls[类名][方法类型][方法名]这个数组中存放的都是开发者在js中定义的oc类在处理该类的时候回保证与oc一致,先去oc里面创建类添加方法然后再把这个方法实现添加到_ocCls,而在js中调用的方法都会被转发到__c这个方法上,这个方法会去查找你的方法名是不是在__ocCls表里面有这个实现,有的话直接调用这个方法,没有就去oc的运行时找有没有这个实现