1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        JSB 原理與實(shí)踐

        共 9251字,需瀏覽 19分鐘

         ·

        2021-08-03 08:12


         大廠技術(shù)  堅(jiān)持周更  精選好文

        什么是 JSB

        我們開發(fā)的 h5 頁面運(yùn)行在端上的 WebView 容器之中,很多業(yè)務(wù)場(chǎng)景下 h5 需要依賴端上提供的信息/能力,這時(shí)我們需要一個(gè)可以連接原生運(yùn)行環(huán)境和 JS 運(yùn)行環(huán)境的橋梁 。 這個(gè)橋梁就是 JSB,JSB 讓 Web 端和 Native 端得以實(shí)現(xiàn)雙向通信

        WebView 概述

        WebView 是移動(dòng)端中的一個(gè)控件,它為 JS 運(yùn)行提供了一個(gè)沙箱環(huán)境。WebView 能夠加載指定的 url,攔截頁面發(fā)出的各種請(qǐng)求等各種頁面控制功能,JSB 的實(shí)現(xiàn)就依賴于 WebView 暴露的各種接口。由于歷史原因,安卓和 iOS 均有高低兩套版本的 WebView 內(nèi)核:

        平臺(tái)和版本WebView 內(nèi)核
        iOS 8+WKWebView
        iOS 2-8UIWebView
        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 容器中動(dòng)態(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)我們?cè)谖谋究蛑休斎胂铝凶址Ⅻc(diǎn)擊按鈕后,h5 頁面中 id 為 test 的 p 標(biāo)簽內(nèi)容被修改了。

        document.querySelector('#test').innerHTML = 'I am from native';

        敏銳同學(xué)到這一步其實(shí)就已經(jīng)知道我們?cè)谌粘J褂?JSB 時(shí)客戶端是如何調(diào)用前端 JS 代碼了,我們?cè)趧倓偟撵o態(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ā)出的所有請(qǐng)求都是可以被 Native 容器感知到的(是不是想到了Gecko),因此攔截式具體指的是 Native 攔截 Web 發(fā)出的 URL 請(qǐng)求,雙方在此之前約定一個(gè) JSB 請(qǐng)求格式,如果該請(qǐng)求是 JSB 則進(jìn)行相應(yīng)的處理,若不是則直接轉(zhuǎn)發(fā)。

        Native 攔截請(qǐng)求的鉤子方法:

        平臺(tái)API
        AndroidshouldOverrideUrlLoading
        iOS 8+decidePolicyForNavigationAction
        iOS 8-shouldStartLoadWithRequest

        攔截式的基本流程如下

        上述流程存在幾個(gè)問題:

        1. 通過何種方式發(fā)出請(qǐng)求?

        Web 端發(fā)出請(qǐng)求的方式非常多樣,例如 <a/> 、iframe.srclocation.href、ajax 等,但 <a/> 需要用戶手動(dòng)觸發(fā),location.href 可能會(huì)導(dǎo)致頁面跳轉(zhuǎn),安卓端攔截 ajax 的能力有所欠缺,因此絕大多數(shù)攔截式實(shí)現(xiàn)方案均采用iframe 來發(fā)送請(qǐng)求。

        1. 如何規(guī)定 JSB 的請(qǐng)求格式?

        一個(gè)標(biāo)準(zhǔn)的 URL 由 <scheme>://<host>:<port><path> 組成,相信大家都有過從微信或手機(jī)瀏覽器點(diǎn)擊某個(gè)鏈接意外跳轉(zhuǎn)到其他 App 的經(jīng)歷,如果有仔細(xì)留意過這些鏈接的 URL 你會(huì)發(fā)現(xiàn)目前主流 App 都有其專屬的一個(gè) scheme 來作為該應(yīng)用的標(biāo)識(shí),例如微信的 URL scheme 就是 weixin://。JSB 的實(shí)現(xiàn)借鑒這一思路,定制業(yè)務(wù)自身專屬的一個(gè) URL scheme 來作為 JSB 請(qǐng)求的標(biāo)識(shí),例如字節(jié)內(nèi)部實(shí)現(xiàn)攔截式 JSB 的 SDK 中就定義了 bytedance:// 這樣一個(gè) scheme。

        // Web 通過動(dòng)態(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è)劣勢(shì):

        • 連續(xù)發(fā)送時(shí)可能會(huì)造成消息丟失(可以使用消息隊(duì)列解決該問題)
        • URL  字符串長度有限制
        • 性能一般,URL request 創(chuàng)建請(qǐng)求有一定的耗時(shí)(Android 端 200-400ms)

        實(shí)踐案例

        同樣用一個(gè)簡(jiǎn)單的 Demo2 來看一下如何使用攔截式實(shí)現(xiàn) Web 向 Native 發(fā)送消息,這里實(shí)現(xiàn)了在 Web 端喚起 Native 的相冊(cè)。

        遵循上述實(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)請(qǐng)求攔截,解析 URL 參數(shù),若 URL scheme 是 prek 則認(rèn)為該請(qǐng)求是一個(gè)來自 Web 的 JSB 調(diào)用:

        - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
          NSURL *url = navigationAction.request.URL;
          NSLog(@"攔截到 Web 發(fā)出的請(qǐng)求 = %@", url);

          if ([self isSchemeMatchPrek:url]) {
            NSString* host = url.host.lowercaseString;
            if ([host isEqualToString: @"openphotoalbum"]) {
              [self openCameraForWeb]; // 打開相冊(cè)
              NSLog(@"打開相冊(cè)");
            }
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
          } else {
            decisionHandler(WKNavigationActionPolicyAllow);
          }
        }

        為了更清晰地看到 Native 攔截的結(jié)果,在上述代理方法中打個(gè)斷點(diǎn):

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

        注入式

        注入式的原理是通過 WebView 提供的接口向 JS 全局上下文對(duì)象(window)中注入對(duì)象或者方法,當(dāng) JS 調(diào)用時(shí),可直接執(zhí)行相應(yīng)的 Native 代碼邏輯,從而達(dá)到 Web 調(diào)用 Native 的目的。

        Native 注入 API 的相關(guān)方法:

        平臺(tái)API特點(diǎn)
        AndroidaddJavascriptInterface4.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'

        這種方法簡(jiǎn)單而直觀,并且不存在參數(shù)長度限制和性能瓶頸等問題,目前主流的 JSB SDK 都將注入式方案作為優(yōu)先使用的對(duì)象。注入式的實(shí)現(xiàn)非常簡(jiǎn)單,這里不做案例展示。

        兩種方案對(duì)比

        為了更清晰地表達(dá)這兩種方式的區(qū)別,這里貼一個(gè)對(duì)比表格:

        方案兼容性性能參數(shù)長度限制
        攔截式無兼容性問題較差,安卓端尤為明顯有限制
        注入式安卓4.2+ 和 iOS 7+以上可用較好

        如何執(zhí)行回調(diào)

        通過上述介紹我們已經(jīng)知道如何實(shí)現(xiàn)雙端互相發(fā)送消息,但上述兩個(gè)通信過程缺少了“回應(yīng)”這一動(dòng)作,原因就是上述步驟缺少了回調(diào)函數(shù)的執(zhí)行。以攔截式為例,常見的一個(gè) JSB 調(diào)用是 Web 獲取當(dāng)前 App 信息, Native 攔截到 bytedance://getAppInfo這樣一個(gè)請(qǐng)求后將獲取當(dāng)前 App 信息,那獲取完成后如何讓 Web 端拿到該信息呢?

        一個(gè)最簡(jiǎn)單的做法是類比 JSONP 的實(shí)現(xiàn),我們可以在請(qǐng)求的 URL 上拼接回調(diào)方法的事件名,將該事件掛載在全局 window 上,由于 Native 端可以輕松執(zhí)行 JS 代碼,因此在完成端邏輯后直接執(zhí)行該事件名對(duì)應(yīng)的回調(diào)方法即可。以 getAppInfo 為例:

        // Web
        const uniqueID = 1 // 為防止事件名沖突,給每個(gè) callback 設(shè)置一個(gè)唯一標(biāo)識(shí)
        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)方法掛載在全局對(duì)象上,Native 即可把每次調(diào)用后的響應(yīng)通過動(dò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ù)和對(duì)應(yīng)的回調(diào)函數(shù),其次回調(diào)函數(shù)還需要一個(gè)一個(gè)地掛載在全局對(duì)象上。

        我們期望的使用方式其實(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é)請(qǐng)自行戳源碼地址 JSB_Demo 自行體驗(yàn)。(需要使用 Xcode 打開,會(huì)涉及一些客戶端的知識(shí),請(qǐng)配合文檔和 Google 使用)。

        一點(diǎn)感受

        筆者所在業(yè)務(wù)使用的 bridge 即司內(nèi)目前最新的 SDK,沒有歷史包袱、使用體驗(yàn)也非常良好。得益于客戶端遵循該 SDK 配套的實(shí)現(xiàn)機(jī)制,即使完全不了解 JSB 原理的同學(xué)在與端上對(duì)接 bridge 時(shí)也幾乎沒有遇到障礙。倘若拋開公司完備的基礎(chǔ)建設(shè),想實(shí)現(xiàn)一個(gè)通用且好用的 JSB 并非易事,因此了解其中的門道還是非常有益的。(巨人的肩膀站久了,確實(shí)巴適得很??)

        參考文獻(xiàn)


        深入淺出 JSBridge[4]

        JSB 實(shí)戰(zhàn)[5]

        [1]

        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)容,希望對(duì)你有所幫助^_^

        喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

        歡迎關(guān)注公眾號(hào) 前端Sharing 收貨大廠一手好文章~


        瀏覽 68
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日韩一区二区三区在线看 | 天天操综合视频 | 日韩三级黄色 | 免费视频性 | 日本边添边摸边做边爱的主演 | 日韩免费精品一区二区三区色欲AV | 麻豆精品久久久 | 天天日天天操天天摸 | 久久短视频 | 国产一级AAA毛片 |