<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>