天天看點

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>

繼續閱讀