项目中使用到了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全局上下文中定义了defineClass、defineProtocol等。
解析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.js中defineClass函数可以动态添加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,提取出className,superClassName以及protocolNames - 如果
className类不存在,则通过运行时方法objc_allocateClassPair和objc_registerClassPair创建并注册className类 - 解析实例方法和类方法,
- 将方法名中的双下划线
__替换成单连字符- - 将单下划线
_替换成冒号: - 将单连字符
-替换成单下划线_,这里其实就是告诉开发者如果方法名中实际包含下划线,则需要使用双下划线
- 将方法名中的双下划线
- 比较替换后的方法中冒号
:的个数以及_formatDefineMethods返回的方法参数个数,决定替换后的方法名后是否需要增加一个: - 通过调用
class_respondsToSelector判断className类是否已经定义该方法,如果定义了该方法,则调用overrideMethod重写该方法,调用该方法不需要传入参数描述。 - 如果
className类没有定义该方法,调用methodTypesInProtocol方法查询继承的protocol中有没有定义该方法,如果定义了该方法,则返回该方法的参数描述,然后将这个参数描述作为参数调用overrideMethod - 如果继承的
protocol里没有定义该方法,则根据参数个数构建一个参数描述,这里的参数个数需要加上self和_cmd参数,并且所有其它参数都是id类型,然后将这个生成的参数描述作为参数调用overrideMethod - 调用运行时方法
class_addMethod给className类添加了getProp和setProp: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)
- 如果传入的参数描述符
typeDescription为null,则说明这个方法已经定义过,通过class_getInstanceMethod从cls类获取到selectorName对应的Method,再通过method_getTypeEncoding得到Method的参数描述符。 如果cls类已经实现过
selectorName方法,则获取到原来方法对应的函数实现IMP:IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;根据
typeDescription可知道方法的返回值是不是结构体,建立selectorName选择子和_objc_msgForward(IMP)或者_objc_msgForward_stret(IMP)的映射关系,class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);将
@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@:@"); }如果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); } }通过全局字典数组保存
cls、_JPselectorName、function之间的映射关系:_initJPOverideMethods(cls); _JSOverideMethods[cls][JPSelectorName] = function;- 给
cls添加_JPselectorName方法,映射到前面得到的msgForwardIMP。
3998

被折叠的 条评论
为什么被折叠?



