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的运行时找有没有这个实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容