JSB 原理與實(shí)踐
大廠技術(shù) 堅(jiān)持周更 精選好文
什么是 JSB
我們開發(fā)的 h5 頁面運(yùn)行在端上的 WebView 容器之中,很多業(yè)務(wù)場景下 h5 需要依賴端上提供的信息/能力,這時(shí)我們需要一個(gè)可以連接原生運(yùn)行環(huán)境和 JS 運(yùn)行環(huán)境的橋梁 。 這個(gè)橋梁就是 JSB,JSB 讓 Web 端和 Native 端得以實(shí)現(xiàn)雙向通信。

WebView 概述
WebView 是移動端中的一個(gè)控件,它為 JS 運(yùn)行提供了一個(gè)沙箱環(huán)境。WebView 能夠加載指定的 url,攔截頁面發(fā)出的各種請求等各種頁面控制功能,JSB 的實(shí)現(xiàn)就依賴于 WebView 暴露的各種接口。由于歷史原因,安卓和 iOS 均有高低兩套版本的 WebView 內(nèi)核:
| 平臺和版本 | WebView 內(nèi)核 |
|---|---|
| iOS 8+ | WKWebView |
| iOS 2-8 | UIWebView |
| Android 4.4+ | Chrome |
| Android 4.4- | Webkit |
PS: 下文中出現(xiàn)的高版本均代指 iOS 8+ 或 Android 4.4+,低版本則相反。
JSB 原理
要實(shí)現(xiàn)雙向通信自然要依次實(shí)現(xiàn) Native 向 Web 發(fā)送消息和 Web 向 Native 發(fā)送消息。
Native 向 Web 發(fā)送消息
Native 向 Web 發(fā)送消息基本原理上是在 WebView 容器中動態(tài)地執(zhí)行一段 JS 腳本,通常情況下是調(diào)用一個(gè)掛載在全局上下文的方法。Android 和 iOS 均提供了不同的接口來實(shí)現(xiàn)這一過程。
方法
Android 高低版本存在兩種直接執(zhí)行 JS 字符串的方法:
| Android 版本 | API | 特點(diǎn) |
|---|---|---|
| 低版本 | WebView.loadUrl | 無法執(zhí)行回調(diào) |
| 高版本 | WebView.evaluateJavascript | 可以拿到 JS 執(zhí)行完畢的返回值 |
iOS 高低版本同樣存在兩種不同的實(shí)現(xiàn)方式:
| iOS 版本 | API | 特點(diǎn) |
|---|---|---|
| 低版本 | UIWebView.stringByEvaluatingJavaScriptFromString | 無法執(zhí)行回調(diào) |
| 高版本 | WKWebView.evaluateJavaScript | 可以拿到 JS 執(zhí)行完畢的返回值 |
實(shí)踐
下面我們通過一個(gè)小 Demo 來看一下在 iOS 端實(shí)現(xiàn) Native 向 Web 端發(fā)消息的實(shí)際效果:
(本文所有 Demo 均運(yùn)行在 iOS14.5 模擬器中,WebView 容器采用 WKWebView 內(nèi)核)

頁面上半部分的 UI 是由 HTML + CSS 渲染所得,是一個(gè)純靜態(tài)的 webpage,中間的輸入框和按鈕是 Native 原生控件,直接覆蓋在 WebView 容器之上。在 Native 按鈕上綁定了一個(gè)點(diǎn)擊事件:將文本框輸入的字符視為 JS 字符串并調(diào)用相關(guān) API 直接執(zhí)行。
可以看到當(dāng)我們在文本框中輸入下列字符并點(diǎn)擊按鈕后,h5 頁面中 id 為 test 的 p 標(biāo)簽內(nèi)容被修改了。
document.querySelector('#test').innerHTML = 'I am from native';

敏銳同學(xué)到這一步其實(shí)就已經(jīng)知道我們在日常使用 JSB 時(shí)客戶端是如何調(diào)用前端 JS 代碼了,我們在剛剛的靜態(tài) html 文件中添加幾行 JS 代碼:
function evaluateByNative(params) {
const p = document.createElement('p');
p.innerText = params;
document.body.appendChild(p);
return 'Hello Bridge!';
}
在文本框中輸入 evaluateByNative(23333),來看一下調(diào)用的結(jié)果:


可以看到 Native 端可以直接調(diào)用掛載在 window 上的全局方法并傳入相應(yīng)的函數(shù)執(zhí)行參數(shù),并且在函數(shù)執(zhí)行結(jié)束后 Native 端可以直接拿到執(zhí)行成功的返回值。
Web 向 Native 發(fā)送消息
Web 向 Native 發(fā)送消息本質(zhì)上就是某段 JS 代碼的執(zhí)行端上是可感知的,目前業(yè)界主流的實(shí)現(xiàn)方案有兩種,分別是攔截式和注入式。
攔截式
和瀏覽器類似 WebView 中發(fā)出的所有請求都是可以被 Native 容器感知到的(是不是想到了Gecko),因此攔截式具體指的是 Native 攔截 Web 發(fā)出的 URL 請求,雙方在此之前約定一個(gè) JSB 請求格式,如果該請求是 JSB 則進(jìn)行相應(yīng)的處理,若不是則直接轉(zhuǎn)發(fā)。
Native 攔截請求的鉤子方法:
| 平臺 | API |
|---|---|
| Android | shouldOverrideUrlLoading |
| iOS 8+ | decidePolicyForNavigationAction |
| iOS 8- | shouldStartLoadWithRequest |
攔截式的基本流程如下:

上述流程存在幾個(gè)問題:
通過何種方式發(fā)出請求?
Web 端發(fā)出請求的方式非常多樣,例如 <a/> 、iframe.src、location.href、ajax 等,但 <a/> 需要用戶手動觸發(fā),location.href 可能會導(dǎo)致頁面跳轉(zhuǎn),安卓端攔截 ajax 的能力有所欠缺,因此絕大多數(shù)攔截式實(shí)現(xiàn)方案均采用iframe 來發(fā)送請求。
如何規(guī)定 JSB 的請求格式?
一個(gè)標(biāo)準(zhǔn)的 URL 由 <scheme>://<host>:<port><path> 組成,相信大家都有過從微信或手機(jī)瀏覽器點(diǎn)擊某個(gè)鏈接意外跳轉(zhuǎn)到其他 App 的經(jīng)歷,如果有仔細(xì)留意過這些鏈接的 URL 你會發(fā)現(xiàn)目前主流 App 都有其專屬的一個(gè) scheme 來作為該應(yīng)用的標(biāo)識,例如微信的 URL scheme 就是 weixin://。JSB 的實(shí)現(xiàn)借鑒這一思路,定制業(yè)務(wù)自身專屬的一個(gè) URL scheme 來作為 JSB 請求的標(biāo)識,例如字節(jié)內(nèi)部實(shí)現(xiàn)攔截式 JSB 的 SDK 中就定義了 bytedance:// 這樣一個(gè) scheme。
// Web 通過動態(tài)創(chuàng)建 iframe,將 src 設(shè)置為符合雙端規(guī)范的 url scheme
const CUSTOM_PROTOCOL_SCHEME = 'prek'
function web2Native(event) {
const messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + event;
document.documentElement.appendChild(messagingIframe);
setTimeout(() => {
document.documentElement.removeChild(messagingIframe);
}, 200)
}
攔截式在雙端都具有非常好的向下兼容性,曾經(jīng)是最主流的 JSB 實(shí)現(xiàn)方案,但目前在高版本的系統(tǒng)中已經(jīng)逐漸被淘汰,理由是它有如下幾個(gè)劣勢:
連續(xù)發(fā)送時(shí)可能會造成消息丟失(可以使用消息隊(duì)列解決該問題) URL 字符串長度有限制
性能一般,URL request 創(chuàng)建請求有一定的耗時(shí)(Android 端 200-400ms)
實(shí)踐案例
同樣用一個(gè)簡單的 Demo2 來看一下如何使用攔截式實(shí)現(xiàn) Web 向 Native 發(fā)送消息,這里實(shí)現(xiàn)了在 Web 端喚起 Native 的相冊。

遵循上述實(shí)現(xiàn)方式,Web 發(fā)送消息的代碼如下:
const CUSTOM_PROTOCOL_SCHEME = 'prek' // 自定義 url scheme
function web2Native(event_name) {
const messagingIframe = document.createElement('iframe')
messagingIframe.style.display = 'none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + event_name
document.documentElement.appendChild(messagingIframe)
setTimeout(() => {
document.documentElement.removeChild(messagingIframe)
}, 0)
}
const btn = document.querySelector('#btn')
btn.onclick = () => {
web2Native('openPhotoAlbum')
}
Native 側(cè)通過 decidePolicyForNavigationAction 這一 delegate 實(shí)現(xiàn)請求攔截,解析 URL 參數(shù),若 URL scheme 是 prek 則認(rèn)為該請求是一個(gè)來自 Web 的 JSB 調(diào)用:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
NSLog(@"攔截到 Web 發(fā)出的請求 = %@", url);
if ([self isSchemeMatchPrek:url]) {
NSString* host = url.host.lowercaseString;
if ([host isEqualToString: @"openphotoalbum"]) {
[self openCameraForWeb]; // 打開相冊
NSLog(@"打開相冊");
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
為了更清晰地看到 Native 攔截的結(jié)果,在上述代理方法中打個(gè)斷點(diǎn):

繼續(xù)執(zhí)行,Congratulation!模擬器的相冊被打開了!

注入式
注入式的原理是通過 WebView 提供的接口向 JS 全局上下文對象(window)中注入對象或者方法,當(dāng) JS 調(diào)用時(shí),可直接執(zhí)行相應(yīng)的 Native 代碼邏輯,從而達(dá)到 Web 調(diào)用 Native 的目的。
Native 注入 API 的相關(guān)方法:
| 平臺 | API | 特點(diǎn) |
|---|---|---|
| Android | addJavascriptInterface | 4.2 版本以下有安全風(fēng)險(xiǎn) |
| iOS 8+ | WKScriptMessageHandler | 無 |
| iOS 7+ | JavaSciptCore | 無 |
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"getAppInfo"] = ^(msg) {
return @"ggl_2693";
};
window.getAppInfo(); // 'ggl_2693'
這種方法簡單而直觀,并且不存在參數(shù)長度限制和性能瓶頸等問題,目前主流的 JSB SDK 都將注入式方案作為優(yōu)先使用的對象。注入式的實(shí)現(xiàn)非常簡單,這里不做案例展示。
兩種方案對比
為了更清晰地表達(dá)這兩種方式的區(qū)別,這里貼一個(gè)對比表格:
| 方案 | 兼容性 | 性能 | 參數(shù)長度限制 |
|---|---|---|---|
| 攔截式 | 無兼容性問題 | 較差,安卓端尤為明顯 | 有限制 |
| 注入式 | 安卓4.2+ 和 iOS 7+以上可用 | 較好 | 無 |
如何執(zhí)行回調(diào)
通過上述介紹我們已經(jīng)知道如何實(shí)現(xiàn)雙端互相發(fā)送消息,但上述兩個(gè)通信過程缺少了“回應(yīng)”這一動作,原因就是上述步驟缺少了回調(diào)函數(shù)的執(zhí)行。以攔截式為例,常見的一個(gè) JSB 調(diào)用是 Web 獲取當(dāng)前 App 信息, Native 攔截到 bytedance://getAppInfo這樣一個(gè)請求后將獲取當(dāng)前 App 信息,那獲取完成后如何讓 Web 端拿到該信息呢?
一個(gè)最簡單的做法是類比 JSONP 的實(shí)現(xiàn),我們可以在請求的 URL 上拼接回調(diào)方法的事件名,將該事件掛載在全局 window 上,由于 Native 端可以輕松執(zhí)行 JS 代碼,因此在完成端邏輯后直接執(zhí)行該事件名對應(yīng)的回調(diào)方法即可。以 getAppInfo 為例:
// Web
const uniqueID = 1 // 為防止事件名沖突,給每個(gè) callback 設(shè)置一個(gè)唯一標(biāo)識
function webCallNative(event, params, callback) {
if (typeof callback === 'Function') {
const callbackID = 'jsb_cb_' + (uniqueID++) + '_' + Date.now();
window[callbackID] = callback
}
const params = {callback: callbackID}
// 構(gòu)造 url scheme
const src = 'bytedance://getAppInfo?' + JSON.stringify(params)
...
}
// Native
1. 解析傳入的參數(shù) 'getAppInfo' 得知 Web 希望獲取 AppInfo
2. 執(zhí)行端邏輯獲取 AppInfo
3. 執(zhí)行參數(shù)中掛載在全局的 callback 方法,AppInfo 作為回調(diào)方法的參數(shù)
因此只要把相應(yīng)的回調(diào)方法掛載在全局對象上,Native 即可把每次調(diào)用后的響應(yīng)通過動態(tài)執(zhí)行 JS 方法的形式傳遞到 Web 端,這樣一來整個(gè)通信過程就實(shí)現(xiàn)了閉環(huán)。
串聯(lián)雙端通信的過程
現(xiàn)在我們已經(jīng)知道如何實(shí)現(xiàn)兩端互相發(fā)送消息以及執(zhí)行回調(diào)了,但看起來并不好用:首先調(diào)用 JSB 時(shí)需要在方法名后拼接參數(shù)和對應(yīng)的回調(diào)函數(shù),其次回調(diào)函數(shù)還需要一個(gè)一個(gè)地掛載在全局對象上。
我們期望的使用方式其實(shí)是這樣:
// Web
web.call('event1', {param1}, (res) => {...}) // 觸發(fā) native event1 執(zhí)行
web.on('event2', (res) => {...})
// Native
// 這里用 js 代替,理解大致意思即可
native.call('event2', {param2}, (res) => {...}) // 觸發(fā) web event2 執(zhí)行
native.on('event1', (res) => {...})
這里的 JSB 就像是一個(gè)跨越兩端的 EventEmitter,因此需要 Web 和 Native 遵循同一套調(diào)度機(jī)制。

上圖給出了 Web 調(diào)用 -> Native 監(jiān)聽的執(zhí)行過程,同理 Native 調(diào)用 -> Web 監(jiān)聽也是同樣的邏輯,只是把兩邊的實(shí)現(xiàn)調(diào)換一種語言,這里不贅述了。

貼一張其他同學(xué)畫的時(shí)序圖,幫助理解整個(gè)通信過程
Demo3 基于開源的 WebViewJavascriptBridge 演示了一套完整的通訊流程是怎樣進(jìn)行的,有興趣的同學(xué)請自行戳源碼地址 JSB_Demo 自行體驗(yàn)。(需要使用 Xcode 打開,會涉及一些客戶端的知識,請配合文檔和 Google 使用)。
一點(diǎn)感受
筆者所在業(yè)務(wù)使用的 bridge 即司內(nèi)目前最新的 SDK,沒有歷史包袱、使用體驗(yàn)也非常良好。得益于客戶端遵循該 SDK 配套的實(shí)現(xiàn)機(jī)制,即使完全不了解 JSB 原理的同學(xué)在與端上對接 bridge 時(shí)也幾乎沒有遇到障礙。倘若拋開公司完備的基礎(chǔ)建設(shè),想實(shí)現(xiàn)一個(gè)通用且好用的 JSB 并非易事,因此了解其中的門道還是非常有益的。(巨人的肩膀站久了,確實(shí)巴適得很??)
參考文獻(xiàn)
深入淺出 JSBridge[4]
JSB 實(shí)戰(zhàn)[5]
JSONP: https://en.wikipedia.org/wiki/JSONP
[2]WebViewJavascriptBridge: https://github.com/marcuswestin/WebViewJavascriptBridge
[3]JSB_Demo: https://code.byted.org/caocheng.viccc/JSB_Demo
[4]深入淺出 JSBridge: https://juejin.cn/post/6936814903021797389#heading-8
[5]JSB 實(shí)戰(zhàn): https://juejin.cn/post/6844903702721986568
?? 謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。
歡迎關(guān)注公眾號 ELab團(tuán)隊(duì) 收貨大廠一手好文章~
我們來自字節(jié)跳動,是旗下大力教育前端部門,負(fù)責(zé)字節(jié)跳動教育全線產(chǎn)品前端開發(fā)工作。
我們圍繞產(chǎn)品品質(zhì)提升、開發(fā)效率、創(chuàng)意與前沿技術(shù)等方向沉淀與傳播專業(yè)知識及案例,為業(yè)界貢獻(xiàn)經(jīng)驗(yàn)價(jià)值。包括但不限于性能監(jiān)控、組件庫、多端技術(shù)、Serverless、可視化搭建、音視頻、人工智能、產(chǎn)品設(shè)計(jì)與營銷等內(nèi)容。
歡迎感興趣的同學(xué)在評論區(qū)或使用內(nèi)推碼內(nèi)推到作者部門拍磚哦 ??
字節(jié)跳動校/社招投遞鏈接: https://job.toutiao.com/
內(nèi)推碼:7EZKXME
