JSPatch簡介
JSPatch 是一個開源項目(Github鏈接),只需要在項目里引入極小的引擎文件,就可以使用 JavaScript 調用任何 Objective-C 的原生接口,替換任意 Objective-C 原生方法。目前主要用于下發 JS 腳本替換原生 Objective-C 代碼,實時修復線上 bug。已超過 3500 個 App 在使用,成為 App 標配功能。
基礎原理
1、Objective-C 方面
Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的優勢在于:代碼時更具靈活性,我們可以把消息轉發給我們想要的對象,或者隨意交換一個方法的實現等。這種特性意味著需要一個運行時系統來執行編譯的代碼,它讓所有的工作可以正常的運行。這個運行時系統即 Objc Runtime。Objc Runtime 其實是一個Runtime庫,它基本上是用C和匯編寫的,這個庫使得C語言有了面向對象的能力。
要理解 Runtime 庫,首先要了解 Objective-C 類與對象基礎數據結構。類是由 Class 類型來表示的,它實際上是一個指向 objc_class 結構體的指針,定義可在 objc/runtime.h 中看到:
在這個定義中,這里只關注3個字段
1、ivars :存放屬性鏈表,記錄類實例的所有屬性定義。
2、methodLists :存放法樹鏈表,記錄類實例的所有方法實現指針。
3、cache:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據 isa 指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是 methodLists 中遍歷一遍,性能勢必很差。這時 cache 就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到 cache 列表中,下次調用的時候 Runtime 就會優先去 cache 中查找,如果 cache 沒有命中,才去 methodLists 中查找方法。這樣大大提高了調用的效率。
同時 objc/runtime.h 中還提供了大量的 API 來操作類與對象。類的操作方法大部分是以 class 為前綴的,而對象的操作方法大部分是以 objc 或 object_ 為前綴。這里我們只關注方法操作函數,如下:
1、class_addMethod:如果本類中包含一個同名的實現,則函數會返回 NO。如果要修改已存在實現,可以使用 method_setImplementation。
2、class_replaceMethod:該函數的行為可以分為兩種:如果類中不存在 name 指定的方法,則類似于 class_addMethod 函數一樣會添加方法;如果類中已存在 name 指定的方法,則類似于 method_setImplementation 一樣替代原方法的實現。
3、method_setImplementation:重置方法實現。
4、method_exchangeImplementations:交換方法實現。
在 Objective-C 中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是 SEL 的名字。每個類都有一個方法列表 methodLists ,存放著 SEL 的名字和 IMP 方法實現的映射關系如圖示。IMP 類似函數指針,指向具體的 Method 實現。
利用 Runtime 可以實現在運行時偷換 SEL 對應的方法實現 Method 或重置 IMP 方法實現,達到 hook 的目的。
以上也就是大名鼎鼎的黑魔法原理(Method Swizzling),常見的用法
當然 Object-C 還支持動態創建對象,動態添加方法,動態添加屬性(Object-C 中當類注冊完成后無法動態添加屬性,但可以用關聯方法來模擬屬性功能,屬性的本質是 get 和 set 方法)
2、JavaScriptCore 方面
前端開發的同學應該知道,瀏覽器核心模塊主要是渲染引擎和 JavaScript 引擎兩部分組成。前者用于處理頁面布局,渲染及 DOM 結構等,后者用于 JavaScript 的解析、執行及 DOM 交互等。JavaScriptCore 是一種 JavaScript 引擎,主要為 webkit 提供腳本處理能力(其主要以 safari 瀏覽器為代表)。除此之外,還有著名的 Jscript(IE), SpiderMonkey(firefox)和V8(chrome)。它提供了以下主要功能:
1、Objective-C –> JavaScript (即在 Objective-C 語言環境里執行 JavaScript 代碼段、方法,創建 JavaScript 變量及變量操作等等)執行 JavaScript 代碼的方法:首先引入 JavaScriptCore.h,然后通過 JSContext 創建 JS 運行環境,再通過 evaluateScript 來執行結果
需要注意 Objective-C 和 JS 數據類型之間的轉換表:
2、JavaScript –> Objective-C(即在 JavaScript 語言環境里調用 Objective-C 公開給 JavaScript 的方法)。有 JSExport 協議和 Block 兩種方式
3、內存管理和線程封裝(主要是需要注意引用和線程使用沖突)
當 JS 對象引用到 Object-C 對象(繼承了 JSExport 協議),而 Object-C 對象又引用到 JS 對象 時就會發生循環用(很少見的場景,即使真存在,也可以通過架構設計的方式來避免)
這個時候就需要使用到 JSManagerValue 包裝一下
實際代碼如下:
至于線程沖突就涉及到 JSVirtualMachine 的理解:其實每一個 JSVirtualMachine 都管理著一個 JavaScript 虛擬機(JSContext 的載體),它運行在 Object-C 中的一個獨立線程隊列,相同的 JSVirtualMachine 共用同一個線程隊列,不同的 JSVirtualMachine 當然也就處于不同的線程隊列,它們之間無法進行數據通訊,只能通過 Object-C 來做通訊中轉,所以會出現線程沖突問題。理解了這個關鍵點,解決沖突問題就容易了。
通常初始化 JSContext 環境都會加載在一個 JSVirtualMachine 虛擬機,即使不指定 JSVirtualMachine 對象,也會默認加載一個,如下圖示:
3、Object-C 和 JavaScript 之間的橋接
JSPatch 中兩者之間交互依托前面的數據類型轉換表,使用最簡單的字符串傳遞方式交互信息達到動態化的目的。一句話總結:JS 傳遞字符串給 OC,OC 通過 Runtime 接口調用和替換 OC 方法,這是最基礎的原理。如下圖示:
詳細的打怪漲經驗方式,還是去參考 bang 神的文檔,芝麻開門:
https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3
服務端原理
JSPatch 需要使用者有一個后臺可以下發和管理腳本,并且需要處理傳輸安全等部署工作,JSPatch 平臺幫你做了這些事,提供了腳本后臺托管,版本管理,保證傳輸安全等功能,讓你無需搭建一個后臺,無需關心部署操作。但還是需要了解一些服務端原理的。下載 JS 腳本只是簡單的 get 請求,這里要研究的是其中傳輸安全,灰度下發和回滾機制。
1、安全機制
這里就直接引用 bang 神的原文,如下圖示:
1、服務端計算出腳本文件的 MD5 值,作為這個文件的數字簽名。
2、服務端通過私鑰加密第 1 步算出的 MD5 值,得到一個加密后的 MD5 值。
3、把腳本文件和加密后的 MD5 值一起下發給客戶端。
4、客戶端拿到加密后的 MD5 值,通過保存在客戶端的公鑰解密。
5、客戶端計算腳本文件的 MD5 值。
6、對比第 4/5 步的兩個 MD5 值(分別是客戶端和服務端計算出來的 MD5 值),若相等則通過校驗。
只要通過校驗,就能確保腳本在傳輸的過程中沒有被篡改,因為第三方若要篡改腳本文件,必須計算出新的腳本文件 MD5 并用私鑰加密,客戶端公鑰才能解密出這個 MD5 值,而在服務端未泄露的情況下第三方是拿不到私鑰的。
JSPatch 平臺是用 PHP 實現的,這里筆者用 Node.js 仿照流程來模擬基礎實現原理。
運行效果如下:
這里為了看效果并沒有對 data 進行加密,實際生產環境中使用時,腳本需要進行版本管理,提交 PR-Review,通過以后才能通過服務獲取,而腳本內容也是需要進行 RSA 加密的,安全第一嘛。再配合上蘋果的 ATS 要求所有APP域名都支持HTTPS傳輸(此要求 delay 了),至此已經實現了安全傳輸機制。
2、灰度下發機制
灰度下發涉及到數據上傳,分析等,甚至有的公司都已經做到了大數據挖掘的程度,JSPatch 平臺支持按用戶數量、按條件(常被用來做新功能發布或線上調試)灰度下發,其中按條件還支持后臺動態配置條件,功能很強大。詳細的可以參考 http://www.jspatch.com/Docs/rule
3、回滾機制
這部分 JSPatch 平臺并沒有詳細說明,但目前實踐中大部分都是簡單粗暴地重傳:即已下發腳本出 bug 了,就再出一個 fixed patch,重新下發。但這種方式對于日活千萬上億的 APP,是不能容忍的。這里可以提供一種使用基于 git 版本開源項目管理的方式:每一次腳本下發都提交 PR-Review,Review 通過以后 merger 再下發,一旦出錯直接 git revert。Native 端緩存最新版本和上一個版本的 patch 補丁,共兩份,當檢測到回滾發生(服務端下發的版本標識小于 Native 端版本)時,把上一個版本的 patch 標識為最新,出錯的 patch 標識為歷史版本,完成回滾操作。
后記
使用 JSPatch 已有半年多時間了,從中收獲到很多,也踩過不少坑,比如:無法替換 main 函數之前執行的類方法 +(void)load; +(void)initialize; 等,無法調用被 hook 住的源方法,但都一一趟過了,總的來說還是一個非常強大商業化工具。有感興趣的小伙伴還是強烈推薦多多閱讀 bang 神的 Wiki,傳送門:https://github.com/bang590/JSPatch/wiki
希望本文能對準備接入或者學習 JSPatch 的開發人員有所幫助。