JSPatch源码剖析(一)

项目中使用到了JSPatch来实现线上APP bug的hot fix,使用后觉得JSPatch短小精悍并且功能强大,于是想往里窥探下其实现机制。本文从JSPatch的使用角度去分析源码。

JavaScript运行环境建立

JavaScript运行环境的建立很简单,只需要调用一个API即可:

[JPEngine startEngine];

+(void)startEngineAPI的主要工作是建立一个JSContext类型的全局_context变量,后续JS和OC的互调都是在这个上下文中进行。由于JSContext实现了下面两个方法,从而可以进行下标操作访问:

- (JSValue *)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;

_contenxt的初始化中,就大量的运用了下标操作符,就是下面这种方式:

context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };
...

通过这种方式,以后就可以在JS中直接使用_OC_defineClass,从而调用到OC的defineClass函数。
在初始化最后,直接在_context中运行下jspatch.jsJS代码:

    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    NSAssert(path, @"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];

    if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
        [_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
    } else {
        [_context evaluateScript:jsCore];
    }

jspatch.js中在JS全局上下文中定义了defineClassdefineProtocol等。

解析JS代码

下面是JSPatch Demo里的一段JS源码,该demo通过JS写了个UITableViewContorller的示例:

// demo.js
defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', {
  dataSource: function() {
    var data = self.getProp('data')
    ...
    self.setProp_forKey(data, 'data')
    return data;
  },
  numberOfSectionsInTableView: function(tableView) {},
  tableView_cellForRowAtIndexPath: function(tableView, indexPath) {},
  ...
})

通过调用+ (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURLAPI将该JS代码放在之前建立的 _context中运行;在正式运行该JS代码前先对JS字符串进行了预处理,通过正则表达式 \\.\\s*(\\w+)\\s*\\(找到JS脚本中的所有函数调用,比如demo.js中的self.getProp('data'),将函数名替换成 self.__c("getProp")('data'),即添加一层__c(arg)JS调用,这样调用任何方法前,先调用__c(arg)方法,这个方法在JSPatch.js中定义,后续详细讲解这个函数的定义。

    NSString *formatedScript = [NSString stringWithFormat:@"try{%@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

函数替换完成后调用 下面方法执行得到的JS脚本formatedScript。

- (JSValue *)evaluateScript:(NSString *)script;

JSPatch.jsdefineClass函数可以动态添加OC类,也可以给类添加类方法、实例方法、属性。defineClass函数定义如下,主要对传入的实例方法和类方法调用_formatDefineMethods函数进行js格式化,然后再调用_OC_defineClass函数处理前者的结果,进行更深入的类定义、类方法以及实例方法解析:

// JSPatch.js
global.defineClass = function(declaration, instMethods, clsMethods) {
    var newInstMethods = {}, newClsMethods = {}
    _formatDefineMethods(instMethods, newInstMethods)
    _formatDefineMethods(clsMethods, newClsMethods)
    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
    return require(ret["cls"])
  }

其中_formatDefineMethods函数定义如下,该函数对传入的methods字典转化成一个新的字典,key依然是原来的方法名,value变成一个包含两个元素的数组,第一个元素是方法参数个数(不包括self_cmd参数),第二个元素是基于原js函数重新定义的js函数;重新定义的js函数先保存原来的全局self对象,然后将原js方法的第一个参数(即self)赋值给全局的self对象,并将self参数从参数列表里移除,利用剩下的参数调用原来js方法,并将执行结果返回。

// JSPatch.js
  var _formatDefineMethods = function(methods, newMethods) {
    for (var methodName in methods) {
      (function(){
       var originMethod = methods[methodName]
        newMethods[methodName] = [originMethod.length, function() {
          var args = _formatOCToJS(Array.prototype.slice.call(arguments))
          var lastSelf = global.self
          var ret;
          try {
            global.self = args[0]
            args.splice(0,1)
            ret = originMethod.apply(originMethod, args)
            global.self = lastSelf
          } catch(e) {
            _OC_catch(e.message, e.stack)
          }
          return ret
        }]
      })()
    }
  }

前面例子返回的字典为:

{"dataSource" : [0,func], "numberOfSectionsInTableView" : [1, func], "tableView_cellForRowAtIndexPath" : [2, func]}.

实例方法和类方法字典格式化后,调用_OC_defineClass进行进一步解析,_OC_defineClass函数其实是调用了OC函数static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
,定义如下:

   context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };

defineClass函数做具体的解析工作, defineClass函数的具体解析步骤如下:

  • 扫描classDeclaration,提取出 classNamesuperClassName以及 protocolNames
  • 如果className类不存在,则通过运行时方法 objc_allocateClassPairobjc_registerClassPair创建并注册className
  • 解析实例方法和类方法,
    • 将方法名中的双下划线__替换成单连字符-
    • 将单下划线_替换成冒号:
    • 将单连字符-替换成单下划线_,这里其实就是告诉开发者如果方法名中实际包含下划线,则需要使用双下划线
  • 比较替换后的方法中冒号:的个数以及_formatDefineMethods返回的方法参数个数,决定替换后的方法名后是否需要增加一个:
  • 通过调用 class_respondsToSelector判断className类是否已经定义该方法,如果定义了该方法,则调用 overrideMethod重写该方法,调用该方法不需要传入参数描述。
  • 如果className类没有定义该方法,调用 methodTypesInProtocol方法查询继承的protocol中有没有定义该方法,如果定义了该方法,则返回该方法的参数描述,然后将这个参数描述作为参数调用overrideMethod
  • 如果继承的protocol里没有定义该方法,则根据参数个数构建一个参数描述,这里的参数个数需要加上self_cmd参数,并且所有其它参数都是id类型,然后将这个生成的参数描述作为参数调用overrideMethod
  • 调用运行时方法 class_addMethodclassName类添加了 getPropsetProp:forKey:两个方法,这两个方法通过关联对象方式给类实例动态添加属性,代码如下。
  • 最后返回字典 {@"cls": className}require(‘className’)JS方法,该方法将类信息记录在JS全局变量global["className"] = {__isCls : 1, __clsNmae : "className"};
// defineClass函数
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");

static id getPropIMP(id slf, SEL selector, NSString *propName) {
    return objc_getAssociatedObject(slf, propKey(propName));
}
static void setPropIMP(id slf, SEL selector, id val, NSString *propName) {
    objc_setAssociatedObject(slf, propKey(propName), val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

OC的defineClass函数调用了 methodTypesInProtocol方法获取方法的参数描述符,这个函数通过运行时方法 objc_getProtocol得到协议相关信息,

    Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);

然后通过运行时方法 protocol_copyMethodDescriptionList获取到协议中定义的可选(必须)的实例方法或者类方法列表,

    struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, isRequired, isInstanceMethod, &selCount);

最后遍历得到的列表,找到selectorName对应的objc_method_description,并将其参数描述符返回。

从上面分析可知OC方法 defineClass最终其实调用overrideMethod将实例方法instanceMethods和类方法classMethods添加到className类的,overrideMethod具体的实现步骤如下:

//overrideMethod函数原型
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
  1. 如果传入的参数描述符 typeDescriptionnull,则说明这个方法已经定义过,通过 class_getInstanceMethodcls类获取到selectorName对应的Method,再通过 method_getTypeEncoding得到Method的参数描述符。
  2. 如果cls类已经实现过selectorName方法,则获取到原来方法对应的函数实现IMP:

    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
    
  3. 根据typeDescription可知道方法的返回值是不是结构体,建立selectorName选择子和 _objc_msgForward(IMP)或者 _objc_msgForward_stret(IMP)的映射关系,

    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
  4. @selector(forwardInvocation:)的IMP设置为 JPForwardInvocation,将@selector(ORIGforwardInvocation:)的IMP设置为@selector(forwardInvocation:)原来的实现

    //overridMethod
    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
    }
  5. 如果cls类原本实现过selectorName方法,则将原来的方法重新命名为ORIGselectorName,即在原来方法名加前缀ORIG,

    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }
  6. 通过全局字典数组保存cls_JPselectorNamefunction之间的映射关系:

    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;
  7. cls添加_JPselectorName方法,映射到前面得到的 msgForwardIMP
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值