博客> JSPath原理深入分析
JSPath原理深入分析
2018-08-16 18:21 评论:0 阅读:511 老白爱学习
ios JSPatch

曾经红极一时的热修复技术由于苹果的一封邮件而被观望了.虽然这样,但是我也要把我学习JSPath的一些心得分享一下.

1.修复第一步:startEngine [JPEngine startEngine];

使用JSPatch框架首先要调用JPEngine中的类方法startEngine,这个方法的是为了初始化JSContext,JSContext是JS脚本的运行环境。JS脚本可以调用在JSContext中预先定义的方法,方法的参数/返回值都会被JavaScriptCore.framework自动转换.

接下来我们继续回到startEngine代码中可以找到_OC_defineClass,它是来负责定义新的类或替换原有的类的一个方法: context[@"_OC_defineClass"] = ^(NSString classDeclaration, JSValue instanceMethods, JSValue *classMethods) { return defineClass(classDeclaration, instanceMethods, classMethods); };

而执行context[@“_OC_defineClass”]实际上是执行Native中下边的代码: static NSDictionary defineClass(NSString classDeclaration, JSValue instanceMethods, JSValue classMethods) {………}

2.修复第二步:接下来读取main.js代码后执行: [JPEngine evaluateScript:script]; 该方法并非直接将main.js代码提交到JSContext环境执行,而是先调用_evaluateScript: withSourceURL:方法对main.js原始代码做些修改.

  • (JSValue )_evaluateScript:(NSString )script withSourceURL:(NSURL *)resourceURL {………} 进过这个方法,可以将传入的script进行正则处理.将所有函数调用变成了__c(“function”)函数的形式.

因为JS 对于调用没定义的属性/变量,只会马上抛出异常,而不像 OC那样有转发机制。因此对于用户传入的js代码中,类似UIView().alloc().init()这样的代码,js其实根本没办法进行处理.

所以在 OC 执行 JS 脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个 JS 脚本,做到了类似 OC中的消息转发机制。

3.修复第三步:global.defineClass

原脚本代码经过正则处理后交由JSContext环境去执行: [_context evaluateScript:formatedScript withSourceURL:resourceURL] 接下来我们就可以看main.js中的代码,经过正则处理过的: defineClass(ViewController,{instaceMethods...},{classMethods...}) 参数依次为类名、实例方法列表、类方法列表。阅读global.defineClass源码会发现defineClass首先会分别对两个方法列表调用_formatDefineMethods,该方法参数有三个:方法列表(js对象)、空js对象、真实类名: var _formatDefineMethods = function(methods, newMethods, realClsName) { ……… } 该段代码遍历方法列表对象的方法名,向js空对象中添加属性:方法名为键,一个数组为值。数组第一个元素为对应实现函数的参数个数,第二个元素是方法的具体实现。也就是说,_formatDefineMethods将 defineClass传递过来的js对象进行了修改.之后会拿着要重写的类名和经过处理的js对象,调用_OC_defineClass,也就是OC端定义的block方法。

4.修复第四部:OCdefineClass static NSDictionary defineClass(NSString classDeclaration, JSValue instanceMethods, JSValue classMethods)
{………} 可以看到defineClass函数可接受三个参数: 1.字符串:”需要替换或者新增的类名:继承的父类名 <实现的协议1,实现的协议2>” 2.{实例方法} 3.{类方法}

内部代码具体实现是将这三个参数通过bridging传入到OC后,执行以下步骤:(详细的注释我已经添加在代码中)

1.使用NSScanner分离classDeclaration,分离成三部分 类名 : className 父类名 : superClassName 实现的协议名 : protocalNames

2.使用NSClassFromString(className)获得该Class对象。 若该Class对象为nil,则说明JS端要添加一个新的类,使用objc_allocateClassPair与objc_registerClassPair注册一个新的类。 若该Class对象不为nil,则说明JS端要替换一个原本已存在的类.

3.根据从JS端传递来的实例方法与类方法参数,为这个类对象添加/替换实例方法与类方法 遍历传递过过来的实例方法,类方法的js实例,然后依次遍历方法字典,完成方法名js命名到native命名的转换。 通过转换后的方法名,用class_respondsToSelector判断是否该类的方法列表中是否已经存在该方法的实现,存在即调用overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);覆盖方法实现。 如果类的方法列表中不存在该方法的实现,则通过实现的协议的列表查找,依次判断方法是否在协议的实现方法中,如果以上都不是说明是新添加的方法.构造一个typeDescription为”@@:\@*”(返回类型为id,参数值根据JS定义的参数个数来决定。新增方法的返回类型和参数类型只能为id类型,因为在JS端只能定义对象)的IMP。将这个IMP添加到类中。

4.为该类添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject和 objc_setAssociatedObject让JS脚本拥有设置property的能力. 5.返回{className:cls}回JS脚本。

我们通过源码可见,在方法名、实现等处理好之后最终执行overrideMethod方法

5.修复第五部:overrideMethod

overrideMethod方法:

overrideMethod是实现“替换”的最后一步。通过调用一系列runtime 方法增加/替换实现的api,使用jsvalue中将要替换的方法实现来替换oc类中的方法实现.

static void overrideMethod(Class cls, NSString selectorName, JSValue function, BOOL isClassMethod, const char *typeDescription) {…………} 它接受五个参数: • 类名 • 要替换的方法名 • JS中定义的方法 • 是否类方法 • 方法的typeDescription

逻辑步骤如下:(详细的我已经在代码中添加注释)

  1. 初始化SEL:根据selectorName获取对应的Selector;typeDescription获得 NSMethodSignature的方法签名.
  2. 保存原有方法的IMP,添加名为@"ORIG" + selectorName的方法,IMP为原方法的IMP。
  3. 将原方法的IMP设置为消息转发 若该方法的返回值为特殊的struct类型,则需要将IMP设置为(IMP)_objc_msgForward_stret 否则的话将IMP设置为_objc_msgForward
  4. 保存原有转发方法forwardInvocation:的IMP,添加selectorName为 @”ORIGforwardInvocation:”,IMP为原转发方法IMP的方法。
  5. 将原转发方法替换为自己的转发方法JPForwardInvocation
  6. 根据替换/添加方法的返回类型,选择不同的替换IMP(使用宏的形式定义),替换原方法。

至此,selector具体实现 IMP 的替换工作已经完成了。接下来便可以分析一下点击button后的handle事件。

调用 第一步:JPForwardInvocation 这一步是,OC调用JS重写的方法。 OC:handleBtn空实现--->js:handleBtn function

Objective-C都是通过发送消息来调用方法的。而消息转发就是:向一个对象发送它不会处理的消息,是一个错误,不过在报错之前,RuntimeSystem给了接收对象第二次的机会来处理消息。在这种情况下,Runtime System会向对象发一个消息,forwardInvocation:,这个消息只携带一个NSInvocation对象作为参数——这个NSInvocation对象包装了原始消息和相应参数。通过实现forwardInvocation:方法(继承于NSObject),可以给不响应的消息一个默认处理方式。正如方法名一样,通常的处理方式就是转发该消息给另一个对象.

  • (void)forwardInvocation:(NSInvocation *)anInvocation { if ([someOtherObject respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:someOtherObject]; } else { [super forwardInvocation:anInvocation]; } }

JPForwardInvocation方法替换了原有-forwardInvocation方法的实现,使得消息转发都通过该方法,并将消息转发给JS脚本中定义的方法,通过JavascriptCore.frameWork中提供的callWithArguments方法调用JS方法达到替换原方法,添加新方法的目的。是实现替换和新增方法的核心。

static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) {………}

它的内部逻辑并不复杂,主要是读取出传入的invocation对象中的所有参数,根据实际参数的类型将JSValue类型的参数转换成对应的OC类型,最后将参数添加到_TMPInvocationArguments数组以供JS调用。

接下来执行JS中定义的方法实现.现在main.js中所有的函数都被替换成名为c(‘methodName')的函数调用,c调用了_methodFunc函数,_methodFunc会根据方法类型调用_OC_call:,这个函数最终都会调用一个static函数callSelector。

调用 第二步:callSelector

这一步是js重写方法中,调用OC的对象方法等. 比如我们在JS中定义:JPTableViewController.alloc().init(),实际是通过callSelector调用OC的方法.

static id callSelector(NSString className, NSString selectorName, JSValue arguments, JSValue instance, BOOL isSuper)

}

该方法接受五个参数

• 调用对象的类名 • 被调用的selectorName • JS中传递过来的参数 • JS端封装的实例对象 • 是否调用的是super类的方法

大致分为以下三部:

1、把js对象和js参数转换为OC对象;

  2、判断是否调用的是父类的方法,如果是,就走父类的方法实现;

  3、把参数等信息封装成NSInvocation对象,并执行,然后返回结果

至此,JSPatch 热修复核心步骤「方法替换」和「方法调用」就结束了。

对象的持有/转换

UIView.alloc() 通过上述消息传递后会到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对象的指针,如果不经过任何处理,是无法通过这个变量去调用实例方法的。所以在返回对象时,JSPatch会对这个对象进行封装。

首先,告诉JS这是一个OC对象:

static NSDictionary *toJSObj(id obj) { if (!obj) return nil; return @{@"__isObj": @(YES), @"cls": NSStringFromClass([obj class]), @"obj": obj}; }

用__isObj表示这是一个OC对象,对象指针也一起返回。接着在JS端会把这个对象转为一个 JSClass 实例:

var _formatOCToJS = function(obj) {………}

接着看看对象是怎样回传给OC的。上述例子中,view.setBackgroundColor(require(‘UIColor’).grayColor()),这里生成了一个 UIColor 实例对象,并作为参数回传给OC。根据上面说的,这个 UIColor 实例在JS中的表示是一个 JSClass 实例,所以不能直接回传给OC,这里的参数实际上会在 c 函数进行处理,会把对象的 .obj 原指针回传给OC。

收藏
1
sina weixin mail 回到顶部