日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

JSPatch近期新特性解析

發布時間:2025/4/5 javascript 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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近期新特性解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。