一:关于JSPatch
JSPatch : 是一个iOS动态更新框架,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。
二:基础原理
JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替换某个类的方法为新的实现:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
三:方法调用
1. 调用require('UIView')后,就可以直接使用UIView这个变量去调用相应的类方法了,require 做的事很简单,就是在JS全局作用域上创建一个同名变量,变量指向一个对象,对象属性__isCls表明这是一个Class,__clsName保存类名,在调用方法时会用到这两个属性。
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__isCls: 1,
__clsName: clsName
}
}
return global[clsName]
}
2.封装JS对象
_c()元函数:
在 OC 执行 JS 脚本前,通过正则把所有方法调用都改成调用__c()函数,再执行这个 JS 脚本,做到了类似 OC/Lua/Ruby 等的消息转发机制:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
给 JS 对象基类 Object 的 prototype 加上__c成员,这样所有对象都可以调用到__c,根据当前对象类型判断进行不同操作:
Object.prototype.__c = function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}
_methodFunc()就是把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。
3.消息传递
OC 端在启动 JSPatch 引擎时会创建一个JSContext实例,JSContext是 JS 代码的执行环境,可以给JSContext添加方法,JS 就可以直接调用这个方法:
JSContext *context = [[JSContext alloc] init];
context[@"hello"] = ^(NSString *msg) {
NSLog(@"hello %@", msg);
};
[_context evaluateScript:@"hello('word')"];
JS 通过调用JSContext定义的方法把数据传给 OC,OC 通过返回值传会给 JS。调用这种方法,它的参数/返回值 JavaScriptCore 都会自动转换,OC 里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 会分别转为JS端的数组/对象/字符串/数字/函数类型。
4.对象持有/转换
结合上述几点,可以知道UIView.alloc()这个类方法调用语句是怎样执行的:
a.require('UIView')这句话在 JS 全局作用域生成了UIView这个对象,它有个属性叫__isCls,表示这代表一个 OC 类。 b.调用UIView这个对象的alloc()方法,会去到__c()函数,在这个函数里判断到调用者__isCls属性,知道它是代表 OC 类,把方法名和类名传递给 OC 完成调用。
调用类方法过程是这样,那实例方法呢?UIView.alloc()会返回一个 UIView 实例对象给 JS,这个 OC 实例对象在 JS 是怎样表示的?怎样可以在 JS 拿到这个实例对象后可以直接调用它的实例方法UIView.alloc().init()?
对于一个自定义id对象,JavaScriptCore 会把这个自定义对象的指针传给 JS,这个对象在 JS 无法使用,但在回传给 OC 时 OC 可以找到这个对象。对于这个对象生命周期的管理,按我的理解如果JS有变量引用时,这个 OC 对象引用计数就加1 ,JS 变量的引用释放了就减1,如果 OC 上没别的持有者,这个OC对象的生命周期就跟着 JS 走了,会在 JS 进行垃圾回收时释放。
传回给 JS 的变量是这个 OC 对象的指针,这个指针也可以重新传回 OC,要在 JS 调用这个对象的某个实例方法,根据第2点 JS 接口的描述,只需在__c()函数里把这个对象指针以及它要调用的方法名传回给 OC 就行了,现在问题只剩下:怎样在__c()函数里判断调用者是一个 OC 对象指针?
目前没找到方法判断一个 JS 对象是否表示 OC 指针,这里的解决方法是在 OC 把对象返回给 JS 之前,先把它包装成一个 NSDictionary:
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}
让 OC 对象作为这个 NSDictionary 的一个值,这样在 JS 里这个对象就变成:
{__obj: [OC Object 对象指针]}
这样就可以通过判断对象是否有__obj属性得知这个对象是否表示 OC 对象指针,在__c函数里若判断到调用者有__obj属性,取出这个属性,跟调用的实例方法一起传回给 OC,就完成了实例方法的调用。
5.类型转换
JS 把要调用的类名/方法名/对象传给 OC 后,OC 调用类/对象相应的方法是通过 NSInvocation 实现,要能顺利调用到方法并取得返回值,要做两件事:
a.取得要调用的 OC 方法各参数类型,把 JS 传来的对象转为要求的类型进行调用。 b.根据返回值类型取出返回值,包装为对象传回给 JS。
OC上,每个类都是这样一个结构体:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法链表*/
};
其中 methodList 方法链表里存储的是 Method 类型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一个方法的全部信息,包括 SEL 方法名,type 各参数和返回值类型,IMP 该方法具体实现的函数指针。
通过 Selector 调用方法时,会从 methodList 链表里找到对应Method进行调用,这个 methodList 上的的元素是可以动态替换的,可以把某个 Selector 对应的函数指针IMP替换成新的,也可以拿到已有的某个 Selector 对应的函数指针IMP,让另一个 Selector 跟它对应,Runtime 提供了一些接口做这些事,以替换 UIViewController 的-viewDidLoad:方法为例:
static void viewDidLoadIMP (id slf, SEL sel) {
JSValue *jsFunction = …;
[jsFunction callWithArguments:nil];
}
Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
//获得viewDidLoad方法的函数指针
IMP imp = method_getImplementation(method)
//获得viewDidLoad方法的参数类型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一个ORIGViewDidLoad方法,指向原来的viewDidLoad实现
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定义新的实现
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
这样就把 UIViewController 的-viewDidLoad方法给替换成我们自定义的方法,APP里调用 UIViewController 的viewDidLoad方法都会去到上述 viewDidLoadIMP 函数里,在这个新的IMP函数里调用 JS 传进来的方法,就实现了替换 viewDidLoad 方法为JS代码里的实现,同时为 UIViewController 新增了个方法-ORIGViewDidLoad指向原来 viewDidLoad 的 IMP,JS 可以通过这个方法调用到原来的实现。
方法替换就这样很简单的实现了,但这么简单的前提是,这个方法没有参数。如果这个方法有参数,怎样把参数值传给我们新的 IMP 函数呢?例如 UIViewController 的-viewDidAppear:方法,调用者会传一个 Bool 值,我们需要在自己实现的IMP(上述的 viewDidLoadIMP)上拿到这个值,怎样能拿到?如果只是针对一个方法写 IMP,是可以直接拿到这个参数值的:
static void viewDidAppear (id slf, SEL sel, BOOL animated) {
[function callWithArguments:@(animated)];
}
但我们要的是实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转,拿到方法的所有参数回调JS的实现。
以上主要是JSPatch实现的一些基础原理,以及代码展示便于理解;原理很重要,但是也要能做出东西呀!这里我们基于三方的JSPatch做个展示:
http://jspatch.com 这个是三分的一个平台;
通过引入SDK,倒入相关的库,代码处理起来很简单;
1.在app delegate 启动中调用:
[JSPatch startAppWithKey:@""]; //填入自己在该平台注册app,所获得的key
#ifdef DEBUG
[JSPatch setupDevelopment];
#endif
[JSPatch sync];
然后在该平台设置设置自己需要上传的jspatch文件;
当我们再次运行代码的时候,已编辑的JSPatch文件就可以起作用了;(很可惜,三方就是三方,需要花费呀!正常日活少于1万,是不要钱的)!
附录:
1.基础语法学习:
https://github.com/bang590/JSPatch/wiki/JSPatch-基础用法
2.常见问题
https://github.com/bang590/JSPatch/wiki/JSPatch-常见问题
3.懒人快速转换方法:
http://jspatch.com/Tools/convertor (实测过这个转换的方法,对于一些简单的错误很好用,过于复杂的就有点力不从心了,还是需要对基础用法有一定认识!)
网友评论