天天看点

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&amp;mid=2247483662&amp;idx=1&amp;sn=c7d9ee27eff35688180bdc840d31120b&amp;scene=4#wechat_redirect">jspatch 实现原理详解 &lt;一&gt; 核心</a>

<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&amp;mid=2247483662&amp;idx=2&amp;sn=44b62a84a122886b08874861df83d889&amp;scene=4#wechat_redirect">jspatch 实现原理详解 &lt;二&gt; 细节</a>

<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&amp;mid=2247483662&amp;idx=3&amp;sn=9af2403895ff8e09bd7b7d767a34dd5e&amp;scene=4#wechat_redirect">jspatch 实现原理详解 &lt;三&gt; 扩展</a>

<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&amp;mid=2247483662&amp;idx=4&amp;sn=03f7fcdb54ebc8cc49995bf690292ebb&amp;scene=4#wechat_redirect">jspatch 实现原理详解 &lt;四&gt; 新特性</a>

<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&amp;mid=2247483662&amp;idx=5&amp;sn=22c304b6534b17c2ef36ee0afaa7576e&amp;scene=4#wechat_redirect">jspatch 实现原理详解 &lt;五&gt; 优化</a>

这些文章是对 jspatch 内部实现原理和细节诸如“require实现”、“property实现”、“self/super 关键字”、“nil处理”、“内存问题”等具体设计思路和解决方案的阐述,并没有对 jspatch 源码进行解读。在未接触源码、不清楚整个热修复流程的情况下去读这几篇文章难免一头雾水,最好的方法是边读源码边对照上述文章,代码中不理解的地方可以去文章中寻找答案。

本文将从一个小demo入手,跟踪代码执行流程,从cocoa层、javascript层、native层对热修复流程中涉及到的重要步骤和函数进行解析。

引入jspatch,jspatch 核心部分只有三个文件,十分精巧:

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

建立一个小demo,在<code>viewcontroller</code>屏幕中央放置一个button,button 点击事件为空:

热修复js文件(main.js)内容就是添加这个点击事件(弹出一个<code>alertview</code>):

<code>didfinishlaunchingwithoptions:</code>中开启 jspatch 引擎、执行 js 脚本:

修复成功!

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

该方法向<code>jscontext</code>环境注册了一系列供js调用oc方法的block,这些 block 内部大多是 调用 <code>runtime</code> 相关接口的 static 函数。最终读取<code>jspatch.js</code>中的代码到<code>jscontext</code>环境,使得<code>main.js</code>可以调用<code>jspatch.js</code>中定义的方法。

调用关系大致如下:

源码解读:

一张图总结 jspatch 的功能结构:

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

接下来读取<code>main.js</code>代码后执行:

该接口并非直接将<code>main.js</code>代码提交到<code>jscontext</code>环境执行,而是先调用<code>_evaluatescript: withsourceurl:</code>方法对<code>main.js</code>原始代码做些修改。

断点调试看一下<code>script</code>经正则处理之后的结果:

除了添加一些关键字和异常处理外,最大的变化在于所有函数调用变成了<code>__c("function")</code>的形式。据作者讲这是<code>jspatch</code>开发过程中最核心的问题,该问题的解决方案也是<code>jspatch</code>中最精妙之处。

我们进行热修复期望的效果是这样:

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

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

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

一种解决方案是实现所有js类继承机制,每一个类和方法都事先定义好:

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

这种方案是不太现实的,为了调用某个方法需要把该类的所有方法都引进来,占用内存极高(<code>nsobject</code>类有将近1000个方法)。

作者最终想出了第二种方案:

在 oc 执行 js 脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个 js 脚本,做到了类似 oc/lua/ruby 等的消息转发机制。
iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

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

<code>_methodfunc()</code> 把相关信息传给oc,oc用 runtime 接口调用相应方法,返回结果值,这个调用就结束了。

原脚本代码经过正则处理后交由<code>jscontext</code>环境去执行:

回过头看<code>main.js</code>的代码(处理后的):

参数依次为类名、实例方法列表、类方法列表。阅读<code>global.defineclass</code>源码会发现<code>defineclass</code>首先会分别对两个方法列表调用<code>_formatdefinemethods</code>,该方法参数有三个:方法列表(js对象)、空js对象、真实类名:

该段代码遍历方法列表对象的方法名,向js空对象中添加属性:方法名为键,一个数组为值。数组第一个元素为对应实现函数的参数个数,第二个元素是方法的具体实现。也就是说,<code>_formatdefinemethods</code>将 <code>defineclass</code>传递过来的js对象进行了修改:

1. 为什么要传递参数个数?

因为<code>runtime</code>修复类的时候无法直接解析js实现函数,也就无法知道参数个数,但方法替换的过程需要生成方法签名,所以只能从js端拿到js函数的参数个数,并传递给oc。

2. 为什么要修改方法实现?

<code>args.splice(0,1)</code>删除前两个参数:

oc中进行消息转发,前两个参数是<code>self</code>和<code>selector</code>,实际调用js的具体实现的时候,需要把这两个参数删除。

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

回到<code>defineclass</code>,调用<code>_formatdefinemethods</code>之后,拿着要重写的类名和经过处理的js对象,调用<code>_oc_defineclass</code>,也就是oc端定义的block方法。

<code>jpengine</code>中的<code>defineclass</code>对类进行真正的重写操作,将类名、<code>selector</code>、方法实现(imp)、方法签名等<code>runtime</code>重写方法所需的基本元素提取出来。

由源码可见,方法名、实现等处理好之后最终执行<code>overridemethod</code>方法。

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

该函数做的事情比较多,一张图概括如下:

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结

4.向class添加名为orig+selector,对应原始selector的imp。

这一步是为了让js通过这个方法调用原来的实现。

5.向class添加名为<code>origforwardinvocation</code>的方法,实现是原始的<code>forwardinvocation</code>的imp。

这一步是为了保存<code>forwardinvocation</code>的旧有实现,在新的实现中做判断,如果转发的方法是欲改写的,就走新逻辑,反之走原来的流程。

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

经过上一步处理,<code>handle:</code>直接走<code>objc_msgforward</code>进行消息转发环节。当点击button,调用<code>handle:</code>的时候,函数调用的参数会被封装到<code>nsinvocation</code>对象,走到<code>forwardinvocation</code>方法。上一步中<code>forwardinvocation</code>方法的实现替换成了<code>jpforwardinvocation</code>,负责拦截系统消息转发函数传入的<code>nsinvocation</code>并从中获取到所有的方法执行参数值,是实现替换和新增方法的核心。

接下来执行js中定义的方法实现。“修复 step 2”中已经讨论过,现在main.js中所有的函数都被替换成名为<code>__c('methodname')</code>的函数调用,<code>__c</code>调用了<code>_methodfunc</code>函数,<code>_methodfunc</code>会根据方法类型调用<code>_oc_call</code>:

<code>_oc_calli</code>或<code>_oc_callc</code>最终都会调用一个<code>static</code>函数<code>callselector</code>。

<code>main.js</code>中类似<code>uialertview.alloc().init()</code>实际是通过<code>callselector</code>调用 oc 的方法。

将 js 对象和参数转化为 oc 对象;

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

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

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

jspatch 基于<code>javascriptcore.framework</code>和objective-c中的runtime技术。

采用 ios7 后引入的 <code>javascriptcore.framework</code>作为 javascript 引擎解析js脚本,执行js代码并与oc端代码进行桥接。

使用objective-c <code>runtime</code>中的<code>method swizzling</code>方式达到使用js脚本动态替换原有oc方法的目的,并利用<code>forwardinvocation</code>消息转发机制使得在js脚本中调用oc的方法成为可能。

iOS 热更新解读(二)—— JSPatch 源码解析JSPatch 使用流程修复 step 1:startEngine修复 step 2:__c()元函数修复 step 3:global.defineClass修复 step 4:_OC_defineClass修复 step 5:overrideMethod调用 step 1:JPForwardInvocation调用 step 2:callSelector总结
相关文章

<a href="https://yq.aliyun.com/articles/58875">ios 热更新解读(一)apatch &amp; javascriptcore</a>

<a href="https://yq.aliyun.com/articles/58873">ios热更新解读(三)—— jspatch 之于 swift</a>

继续阅读