javascript
JSPatch近期新特性解析
JSPatch在社區的推動下不斷在優化改善,這篇文章總結下這幾個月以來 JSPatch 的一些新特性,以及它們的實現原理。
performSelectorInOC
JavaScript 語言是單線程的,在 OC 使用 JavaScriptCore 引擎執行 JS 代碼時,會對 JS 代碼塊加鎖,保證同個 JSContext 下的 JS 代碼都是順序執行。所以調用 JSPatch 替換的方法,以及在 JSPatch 里調用 OC 方法,都會在這個鎖里執行,這導致三個問題:
JSPatch替換的方法無法并行執行,如果如果主線程和子線程同時運行了 JSPatch 替換的方法,這些方法的執行都會順序排隊,主線程會等待子線程的方法執行完后再執行,如果子線程方法耗時長,主線程會等很久,卡住主線程。
某種情況下,JavaScriptCore 的鎖與 OC 代碼上的鎖混合時,會產生死鎖。
UIWebView 的初始化會與 JavaScriptCore 沖突。若在 JavaScriptCore 的鎖里(第一次)初始化 UIWebView 會導致 webview 無法解析頁面。
為解決這些問題,JSPatch 新增了 .performSelectorInOC(selector, arguments, callback) 接口,可以在執行 OC 方法時脫離 JavaScriptCore 的鎖,同時又保證程序順序執行。
舉個例子:
defineClass('JPClassA', {methodA: function() {//run in mainThread},methodB: function() {//run in childThreadvar limit = 20;var data = self.readData(limit);var count = data.count();return {data: data, count: count};} })上述例子中若在主線程和子線程同時調用 -methodA 和 -methodB,而 -methodB 里self.readData(limit) 這句調用耗時較長,就會卡住主線程方法 -methodA 的執行,對此可以讓這個調用改用 .performSelectorInOC() 接口,讓它在 JavaScriptCore 鎖釋放后再執行,不卡住其他線程的 JS 方法執行:
defineClass('JPClassA', {methodA: function() {//run in mainThread},methodB: function() {//run in childThreadvar limit = 20;return self.performSelectorInOC('readData', [limit], function(ret) {var count = ret.count();return {data: ret, count: count};});} })這兩份代碼在調用順序上的區別如下圖:
第一份代碼對應左邊的流程圖,-methodB 方法被替換,當 OC 調用到 -methodB 時會去到 JSPatch 核心的 JPForwardInvocation 方法里,在這里面調用 JS 函數 -methodB,調用時 JavascriptCore 加鎖,接著在 JS 函數里做這種處理,調用 reloadData() 函數,進而去到 OC 調用 -reloadData 方法,這時 -reloadData 方法是在 JavaScriptCore 的鎖里調用的。直到 JS 函數執行完畢 return 后,JavaScriptCore 的才解鎖,結束本次調用。
第二份代碼對應右邊的流程圖,前面是一樣的,調用 JS 函數 -methodB,JavaScriptCore 加鎖,但 -methodB 函數在調用某個 OC 方法時(這里是reloadData()),不直接去調用,而是直接 return 返回一個對象 {obj},這個{obj}的結構如下:
{ __isPerformInOC:1, obj:self.__obj, clsName:self.__clsName, sel: args[0], args: args[1], cb: args[2] }JS 函數返回這個對象,JS 的調用就結束了,JavaScriptCore 的鎖也就釋放了。在 OC 可以拿到 JS 函數的返回值,也就拿到了這個對象,然后判斷它是否 __isPerformInOC=1 對象,若是就根據對象里的 selector / 參數等信息調用對應的 OC 方法,這時這個 OC 方法的調用是在 JavaScriptCore 的鎖之外調用的,我們的目的就達到了。
執行 OC 方法后,會去調 {obj} 里的的 cb 函數,把 OC 方法的返回值傳給 cb 函數,重新回到 JS 去執行代碼。這里會循環判斷這些回調函數是否還返回 __isPerformInOC=1 的對象,若是則重復上述流程執行,不是則結束。
整個原理就是這樣,相關代碼在 這里 和 這里,實現起來其實挺簡單,也不會對其他流程和邏輯造成影響,就是理解起來會有點費勁。
performSelectorInOC 文檔里還有關于死鎖的例子,有興趣可以看看。
可變參數方法調用
一直以來這樣參數個數可變的方法是不能在 JSPatch 動態調用的:
- (instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message delegate:(nullable id)delegate cancelButtonTitle:(nullable NSString *)cancelButtonTitle otherButtonTitles:(nullable NSString *)otherButtonTitles, ...原因是 JSPatch 調用 OC 方法時,是根據 JS 傳入的方法名和參數組裝成 NSInvocation 動態調用,而 NSInvocation 不支持調用參數個數可變的方法。
后來 @wjacker 換了種方式,用 objc_msgSend 的方式支持了可變參數方法的調用。之前一直想不到使用 objc_msgSend 是因為它不適用于動態調用,在方法定義和調用上都是固定的:
1.定義
需要事先定義好調用方法的參數類型和個數,例如想通過 objc_msgSend 調用方法
- (int)methodWithFloat:(float)num1 withObj:(id)obj withBool:(BOOL)flag那就需要定義一個這樣的c函數:
int (*new_msgSend)(id, SEL, float, id, BOOL) = (int (*)(id, SEL, float, id, BOOL)) objc_msgSend;才能通過 new_msgSend 調用這個方法。而這個過程是無法動態化的,需要編譯時確定,而各種方法的參數/返回值類型不同,參數個數不同,是沒辦法在編譯時窮舉寫完的,所以不能用于所有方法的調用。
而對于可變參數方法,只支持參數類型和返回值類型都是 id 類型的方法,已經可以滿足大部分需求,所以讓使用它變得可能:
id (*new_msgSend1)(id, SEL, id,...) = (id (*)(id, SEL, id,...)) objc_msgSend;這樣就可以用 new_msgSend1 調用固定參數一個,后續是可變參數的方法了。實際上在模擬器這個方法也可以支持固定參數是N個id的方法,也就是已經滿足我們調用可變參數方法的需求了,但根據@wjacker 和 @Awhisper 的測試,在真機上不行,不同的固定參數都需要給它定義好對應的函數才行,官網文檔對這點略有說明。于是,多了一大堆這樣的定義,以應付1-10個固定參數的情況:
id (*new_msgSend2)(id, SEL, id,id,...) = (id (*)(id, SEL, id,id,...)) objc_msgSend; id (*new_msgSend3)(id, SEL, id,id,id,...) = (id (*)(id, SEL, id,id,id,...)) objc_msgSend; id (*new_msgSend4)(id, SEL, id,id,id,id,...) = (id (*)(id, SEL, id,id,id,id,...)) objc_msgSend; ...2.調用
解決上述參數類型和個數定義問題后,還有調用的問題,objc_msgSend 不像 NSInvocation 可以在運行時動態添加組裝傳入的參數個數,objc_msgSend 則需要在編譯時確定傳入多少個參數。這對于1-10個參數的調用,不得不用 if else 寫10遍調用語句,另外根據方法定義的固定參數個數不一樣,還需要調用不同的 new_msgSend 函數,所以需要寫10!條調用,于是有了這樣的大長篇(gist代碼)。后來用宏格式化了一下,會好看一點。
defineProtocol
JSPatch 為一個類新增原本 OC 不存在的方法時,所有的參數類型都會定義為 id 類型,這樣實現是因為這種在 JS 里新增的方法一般不會在 OC 上調用,而是在 JS 上用,JS 可以認為一切變量都是對象,沒有類型之分,所以全部定義為 id 類型。
但在實際使用 JSPatch 過程中,出現了這樣的需求:在 OC 里 .h 文件定義了一個方法,這個方法里的參數和返回值不都是 id 類型,但是在 .m 文件中由于疏忽沒有實現這個方法,導致其他地方調用這個方法時找不到這個方法造成 crash,要用 JSPatch 修復這樣的 bug,就需要 JSPatch 可以動態添加指定參數類型的方法。
實際上如果在 JS 用 defineClass() 給類添加新方法時,通過某些接口把方法的各參數和返回值類型名傳進去,內部再做些處理就可以解決上述問題,但這樣會把 defineClass 接口搞得很復雜,不希望這樣做。最終 @Awhisper 想出了個很好的方法,用動態新增 protocol 的方式支持。
首先 defineClass 是支持 protocol 的:
defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {})這樣做的作用是,當添加 Protocol 里定義的方法,而類里沒有實現的方法時,參數類型不再全是 id,而是會根據 Protocol 里定義的參數類型去添加。
于是若想添加一些指定參數類型的方法,只需動態新增一個 protocol,定義新增的方法名和對應的參數類型,再在 defineClass 定義里加上這個 protocol 就可以了。這樣的不污染 defineClass() 的接口,也沒有更多概念,十分簡潔地解決了這問題。范例:
defineProtocol('JPDemoProtocol',{stringWithRect_withNum_withArray: {paramsType:"CGRect, float, NSArray*",returnType:"id",}, }defineClass('JPTestObject : NSObject <JPDemoProtocol>', {stringWithRect_withNum_withArray:function(rect, num, arr){//use rect/num/arr params herereturn @"success";}, }具體實現原理原作者已寫得挺清楚,參見這里。
支持重寫dealloc方法
之前 JSPatch 不能替換 -dealloc 方法,原因:
1.按之前的流程,JS 替換 -dealloc 方法后,調用到 -dealloc 時會把 self 包裝成 weakObject 傳給 JS,在包裝的時候就會出現以下 crash:
Cannot form weak reference to instance (0x7fb74ac26270) of class JPTestObject. It is possible that this object was over-released, or is in the process of deallocation.意思是在 dealloc 過程中對象不能賦給一個 weak 變量,無法包裝成一個 weakObject 給 JS。
2.若在這里不包裝當前調用對象,或不傳任何對象給 JS,就可以成功執行到 JS 上替換的 dealloc 方法。但這時沒有調用原生 dealloc 方法,此對象不會釋放成功,會造成內存泄露。
-dealloc 被替換后,原 -dealloc 方法 IMP 對應的 selector 已經變成 ORIGdealloc,若在執行完 JS 的 dealloc 方法后再強制調用一遍原 OC 的 ORIGdealloc ,會crash。猜測原因是 ARC 對 -dealloc 有特殊處理,執行它的 IMP(也就是真實函數)時傳進去的 selectorName 必須是 dealloc,runtime 才可以調用它的 [super dealloc],做一些其他處理。
到這里我就沒什么辦法了,后來 @ipinka 來了一招欺騙 ARC 的實現,解決了這個問題:
1.首先對與第一個問題,調用 -dealloc 時 self 不包裝成 weakObject,而是包裝成 assignObject 傳給 JS,解決了這個問題。
2.對于第二個問題,調用 ORIGdealloc 時因為 selectorName 改變,ARC 不認這是 dealloc 方法,于是用下面的方式調用:
Class instClass = object_getClass(assignSlf); Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc")); void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));做的事情就是,拿出 ORIGdealloc 的 IMP,也就是原 OC 上的 dealloc 實現,然后調用它時 selectorName 傳入 dealloc,這樣 ARC 就能認得這個方法是 dealloc,做相應處理了。
擴展
JPCleaner即時回退
有些 JSPatch 使用者有這樣的需求:腳本執行后希望可以回退到沒有替換的狀態。之前我的建議使用者自己控制下次啟動時不要執行,就算回退了,但還是有不重啟 APP 即時回退的需求。但這個需求并不是核心功能,所以想辦法把它抽離,放到擴展里了。
只需引入 JPCleaner.h,調用 +cleanAll 接口就可以把當前所有被 JSPatch 替換的方法恢復原樣。另外還有 +cleanClass: 接口支持只回退某個類。這些接口可以在 OC 調用,也可以在 JS 腳本動態調用:
[JPCleaner cleanAll] [JPCleaner cleanClass:@“JPViewController”];實現原理也很簡單,在 JSPatch 核心里所有替換的方法都會保存在內部一個靜態變量 _JSOverideMethods 里,它的結構是 _JSOverideMethods[cls][selectorName] = jsFunction。我給 JPExtension 添加了個接口,把這個靜態變量暴露給外部,遍歷這個變量里保存的 class 和 selectorName,把 selector 對應的 IMP 重新指向原生 IMP 就可以了。詳見源碼。
JPLoader
JSPatch 腳本需要后臺下發,客戶端需要一套打包下載/執行的流程,還需要考慮傳輸過程中安全問題,JPLoader 就是幫你做了這些事情。
下載執行腳本很簡單,這里主要做的事是保證傳輸過程的安全,JPLoader 包含了一個打包工具 packer.php,用這個工具對腳本文件進行打包,得出打包文件的 MD5,再對這個MD5 值用私鑰進行 RSA 加密,把加密后的數據跟腳本文件一起大包發給客戶端。JPLoader 里的程序對這個加密數據用私鑰進行解密,再計算一遍下發的腳本文件 MD5 值,看解密出來的值跟這邊計算出來的值是否一致,一致說明腳本文件從服務器到客戶端之間沒被第三方篡改過,保證腳本的安全。對這一過程的具體描述詳見舊文 JSPatch部署安全策略。對 JPLoader 的使用方式可以參照 wiki 文檔
總結
以上是生活随笔為你收集整理的JSPatch近期新特性解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分类条件概率
- 下一篇: cocos2d-x 关于tilemap滚