项目中使用到了JSPatch来实现线上APP bug的hot fix,使用后觉得JSPatch短小精悍并且功能强大,于是想往里窥探下其实现机制。本文从JSPatch的使用角度去分析源码。
JavaScript运行环境建立
JavaScript运行环境的建立很简单,只需要调用一个API即可:
[JPEngine startEngine];
+(void)startEngine
API的主要工作是建立一个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.js
JS代码:
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 *)resourceURL
API将该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
。