美文网首页
IOS关于热修复JSPatch

IOS关于热修复JSPatch

作者: dahaibushen | 来源:发表于2017-02-27 21:42 被阅读0次

一:关于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   (实测过这个转换的方法,对于一些简单的错误很好用,过于复杂的就有点力不从心了,还是需要对基础用法有一定认识!)

相关文章

网友评论

      本文标题:IOS关于热修复JSPatch

      本文链接:https://www.haomeiwen.com/subject/uyrtgttx.html