博客> JSPatch 源码解析
JSPatch 源码解析
2019-08-21 18:11 评论:0 阅读:1687 lvhahaha
热更新 JSPatch

不知道这个Bang大神怎么能想到这么巧妙的方法,真的很帅!

一、入口 startEngine

首先建立 JSContext *context = [[JSContext alloc] init];上下文对像。

该方法中 向JSContext环境注册了一系列供js调用oc方法的block,这些 block 内部大多是 调用 runtime 相关接口的 static 函数。最终读取JSPatch.js中的代码到JSContext环境,使得main.js可以调用JSPatch.js中定义的方法。 调用关系大致如下: main.js ---> JSPatch.js ---> OC Block ---> runtime

_OC_defineClass 创建类 _OC_defineProtocol 给类实现某协议. _OC_callI js调用oc的实例方法. _OC_callC js调用oc的类方法. _OC_formatJSToOC js 对象转 oc 对象. _OC_formatOCToJS oc 对象 转 js 对象 _OC_getCustomProps 获取对象的动态成员变量 _OC_setCustomProps 给对象动态添加成员变量. weak 给 js 对象设置 weak. strong 给 js 对象设置 strong. include 在JS中调用include方法,可以在一个JS文件中加载其他JS文件 dispatch_after 让 js 方法延迟执行 dispatch_async_main 让js方法在 main queue dispatch async 执行 releaseTmpObj 释放js创建的oc对象. _OC_log js调用oc方法进行打印. _OC_catch 将js捕捉到的异常交给oc方法处理. oc 中的 nil 对象 _nilObj _JSMethodSignatureLock 同步锁.

[_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]]; 读取JSPatch.js,方便传入的js代码中使用JSPatch.js提供的函数.

二、 修复 :__c()元函数 [JPEngine evaluateScript:script];该接口并非直接将main.js代码提交到JSContext环境执行,而是调用 _evaluateScript: withSourceURL:方法对main.js原始代码做些修改 在此方法中需要用到正则式。需要对js脚本的方法进行修复。 1 正则式构建 (?<!\\)\.\s(\w+)\s\( _regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];

2 使用正则式处理 比如 将传入的 js代码 >>> 将 alloc()这样的函数调用 替换成 __c("alloc")() NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

3.将正则处理后的js代码加载到 context 执行.(进入 JavaScriptCore)

if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) { return [_context evaluateScript:formatedScript withSourceURL:resourceURL]; } else { return [_context evaluateScript:formatedScript]; }

最后可以看到 除了添加一些关键字和异常处理外,最大的变化在于所有函数调用变成了__c("function")的形式。这是JSPatch开发过程中最核心的问题。

给 JS 对象基类 Object 的 prototype 加上 c 成员,这样所有对象都可以调用到 c,根据当前对象类型判断进行不同操作:

__c: function(methodName) { ... ... return function(){ var args = Array.prototype.slice.call(arguments) return _methodFunc(slf.obj, slf.clsName, methodName, args, slf.__isSuper) } }

_methodFunc() 把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。

在JSPatch.js文件中, _methodFunc起到js对oc的接口作用

var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) 。 instance: 对象 clsName: 类名 methodName: 方法名 args: 参数列表 isSuper: 是否调用super父类的方法 isPerformSelector:是否用performSelector方式调用

methodName = methodName.replace(/__/g, "-") 处理得到OC中的方法SEL

var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args) 获取调用OC方法后的返回值,如果是获取一个OC对象,那么ret = {"__obj":OC对象},因为OC把对象返回给js之前会先包装成NSDictionary

return _formatOCToJS(ret) 获取OC方法执行完毕的返回值,并转化成JS对象

三、修复2,global.defineClass

在之前修复处理后的脚本代码中defineClass(ViewController,{instaceMethods...},{classMethods...}) 会分别对两个方法列表调用_formatDefineMethods,该方法参数有三个:方法列表(js对象)、空js对象、真实类名:

var _formatDefineMethods = function(methods, newMethods, realClsName)

遍历此类的方法列表,_formatDefineMethods将 defineClass传递过来的js对象进行了修改:1.参数转化为js对象 2.self 处理。使得js修复代码中我们可以像在 OC 中一样使用self。3.args.splice(0,1)删除前两个参数,前两个参数是self和selector

四、修复3,_OC_defineClass

JPEngine中的defineClass对类进行真正的重写操作,将类名、selector、方法实现(IMP)、方法签名等runtime重写方法所需的基本元素提取出来。

详见JPEngine.m的 defineClass方法的实现。 static NSDictionary defineClass(NSString classDeclaration, JSValue instanceMethods, JSValue classMethods)

  • 定义一个类/覆盖或新增一个方法.
  • @param classDeclaration 类的声明(需要替换或者新增的类名:继承的父类名 <实现的协议1,实现的协议2>)
  • @param instanceMethods {实例方法}
  • @param classMethods {类方法}
  • @return 返回@{@"cls": className, @"superCls": superClassName} 在此方法内部接着调用了overrideMethod方法。

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

通过获得要进行1替换的method的方法的selectorName,2 获取重写方法的具体实现函数的格式编码.3获取 class 中被重写 SEL 对应的原始IMP.在上边三者准备之后,则进行最重要的一步。将class中原来 forwardInvocaiton: 的实现替换成 JPForwardInvocation:函数实现.不管是替换原来的selector还是ad D最新的selector,

都在这里构建为一个新的sel。 NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName]; _initJPOverideMethods(cls); _JSOverideMethods[cls][JPSelectorName] = function;

class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);.替换原SEL的实现IMP为msgForwardIMP,被替换的方法调用时,直接进入“消息转发”流程(_objc_msgForward 或 _objc_msgForward_stret)

六、消息的拦截:JPForwardInvocation

上一步中消息已经经过 _objc_msgForward 发送出来,函数调用的参数会被封装到NSInvocation对象,走到forwardInvocation方法。上一步中forwardInvocation方法的实现替换成了JPForwardInvocation,负责拦截系统消息转发函数传入的NSInvocation并从中获取到所有的方法执行参数值,是实现替换和新增方法的核心。 1.SEL转化为JS的JPSEL (JSValue* function的key格式 ) 2.把self与相应的参数都转换成js对象并封装到一个集合中 3.存储NSInvacation中获取的参数列表,传给对应的js函数(包括OC中的基本数据类型,指针类型数据,Class类型,枚举类型)

现在main.js中所有的函数都被替换成名为c('methodName')的函数调用,c调用了_methodFunc函数,_methodFunc会根据方法类型调用_OC_call: var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args)

最终会调用一个定义好的函数:callSelector。

七、消息的调用: callSelector

main.js中的每个方法的调用都经过oc中的callSelector进行处理。

其实这个callSelector是把JS的对象再转回OC对象,包括方法的调用,可以把参数等信息封装成NSInvocation对象,并执行,然后返回结果。

1.将js封装的instance对象进行拆装,得到oc对象. 2.将js封装的参数列表转为oc类型. 3.根据类名与selectorName获得对应的类对象与selector

其实还是NSInvacation的组装,数据类型的转化。之后在OC执行完方法之后,把结果返回給JS。

JSPatch 基于JavaScriptCore.framework和Objective-C中的runtime技术。

  • 采用 iOS7 后引入的 JavaScriptCore.framework作为 JavaScript 引擎解析js脚本,执行js代码并与OC端代码进行桥接。
  • 使用Objective-C runtime中的method swizzling方式达到使用js脚本动态替换原有OC方法的目的,并利用forwardInvocation消息转发机制使得在js脚本中调用OC的方法成为可能。
收藏
1
sina weixin mail 回到顶部