<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&mid=2247483662&idx=1&sn=c7d9ee27eff35688180bdc840d31120b&scene=4#wechat_redirect">jspatch 实现原理详解 <一> 核心</a>
<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&mid=2247483662&idx=2&sn=44b62a84a122886b08874861df83d889&scene=4#wechat_redirect">jspatch 实现原理详解 <二> 细节</a>
<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&mid=2247483662&idx=3&sn=9af2403895ff8e09bd7b7d767a34dd5e&scene=4#wechat_redirect">jspatch 实现原理详解 <三> 扩展</a>
<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&mid=2247483662&idx=4&sn=03f7fcdb54ebc8cc49995bf690292ebb&scene=4#wechat_redirect">jspatch 实现原理详解 <四> 新特性</a>
<a href="http://mp.weixin.qq.com/s?__biz=mzizntq2mdg2ng==&mid=2247483662&idx=5&sn=22c304b6534b17c2ef36ee0afaa7576e&scene=4#wechat_redirect">jspatch 实现原理详解 <五> 优化</a>
这些文章是对 jspatch 内部实现原理和细节诸如“require实现”、“property实现”、“self/super 关键字”、“nil处理”、“内存问题”等具体设计思路和解决方案的阐述,并没有对 jspatch 源码进行解读。在未接触源码、不清楚整个热修复流程的情况下去读这几篇文章难免一头雾水,最好的方法是边读源码边对照上述文章,代码中不理解的地方可以去文章中寻找答案。
本文将从一个小demo入手,跟踪代码执行流程,从cocoa层、javascript层、native层对热修复流程中涉及到的重要步骤和函数进行解析。
引入jspatch,jspatch 核心部分只有三个文件,十分精巧:
建立一个小demo,在<code>viewcontroller</code>屏幕中央放置一个button,button 点击事件为空:
热修复js文件(main.js)内容就是添加这个点击事件(弹出一个<code>alertview</code>):
<code>didfinishlaunchingwithoptions:</code>中开启 jspatch 引擎、执行 js 脚本:
修复成功!
该方法向<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 的功能结构:
接下来读取<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>中最精妙之处。
我们进行热修复期望的效果是这样:
但js 对于调用没定义的属性/变量,只会马上抛出异常,而不像 oc/lua/ruby 那样有转发机制。因此对于用户传入的js代码中,类似<code>uiview().alloc().init()</code>这样的代码,js其实根本没办法进行处理。
一种解决方案是实现所有js类继承机制,每一个类和方法都事先定义好:
这种方案是不太现实的,为了调用某个方法需要把该类的所有方法都引进来,占用内存极高(<code>nsobject</code>类有将近1000个方法)。
作者最终想出了第二种方案:
在 oc 执行 js 脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个 js 脚本,做到了类似 oc/lua/ruby 等的消息转发机制。
给 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的具体实现的时候,需要把这两个参数删除。
回到<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类中的方法实现。
该函数做的事情比较多,一张图概括如下:
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的方法成为可能。
相关文章
<a href="https://yq.aliyun.com/articles/58875">ios 热更新解读(一)apatch & javascriptcore</a>
<a href="https://yq.aliyun.com/articles/58873">ios热更新解读(三)—— jspatch 之于 swift</a>