按照常例,是要给demo的
JSPatch下载
还有作者详解
首先搭建第一个JSPatch项目
1、下载源码拖进去,只需要如下目录
2、创建自己的js文件,然后在js文件中敲下如下代码
require('UIView, UIColor, UILabel')
defineClass('AppDelegate', {
// replace the -genView method
getView: function() {
var view = self.ORIGgetView();
view.setBackgroundColor(UIColor.greenColor())
var label = UILabel.alloc().initWithFrame(view.frame());
label.setText("JSPatch");
label.setTextAlignment(1);
view.addSubview(label);
return view;
}
});
大概意思就是,为AppDelegate重写getView方法,方法中调用ORIGgetView,也就是原来的getview方法。require就是创建了这几个全局变量,变量指向一个_clsName为“UIView"的对象。
require生成类对象时,把类名传入OC,OC 通过runtime方法找出这个类所有的方法返回给 JS,JS 类对象为每个方法名都生成一个函数,函数内容就是拿着方法名去 OC 调用相应方法。
3、然后在appdelegate中,调用。
[JPEngine startEngine];
NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
这里需要注意的是,在Build Phases 中Copy Bundle Resources 中一定要有demo.js,不然就拿不到了。
好,搭建流程就是这么简单。具体的怎么新建类,或者替换类中的方法,可以参考作者gitbub中详细的用法介绍。
JSPatch的原理是运用oc的动态性,所以在读源码之前。可以先看一下http://www.jianshu.com/p/a3f95abc745f ,了解一下OC 中runtime是怎么调用,以及偷换运行时方法的(addMethod以及replaceMethod)
从入口开始读源码
/*!
@method
@discussion start the JSPatch engine, execute only once.
*/
+ (void)startEngine;
/*!
@method
@description Evaluate Javascript code from a file Path. Call
it after +startEngine.
@param filePath: The filePath of the Javascript code.
@result The last value generated by the script.
*/
+ (JSValue *)evaluateScriptWithPath:(NSString *)filePath;
刚接触JSPatch,只用到了这两个方法。
先看第一个方法
-(void)startEngine;
具体的源码可以下载下来查看,在源码里面看到很多相似的东西
比如
JSContext *context = [[JSContext alloc] init];
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
};
JScontext是JavaScriptCore这个库里面的类。暂时理解一个js与ios交互的上下文。传入一个方法名,js中就可以调用这个方法,执行的就是block中的这段代码。参数列表也是从js中调用方法的时候传入,oc这边接收。
简单看一下jspatch.m中的代码。
找到js中调用_OC_defineClass的代码。
global.defineClass = function(declaration, instMethods,
clsMethods) {
var newInstMethods = {}, newClsMethods = {}
_formatDefineMethods(instMethods, newInstMethods,declaration)
_formatDefineMethods(clsMethods, newClsMethods,declaration)
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
return require(ret["cls"])
}
global.defineClass这里我理解为,定义了全局的defineClass方法为function(···){};(我在demo.js中调用的defineclass方法,就算在这里定义的)
然后具体看一下,在defineClass的时候,都进行了哪些操作
先是初始化了两个方法的字典对象(因为oc中是运行时发送消息机制,所以,一个方法中需要带有方法名,方法指针,方法参数,方法接收对象等参数)。
然后看一下_formatDefineMethods方法中执行了什么,直接贴上源码
var _formatDefineMethods = function(methods,
newMethods, declaration) {
//遍历我们要求覆盖的方法
for (var methodName in methods) {
(function(){
var originMethod = methods[methodName]
newMethods[methodName] = [originMethod.length, function() {
//oc转js , arguments在js中代表被传递的参数,这里是为了把参数转化为js数组
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
var ret;
try {
global.self = args[0]
if (global.self) {
// 把类名作为全局变量保存下来
global.self.__clsDeclaration = declaration
}
//删除第0个参数,也就是self。因为在执行的过程中,第一个参数是消息接收的对象,现在需要复制
这个方法,所以,不需要第一个参数,因为调用的对象可能就不再是self了。
args.splice(0,1)
js 中apply
//复制了originMethod的方法和属性,我理解为只是更新了参数,然后返回方法名。
ret = originMethod.apply(originMethod, args)
global.self = lastSelf
} catch(e) {
_OC_catch(e.message, e.stack)
}
return ret
}]
})()
}
}
看以上代码,能够知道,是把新方法中的实现和相关参数,关联到了老方法中。也就是生成了一个方法名和老方法一样,但是执行函数不一样的方法(oc用是一个结果体),这里生成一个字典,在oc中再去拿到相应值去处理。
然后在defineClass方法中,调用了oc中的方法OC_defineClass(declaration, newInstMethods, newClsMethods),具体实现内容可以在源码中查看,这里,引擎就从js中抽取了我们要覆盖的类,和方法。然后就交给oc去覆盖方法。
以下是纯oc中的实现
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *
classMethods)
{
NSDictionary *declarationDict = convertJPDeclarationString(classDeclaration);
NSString *className = declarationDict[@"className"];
NSString *superClassName = declarationDict[@"superClassName"];
//有关protocol的我直接略过了,看着好头疼。
NSArray *protocols = [declarationDict[@"protocolNames"] length] ?
[declarationDict[@"protocolNames"] componentsSeparatedByString:@","] : nil;
Class cls = NSClassFromString(className);
if (!cls) {
Class superCls = NSClassFromString(superClassName);
if (!superCls) {
NSCAssert(NO, @"can't find the super class %@", superClassName);
return @{@"cls": className};
}
//找到父类,然后分配内存,新建类,具体用法查阅runtime 的API
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
}
for (int i = 0; i < 2; i ++) {(传进来的参数中,在前的是实例方法,在后的是类方法)
BOOL isInstance = i == 0;
// 判断是实例方法还是类方法?
JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
//是实例方法就拿它所属的类,是类方法就拿它锁属的元类(oc中,一个实例是类对象,
//他的isa指向它的类,一个类也是类(元类)对象,它的类方法(isa指向元类)存在于元类中)
Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
NSDictionary *methodDict = [jsMethods toDictionary];
//这个for循环开始遍历给这个类添加的所有方法,并覆盖原有方法
for (NSString *jsMethodName in methodDict.allKeys) {
JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
int numberOfArg = [jsMethodArr[0] toInt32];
//选择器名,也就是一个方法(method)中的方法名
NSString *selectorName = convertJPSelectorString(jsMethodName);
if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
selectorName = [selectorName stringByAppendingString:@":"];
}
JSValue *jsMethod = jsMethodArr[1];
//如果currCls实现了这个方法,则override,覆盖
if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
//overrideMethod方法中的核心方法是,replaceMethod,给一个对象传入一个selector方法名
//和一个需要覆盖这个selector的imp(函数地址)和相关的参数
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) {
NSMutableString *typeDescStr = [@"@@:" mutableCopy];
for (int i = 0; i < numberOfArg; i ++) {
[typeDescStr appendString:@"@"];
}
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
#pragma clang diagnostic pop
return @{@"cls": className};
}
大概的调用流程就是这样的,(js拿到各种参数->转oc->runtime添加方法)更详细的原理以及思考可以在作者github中看到。其他还有很多其他函数的实现。如果要读,也可以直接从入口startEngine中去看。
在实际的操作中,我们需要设计一个下载机制,然后在通过作者提供的使用方法,把需要修复的类的某个方法替换掉,就可以实行热修复了。