JSPatch 開源以來大部分被用於 hotfix,替換原生方法修復線上bug,但實際上 JSPatch 一直擁有動態添加功能模塊的能力,由於 JSPatch 能夠建立和調用任意 OC 類和方法,徹底能夠用 JSPatch 寫功能模塊,而後動態下發加載。只是以前在性能和開發體驗上有些問題,尚未太多這方面的應用。此次 JSPatch 作了較大的更新,掃除這些問題,讓用純 JS 寫功能模塊變得實用。這裏有個用 JS 寫的 Dribbble 客戶端 Demo,能夠體驗下效果。git
來看看此次更新作了什麼。github
經過工具能夠看到使用 JSPatch 寫功能模塊時,耗時較多的點在於 JS 和 OC 的通訊,以及通訊過程當中參數的轉換,因而在這塊尋找優化點。寫功能時須要新增不少類和方法,例如:數組
defineClass('JPDribbbleView:UIView', { renderItem: function(item) { ... }, }) defineClass('JPDribbbleViewController:UIViewController', { render: function(){ var view = JPDribbbleView.alloc().init(); view.renderItem(item); } });
上面兩個都是新增的類,兩個方法也是新增的,按以前的流程,這裏的定義會傳入 OC,在 OC 生成這兩個類,並在這個類上添加這裏定義的方法,調用時進入 OC 尋找這些方法調用。性能優化
例如上面的 view.renderItem(item)
這句,調用流程是:框架
進入 __c 函數 -> 轉換參數item類型(JS-OC) -> 進入 OC -> callSelector -> 字符串轉 Class & Selector 並調用 -> 進入 JPForwardInvocation -> 包裝參數 & 轉換類型(OC-JS) -> 調用 JS 上的 renderItem 方法 -> 轉換返回值類型(OC-JS)。函數
一個簡單的方法調用要通過這麼多處理,對於 hotfix 來講這樣的新方法調用不常見,調用次數也少,但若是用於作業務模塊,就會有大量這種新方法的調用,影響性能。實際上這麼多處理都是沒必要要的,在 JS 定義的方法還要跑到 OC 繞一圈再回來調用,因此優化思路很明顯,就是不要繞去 OC,直接在 JS 調用。工具
通過一輪優化,更新後的 JSPatch 上述的調用流程變成:性能
進入 __c 函數 -> 調用 JS renderItem 方法。學習
很清新的流程,去除了全部多餘的處理,極大地提升了性能。實現原理就是在 JS 端用一個表保存 className 對應的 JS 方法,調用時若是在這個表找到要調用的方法,就直接調用,再也不去到 OC,細節上能夠直接看代碼。測試
這個優化不須要使用者作什麼修改,書寫這些方法的接口並無變,跟原來同樣。實際上實現過程當中碰到最大的問題就是接口問題,有機會再分享下這個過程。
那麼通過此次優化,這種調用性能提升多少呢?
經測試,不帶參數的方法調用提升45倍
,帶一個普通參數的方法調用提升70倍
,帶像 NSDictionary / NSArray 這些須要轉換的參數時提升700倍
。測試用例能夠在 Demo 工程找到。
以前 JSPatch 給類新增 property 是經過 -getProp:
, -setProp:forKey:
這兩個接口:
defineClass('JPTableViewController : UITableViewController', { dataSource: function() { return self.getProp('data'); }, setup: function() { self.setProp_forKey([1,2,3], 'data') } }
這裏有兩個問題:
接口不友好,與 OC 原生property 的寫法不一致,寫起來彆扭。
每一個 property 都是 OC 裏的一個 associatedObject,每次存取都要與 OC 通訊,大量調用時性能低。
對於hotfix,不多有新增 property 的需求,接口挫點不要緊,但如果用來寫新功能,property 是屢見不鮮,就得好好優化了。 此次更新後,能夠這樣新增property:
defineClass('JPTableViewController : UITableViewController', [ 'data', 'name', ], { dataSource: function() { return self.data(); }, setup: function() { self.setData([1,2,3]) } }
接口上作到跟原有 property 一致,解決第一個問題。
對於第二個問題,具體實現上再也不是一個 property 對應一個 associatedObject
,而是每一個對象只有一個對應的 associatedObject
,這個 associatedObject
的值是一個 JS 對象,每個 property 都存在這個JS對象上。
如圖,左邊是修改以前的,右邊是修改後的。修改前每個 property 都單獨保存在 OC,一個 property 對應一個 associatedObject
,JS 經過接口去存取。修改後一個 OC 對象只有一個 associatedObject
,這個 associatedObject
是個 JS 對象,全部 property 集中在這個 JS 對象裏面,JS 能夠直接對它進行存取操做。
這樣作的好處在於在存取 property 時減小了 JS 與 OC 的通訊,不須要每次都與 OC 通訊,只須要第一次取出這個關聯對象,後續對全部 property 的存取操做都是在 JS 內部進行,提升了性能。這個主意來自老郭(samurai-native做者)的腦洞,在此感謝~
通過上述優化,defineClass()
裏方法調用的性能是提升了,但像數據層的 dataSource / manager 這些不須要依賴 OC 的類也使用 defineClass()
定義仍是會比較浪費,由於定義後會生成對應的 OC 類,並在 alloc
時仍是要去到 OC 生成這個對象,property 的存取仍是要經過 associatedObject
,這些都是不必的。
這種類型的類與 OC 沒有聯繫,不須要繼承 OC 類,只在 JS 使用,因此直接使用 JS 原生類就好了,能夠減小上述性能上的浪費。只是 JS 原生類定義和對象生成的那套寫法與 defineClass()
的寫法相去甚遠,兩種風格混在一塊兒開發體驗不太好,因而加了個 defineJSClass()
接口,輔助建立純 JS 類:
defineJSClass('DBDataSource', { init: function(){ this.data = 'xxx'; return this; }, readData: function() { this.super().loadData(); return this.data; } }, { shareInstance: function(){ ... } }) var dataSource = DBDataSource.alloc().init(); DBDataSource.shareInstance();
能夠看到 defineJSClass()
的寫法與 defineClass()
幾乎徹底同樣,包括實例方法/類方法/繼承的寫法/super調用/對象生成都是同樣的,只有有兩個地方不一樣:
用 this 關鍵字代替 self
property 不用 getter/setter,直接存取。
這種方式定義類和使用是比 defineClass()
性能高的,推薦不須要繼承 OC 類時都用這個接口。
還有一個棘手問題,也是使用 JSPatch 時最讓人迷惑的一點,就是 NSDictionary
/ NSArray
/ NSString
這幾個類型與 JS 原生 Object
/ Array
/ String
互轉的問題。
以數組爲例,OC NSArray
數組傳回給 JS 時都會當成一個普通的 OC 對象,能夠調用它的 OC 方法(像 -objectAtIndex
),而 JS 上建立的數組是 JS 數組,能夠用 []
取數組元素,不能調用 OC 方法。JS 數組傳入 OC 會自動轉爲 NSArray
,傳出來會變成 NSArray
。因而在 JS 端數組就會有兩種類型,你知道某個變量是數組後,還須要知道它是從哪裏來的,以此判斷它的類型,再用相應的方法。
初期這樣作是爲了保持功能的完整性,像 NSMutableDictionary
/ NSMutableArray
/ NSMutableString
若是傳到 JS 時自動轉爲 JS 類型就無法對這個對象進行修改了。
好在對於 hotfix 來講問題還不算大,由於代碼量小很容易看出來源判斷它的類型,但對於寫功能模塊,這裏就很容易會被繞暈了。因而加了個開關 autoConvertOCType()
,能夠自由開啓和關閉自動類型轉換。只要在 JS 腳本開頭加上 autoConvertOCType(1)
這句調用,上述幾個類型在通訊過程當中都會自動轉爲 JS 類型,在 JS 上再也不存在兩種類型,只有一種 JS 類型,無需多考慮,這樣開發起來就輕鬆多了。
那若須要調用這些類型 OC 對象的一些方法時怎麼辦?在調用先後先關後開便可:
autoConvertOCType(0) var str = NSString.stringWithString('xx'); var data = str.dataUsingEncoding(4); autoConvertOCType(1)
此次更新還包括完善了 super
的調用,解決某些狀況下調用 super
死循環的問題。另外原先放在擴展的 include()
接口合入做爲核心功能提供,會自動把主腳本所在目錄做爲根目錄去尋找 include 的文件路徑,並保證只 include 一次,還增長了 resourcePath()
接口用於靜態資源文件的獲取。
以前說到阻礙 JSPatch 用於動態更新的障礙有兩個:性能問題和開發效率,此次更新後 JSPatch 在這兩個方面都有所提高,接下來繼續在使用的過程當中挖掘更多的優化點,提供一些經常使用的靜態變量和方法封裝,並嘗試作 XCode 代碼自動補全插件提升開發效率。
如今能夠經過 JSPatch 用 JS 寫完整的功能模塊,再動態下發給 APP 執行,JSPatch 的這套動態化方案相對於 React Native 有如下優點:
小巧。只需引入 JPEngine.h
JPEngine.m
JSPatch.js
三個小文件,體積小巧,也無需搭建環境。
學習成本低。能夠繼續沿用原來 OC 的思惟寫程序,無需學習新一套規則,即刻上手。
限制少。能夠說徹底沒有限制,OC / JS 上玩出花的各類模式均可以照搬使用,不會被某一框架思惟和寫法限定。全部 OC / JS 庫直接使用,無需適配。
歡迎試用,github地址