如何定位 Hybrid Web 頁面中 Native 注入的 JS 代碼
一個網頁除了可以運行在公共的瀏覽器上,也可以運行在 APP 端內的 WebView 組件上。由于這些 Hybrid Web 網頁運行在一個相對封閉的環境里,所以 APP 本身可以向 WebView 中注入一些 JS 代碼,對 Web 頁面做定向增強(最典型的運用就是 JSBridge,提供了一道 Web <--> Native 通信的橋梁)。
在絕大多數情況下,業務開發并不需要感知這些 Native 注入的代碼,但是在一些 性能優化/鏈路排查 的情況下,就需要感知這些 Native 注入代碼的時機和運行情況了,從而更好的定位問題。
由于 Chrome/Safari 的 debug 調試工具基本上是為 純 Web 服務的,而且這個需求很小眾,所以這個能力支持的并不是很好。這個小需求網絡上沒什么總結性的文章,ChatGPT 回答的也差強人意,正好這段時間也做了一些相關的工作,所以順勢就記下來,幫助某個有緣人。
直接查看 Native 代碼
如果你對 Native WebView 的封裝代碼很熟悉,或者有一定的 Native 經驗,直接閱讀源碼是最快的方式。這里我說幾個最常用的 JS 注入 API:
iOS
iOS 主要關注這 3 個 API:
addScriptMessageHandler[1]
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler
name:(NSString *)name;
通過這個方法可以給WKWebView環境中添加一個指定 name 的 JS 對象,前端可以調用該對象的 postMessage 方法,向客戶端發送消息。前端類似于這樣調用:
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
addUserScript[2]
// WKUserContentController
- (void)addUserScript:(WKUserScript *)userScript;
可以用這個函數注入 JS 腳本字符串到 WKWebView 中。
evaluateJavaScript[3]
// WKWebView
- (void)evaluateJavaScript:(NSString *)javaScriptString
completionHandler:(void (^)(id, NSError *error))completionHandler;
這個函數也可以在 WKWebView 上下文中運行一段 JS 代碼。
其實還有很多注入函數,但常用的就這 3 個,其它的函數就是和他們有些細微的差別,感興趣的可以直接看官方文檔。
另外還需注意的是 JS 代碼注入的時機,頁面加載前還是頁面加載后注入代碼,帶來的影響可能是大不一樣的。而且這個 API 也特別多,可參考文檔:WKNavigationDelegate[4],重點關注 didStartProvisionalNavigation[5] 和 didFinishNavigation[6]。
圖片
https://bbs.huaweicloud.com/blogs/331397
Android
Android 主要關注這 2 個 API:
addJavascriptInterface[7]
/** Instantiate the interface and set the context. */class WebAppInterface(private val mContext: Context) { // 通過 @JavascriptInterface 注解,向 WebView 暴露 showToast 方法 @JavascriptInterface fun showToast(toast: String) { Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show() }}val webView: WebView = findViewById(R.id.webview)// "Android" 將會暴露在 Webview 的 window 變量上webView.addJavascriptInterface(WebAppInterface(this), "Android")
然后前端直接調用即可:
<input type="button" value="Say hello" notallow="showAndroidToast('Hello Android!')" /><script type="text/javascript"> function showAndroidToast(toast) { window.Android.showToast(toast); }</script>
evaluateJavascript[8]
public void evaluateJavascript (String script, ValueCallback<String> resultCallback)
類似于 iOS,也是在 WebView 上下文中運行一段 JS 腳本。
同樣的,Android 也要注意 JS 代碼注入的時機。API 太多了,參考這個鏈接:WebViewClient#public-methods[9],重點關注 onPageStarted[10] 和 onPageFinished[11]。
有能力閱讀 Native 源代碼是最理想的情況,但是現實一般很殘酷:
- 絕大部分前端同學不懂 Native 代碼
- 現在還存留的高流量 APP,基本都迭代 5 年以上了,代碼一層一層糊成 ?? 山,老師傅都得在里面繞半天
- 降本增笑,老師傅都沒了
所以其實還有第 2 種基于觀測的方案:利用 WebView Devtool 工具定位。
用 Web 調試工具定位
前面也說了,查看注入的 JS 代碼是一個很小眾的需求,所以調試工具并沒有提供獨立的查看面板,所有的能力都是拼拼湊湊起來的,而且部分能力 iOS 和 Android 互為補集 ??,整體上還有有些凌亂的。
如何遠程調試 Web 頁面,可以參考這篇文章:各種「真機遠程調試」方法匯總[12]
想遠程調試 APP WebView 網頁,需要在 Native 層開啟調試能力
- iOS:16.4 以上版本需要設置 webView.isInspectable = true[13] 開啟遠程調試
- Android:設置 setWebContentsDebuggingEnabled(true)[14] 開啟遠程調試
Common
iOS 和 Android 都通用的方案有這么兩種:
Debug
我們可以通過 debug 到關鍵代碼,然后查看調用棧,找到注入代碼:
iOS 利用 Safari Devtool 調試,查看方式如下:
iOS Safari Debug
Android 利用 Chrome Devtool 調試,查看方法如下:
Log
還有一種方法是在 Native 注入的 JS 代碼中,加入 console.log 的調用,這樣在注入代碼運行的時候,可以從 Console 面板的資源引用找到注入腳本。
但是這個問題有個悖論:
- 一般注入的腳本為了不增加運行時性能負擔,是不會加 log 調用的,所以一般沒法用
- 如果開發者主動去注入的 JS 代碼中加入 log,那說明他有一定的 native 經驗,那為什么不直接看 native 代碼呢?
所以這樣方法更像一種輔助方案,用來配合其它方案一起排查問題。
iOS
iOS 有兩種方式看注入的代碼。
第一種是在「來源」的「附加腳本」里,可以在這里看到 Native 通過 addUserScript 注入的腳本,整整齊齊的,還是比較方便查看的。但有個問題是這里并不會列出 evaluateJavaScript 注入的代碼。
iOS 附加腳本
這里就介紹第二種方法,那就是「全局搜索」。
Safari 的全局搜索功能,可以同時搜索 addUserScript/evaluateJavaScript/正常加載的資源 里的代碼,所以如果你知道注入代碼的一些關鍵信息,可以通過搜索的方式定位代碼。
iOS 全局搜索
Android
Android 這里使用 Chrome Devtool 查看注入代碼。用了這么久的 Chrome Devtool,我是第一次發現它做的不如 Safari 的地方,那就是上面 Safari Devtool 有的東西它都沒有。
但是 Chrome Devtool Performance 可以曲線救國一下,我們可以通過性能錄制得到一份性能分析火焰圖,然后查看主線程的代碼執行情況,一般是 Evalutae Script 階段,然后再定位可能的執行時機,通過點擊 Bar 展開的 Summary 面板,一般有個 VM 開頭的文件,打開就可顯示注入的 JS 代碼:
Chrome 火焰圖
總結
從上面內容可以看出定位「注入 JS 代碼」還是挺麻煩的,需要用各種手段曲線救國,而且很多情況下各個技巧需要交叉使用才能定位到。希望我這篇文章可以幫助到一些開發者,減少 debug 內耗的時間。