🛠️ [跨端开发] 如何定位 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

objectivec 复制代码
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler 
                           name:(NSString *)name;

通过这个方法可以给WKWebView环境中添加一个指定 name 的 JS 对象,前端可以调用该对象的 postMessage 方法,向客户端发送消息。前端类似于这样调用:

javascript 复制代码
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

addUserScript

objectivec 复制代码
// WKUserContentController
- (void)addUserScript:(WKUserScript *)userScript;

可以用这个函数注入 JS 脚本字符串到 WKWebView 中。

evaluateJavaScript

objectivec 复制代码
// WKWebView
- (void)evaluateJavaScript:(NSString *)javaScriptString 
         completionHandler:(void (^)(id, NSError *error))completionHandler;

这个函数也可以在 WKWebView 上下文中运行一段 JS 代码。

其实还有很多注入函数,但常用的就这 3 个,其它的函数就是和他们有些细微的差别,感兴趣的可以直接看官方文档。

另外还需注意的是 JS 代码注入的时机,页面加载前还是页面加载后注入代码,带来的影响可能是大不一样的。而且这个 API 也特别多,可参考文档:WKNavigationDelegate,重点关注 didStartProvisionalNavigationdidFinishNavigation

Android

Android 主要关注这 2 个 API:

addJavascriptInterface

kotlin 复制代码
/** 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")

然后前端直接调用即可:

html 复制代码
<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />

<script type="text/javascript">
  function showAndroidToast(toast) {
    window.Android.showToast(toast);
  }
</script>

evaluateJavascript

kotlin 复制代码
public void evaluateJavascript (String script, ValueCallback<String> resultCallback)

类似于 iOS,也是在 WebView 上下文中运行一段 JS 脚本。

同样的,Android 也要注意 JS 代码注入的时机。API 太多了,参考这个链接:WebViewClient#public-methods,重点关注 onPageStartedonPageFinished

有能力阅读 Native 源代码是最理想的情况,但是现实一般很残酷:

  • 绝大部分前端同学不懂 Native 代码
  • 现在还存留的高流量 APP,基本都迭代 5 年以上了,代码一层一层糊成 💩 山,老师傅都得在里面绕半天
  • 降本增笑,老师傅都没了

所以其实还有第 2 种基于观测的方案:利用 WebView Devtool 工具定位。

用 Web 调试工具定位

前面也说了,查看注入的 JS 代码是一个很小众的需求,所以调试工具并没有提供独立的查看面板,所有的能力都是拼拼凑凑起来的,而且部分能力 iOS 和 Android 互为补集 😓,整体上还有有些凌乱的。

如何远程调试 Web 页面,可以参考这篇文章:各种「真机远程调试」方法汇总
想远程调试 APP WebView 网页,需要在 Native 层开启调试能力

Common

iOS 和 Android 都通用的方案有这么两种:

Debug

我们可以通过 debug 到关键代码,然后查看调用栈,找到注入代码:

iOS 利用 Safari Devtool 调试,查看方式如下:

Android 利用 Chrome Devtool 调试,查看方法如下:

Log

还有一种方法是在 Native 注入的 JS 代码中,加入 console.log 的调用,这样在注入代码运行的时候,可以从 Console 面板的资源引用找到注入脚本。

但是这个问题有个悖论:

  • 一般注入的脚本为了不增加运行时性能负担,是不会加 log 调用的,所以一般没法用
  • 如果开发者主动去注入的 JS 代码中加入 log,那说明他有一定的 native 经验,那为什么不直接看 native 代码呢

所以这样方法更像一种辅助方案,用来配合其它方案一起排查问题。

iOS

iOS 有两种方式看注入的代码。

第一种是在「来源」的「附加脚本 」里,可以在这里看到 Native 通过 addUserScript 注入的脚本,整整齐齐的,还是比较方便查看的。但有个问题是这里并不会 列出 evaluateJavaScript 注入的代码。

这里就介绍第二种方法,那就是「全局搜索」。

Safari 的全局搜索功能,可以同时搜索 addUserScript/evaluateJavaScript/正常加载的资源 里的代码,所以如果你知道注入代码的一些关键信息,可以通过搜索的方式定位代码。

Android

Android 这里使用 Chrome Devtool 查看注入代码。用了这么久的 Chrome Devtool,我是第一次发现它做的不如 Safari 的地方,那就是上面 Safari Devtool 有的东西它都没有。

但是 Chrome Devtool Performance 可以曲线救国一下,我们可以通过性能录制得到一份性能分析火焰图,然后查看主线程的代码执行情况,一般是 Evalutae Script 阶段,然后再定位可能的执行时机,通过点击 Bar 展开的 Summary 面板,一般有个 VM 开头的文件,打开就可显示注入的 JS 代码:

总结

从上面内容可以看出定位「注入 JS 代码」还是挺麻烦的,需要用各种手段曲线救国,而且很多情况下各个技巧需要交叉使用才能定位到。希望我这篇文章可以帮助到一些开发者,减少 debug 内耗的时间。


如果你喜欢我的文章,希望点赞👍 收藏 📁 评论 💬 三连支持一下,谢谢你,这对我真的很重要!

相关推荐
CYRUS STUDIO11 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
四喜花露水32 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy42 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
chaosama1 小时前
禁止uni小程序ios端上下拉伸(橡皮筋效果)
ios·小程序
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
Zender Han1 小时前
Flutter自定义矩形进度条实现详解
android·flutter·ios
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#