IOS关于热修复JSPatch

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容