Android側webview與Js通信的方式(1)

Android側webview與Js通信的方式(1)

JsBridge原理介紹

Android側JsBridge一般指 JsBridge,該框架對應ios側的WebViewJavascriptBridge,兩者的實現(xiàn)細節(jié)各有不同综膀,但是總體原理一致豪治。我們主要看一下其Js與Native通信原理的實現(xiàn)叼丑,對于具體的代碼細節(jié)不做深究。

JsBridge集成

  • Js端

    集成源碼中的js文件怕犁,WebViewJavascriptBridge.js边篮,注意此處不可以通過注入的方式實現(xiàn),不要被各種講解博客誤導奏甫。

  • Android側

 dependencies {
   compile 'com.github.lzyzsd:jsbridge:1.0.4'
   }

Js調用Native

步驟

    1. js側
function _doSend(message, responseCallback) {
          if (responseCallback) {
              //生成唯一callbackid用于標識該次jsbridge通信過程
              var callbackId = 'cb_' + (uniqueId++) + '_' + new  Date().getTime();
              responseCallbacks[callbackId] = responseCallback;
              message.callbackId = callbackId;
           }
           sendMessageQueue.push(message);
           //src:"yy://__QUEUE_MESSAGE__/"
           messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
           }
  • 2.native側
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
      try {
          url = URLDecoder.decode(url, "UTF-8");
      } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
      }

      if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回數(shù)據(jù)
          webView.handlerReturnData(url);
          return true;
      } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
          webView.flushMessageQueue();
          return true;
      } else {
          return super.shouldOverrideUrlLoading(view, url);
      }
      }

這里會走第二個if, 調用BridgeWebView的flushMessageQueue()方法

   void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new  CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    ...
                }
            });
        }
    }

在這個flushMessageQueue方法里, 如果當前是主線程就調用一個loadUrl方法

    public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
    
        // jsUrl = "javascript:WebViewJavascriptBridge._fetchQueue();"
        this.loadUrl(jsUrl);
        // 添加至 Map<String, CallBackFunction>
        String functionName = BridgeUtil.parseFunctionName(jsUrl);
        // functionName = "_fetchQueue"
        responseCallbacks.put(functionName, returnCallback);
    }

在這個方法里, 首先會調用WebViewJavascriptBridge的_fetchQueue()方法, 然后解析方 法名字, 因為這里的方法名字是寫死的, 其實就是_fetchQueue, 請記住這個名字, 因為后面會用到.然后將以這個_fetchQueue為key, 回調方法為value, 放到一個map里面.然后我們再去看js那端的方法.

  • 3.js側
       // 提供給native調用,該函數(shù)作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內容,所以使用url shouldOverrideUrlLoading 的方式返回內容
  function _fetchQueue() {
      var messageQueueString = JSON.stringify(sendMessageQueue);
      console.log('messageQueueString = ' + messageQueueString);
      sendMessageQueue = [];
      // android can't read directly the return data, so we can reload iframe src to communicate with java
      var src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
      messagingIframe.src = src;
  }
  • 4.native側

    觸發(fā)shouldOverrideUrlLoading方法戈轿,并走第一個if,觸發(fā)handlerReturnData方法

        void handlerReturnData(String url) {
        // _fetchQueue
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
        //取出flushMessageQueue方法中放入responseCallbacks隊列中的callback 
        CallBackFunction f = responseCallbacks.get(functionName);
        //取出js側傳來的數(shù)據(jù)
        String data = BridgeUtil.getDataFromReturnUrl(url);
        if (f != null) {
            //執(zhí)行callback
            f.onCallBack(data);
            responseCallbacks.remove(functionName);
            return;
         }
        }

在看一下這個callback

         void flushMessageQueue() {
         if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    // deserializeMessage 反序列化消息
                    List<Message> list = null;
                    try {
                        list = Message.toArrayList(data);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return;
                    }
                    if (list == null || list.size() == 0) {
                        return;
                    }
                    for (int i = 0; i < list.size(); i++) {
                        Message m = list.get(i);
                        String responseId = m.getResponseId();
                        // 是否是response  CallBackFunction
                        if (!TextUtils.isEmpty(responseId)) {
                            CallBackFunction function = responseCallbacks.get(responseId);
                            String responseData = m.getResponseData();
                            function.onCallBack(responseData);
                            responseCallbacks.remove(responseId);
                        } else {
                            CallBackFunction responseFunction = null;
                            // if had callbackId 如果有回調Id
                            final String callbackId = m.getCallbackId();
                            <br>
                            if (!TextUtils.isEmpty(callbackId)) {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        Message responseMsg = new Message();
                                        responseMsg.setResponseId(callbackId);
                                        responseMsg.setResponseData(data);
                                        queueMessage(responseMsg);
                                    }
                                };
                             <br/>
                            } else {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        // do nothing
                                    }
                                };
                            }
                            // BridgeHandler執(zhí)行
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } else {
                                handler = defaultHandler;
                            }
                            if (handler != null){
                                handler.handler(m.getData(), responseFunction);
                            }
                        }
                    }
                }
            });
          }
        }
        

首先將數(shù)據(jù)解析成一個Message的list, 這個Message是自定義的類, 里面包含兩端協(xié)商好格式的信息,最后會執(zhí)行到queueMessage(responseMsg)中

private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }

走dispatch方法

/**
     * 分發(fā)message 必須在主線程才分發(fā)成功
     * @param m Message
     */
    void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string  為json字符串轉義特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
        // javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"http:\\\/\\\/ww3.sinaimg.cn\\\/mw690\\\/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg\",\"responseId\":\"cb_4_1532856634427\"}');
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        // 必須要找主線程才會將數(shù)據(jù)傳遞出去 --- 劃重點
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

首先將這個Message轉化成json格式的字符串, 去掉一些特殊字符, 然后再主線程調用js方法, 方法是WebViewJavascriptBridge._handleMessageFromNative方法

  • 5.js側
// 提供給native調用,receiveMessageQueue 在會在頁面加載完后賦值為null,所以
    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
    }

_handleMessageFromNative方法會處理native傳來數(shù)據(jù)阵子,本次交互結束

  • 流程圖

    avatar
  • 問題

看完JsBridge代碼思杯,可能大家都會有疑問,這一次流程中挠进,為什么js側與native側為什么要交互兩次色乾,第一次其實并沒有傳任何有效數(shù)據(jù)過來,是否多余领突。下面我們著重看下這個問題暖璧。

Cordova方案參考

由于業(yè)界hybrid方案并不多,一般大廠的方案又較為復雜君旦,而且網(wǎng)上資料基本沒有任何對該問題的解釋澎办,因此本文參考了云閃付正在使用的hybrid方案cordova的通信邏輯。

Cordova方案相較Jsbridge方案更為重量級金砍,十分復雜局蚀,因此本文并不做深入研究,僅針對其實現(xiàn)的Native恕稠、JS端通信邏輯進行研究琅绅。

ios側

ios側一般有兩種方式,核心代碼如下

if (bridgeMode === jsToNativeModes.WK_WEBVIEW_BINDING) {
        window.webkit.messageHandlers.cordova.postMessage(command);
    } else {
        // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
        // then the queue will be flushed when it returns; no need for a poke.
        // Also, if there is already a command in the queue, then we've already
        // poked the native side, so there is no reason to do so again.
        if (!isInContextOfEvalJs && commandQueue.length == 1) {
            switch (bridgeMode) {
            case jsToNativeModes.XHR_NO_PAYLOAD:
            case jsToNativeModes.XHR_WITH_PAYLOAD:
            case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
                pokeNativeViaXhr();    // 新建一個XMLHttpRequest鹅巍,并發(fā)送一個HEAD請求千扶,并將commondQueue以json串的形式放在請求頭cmds上料祠。
                break;
            default: // iframe-based.
                pokeNativeViaIframe(); // 創(chuàng)建iframe,通過hash值來傳遞commondQueue 或 execIframe.src = "gap://ready"
            }
        }
    }

可以看出有兩種方式县貌,一是新建一個XMLHttpRequest术陶,并發(fā)送一個HEAD請求凑懂,并將commondQueue以json串的形式放在請求頭cmds上煤痕。native側進行攔截;二是創(chuàng)建iframe接谨,通過hash值來傳遞commondQueue 或 execIframe.src = "gap://ready"摆碉,與jsbridge一個原理。
ios端通過UIWebViewDelegate(iframe方式)或 NSURLProtocol攔截(xhr方式)方式接收到commondQueue后脓豪,執(zhí)行插件的實際功能巷帝。
ios側處理完后回消息給js側也有兩種方式一是通過UIWebView的stringByEvaluatingJavaScriptFromString方法,二是通過注入方式調用js側iOSExec.nativeCallback方法扫夜。

Android側

var messages = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); // 
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2.  See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && messages === "@Null arguments.") {
  androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
  androidExec(success, fail, service, action, args);
  androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
  return;
} else {
  androidExec.processMessages(messages, true);
}
  1. 如果是JS_OBJECT方式楞泼,那么nativeApiProvider.get().exec= 安卓端源碼中注解了@JavascriptInterface的 exec方法
  2. 如果是PROMPT方式,那么nativeApiProvider.get().exec 為如下方法:
exec: function(bridgeSecret, service, action, callbackId, argsJson) {
    return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
  },

即通過promt()與native側的onJsPromt()方法通信笤闯。

android側回調回js側也有兩種方式堕阔,一是通過evaluateJavascript,二是通過loadurl颗味。兩種方式都是通過注入直接調用js側androidExec.processMessages(messages, true)方法超陆。

cordova總結

從以上分析可以看出,cordova不管在ios側還是android側浦马,都是只通信一次时呀。其中android側js與native之間的通信使用了webview提供的多種api,接下來我們看一下這些api的特點及優(yōu)劣晶默。

交互方式總結

Android 端webview與Js通信的方式很多谨娜,要了解jsbridge兩次通信是否合理,首先要了解下Android通過WebView與JS交互的方式磺陡。

  • 總體目錄
avatar

Js主動調用Native

Js主動調Native主要有三種方式

  • 通過 WebView的addJavascriptInterface()(@JavascriptInterface)

    該方法通過addJavascriptInterface()將java對象映射到Js對象趴梢,js端直接調用即可,十分方便仅政。但是該方法在Android4.2(17)之前有較大的安全漏洞垢油,在Android <=4.1.2 (API 16),WebView使用WebKit瀏覽器引擎圆丹,并未正確限制addJavascriptInterface的使用方法滩愁,在應用權限范圍內,攻擊者可以通過Java反射機制實現(xiàn)任意命令執(zhí)行辫封。在Android >=4.2 (API 17)硝枉,WebView使用Chromium瀏覽器引擎廉丽,并且限制了Javascript對Java對象方法的調用權限,只有聲明了@JavascriptInterace注解的方法才能被Web頁面調用妻味。

    優(yōu)點:使用簡單
    缺點:API17之前有嚴重的安全漏洞

  • 通過 WebViewClient 的shouldOverrideUrlLoading ()攔截url

    1.js端通過修改iframe屬性觸發(fā)Android側WebViewClient的回調方法shouldOverrideUrlLoading ()

    2.攔截正压、解析該 url 的協(xié)議

    3.如果檢測到是預先約定好的協(xié)議,就調用相應方法

    優(yōu)點:對Api無要求责球,不存在安全漏洞焦履,較為通用

    缺點:需要js與native側協(xié)商格式,JS獲取Android方法的返回值復雜

  • 通過 WebChromeClient 的onJsAlert()雏逾、onJsConfirm()嘉裤、onJsPrompt()攔截JS對話框alert()、confirm()栖博、prompt()消息

    Android通過 WebChromeClient 的onJsAlert()屑宠、onJsConfirm()、onJsPrompt()方法回調分別攔截JS對話框 (即上述三個方法)仇让,得到他們的消息內容典奉,然后解析即可。對比三個方法我們可以發(fā)現(xiàn)只有prompt()可以返回任意類型的值丧叽,操作最全面方便卫玖、更加靈活;而alert()對話框沒有返回值蠢正;confirm()對話框只能返回兩種狀態(tài)(確定 / 取消)兩個值骇笔,因此promt()方法較為合適

    avatar
  • 總結
    對比三種方式如下圖

    image

    可以發(fā)現(xiàn),利用WebChromeClient的onJsPrompt()方法攔截js側的promt()嚣崭,這種方式最合理

Native主動調用Js

  • 通過WebView的loadUrl(),及我們熟知的js注入

    通過webview的loadUrl()方法笨触, mWebView.loadUrl("javascript:callJS()"),注意javascript為必加的前綴雹舀,callJS()為js對應方法名
    特別注意:
    1. JS代碼調用一定要在 onPageFinished() 回調之后才能調用芦劣,否則不會調用。
    2. loadurl方法在url過長(2000個字符)時會失敗说榆,所以不要嘗試將一些js文件通過注入的方式直接使用虚吟,What is the maximum length of a URL in different browsers?

    優(yōu)點:對Api無要求,不存在安全漏洞签财,較為通用

    缺點:對注入代碼長度有限制串慰,且該方法執(zhí)行會使頁面刷新,并且無返回值

  • 通過WebView的evaluateJavascript()

    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處為 js 返回的結果
          }
        });
      }
    

    優(yōu)點:1. 該方法的執(zhí)行不會使頁面刷新唱蒸。
    2. 有返回值邦鲫,效率更高、使用更簡潔。

    缺點:1. 要求Android4.4以上
    2. onReceiveValue(String value)庆捺,value會多一對引號古今,需要特殊處理

  • 總結

    avatar
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市滔以,隨后出現(xiàn)的幾起案子捉腥,更是在濱河造成了極大的恐慌,老刑警劉巖你画,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抵碟,死亡現(xiàn)場離奇詭異,居然都是意外死亡撬即,警方通過查閱死者的電腦和手機立磁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剥槐,“玉大人,你說我怎么就攤上這事宪摧×J” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵几于,是天一觀的道長蕊苗。 經(jīng)常有香客問我,道長沿彭,這世上最難降的妖魔是什么朽砰? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮喉刘,結果婚禮上瞧柔,老公的妹妹穿的比我還像新娘。我一直安慰自己睦裳,他們只是感情好造锅,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著廉邑,像睡著了一般哥蔚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛛蒙,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天糙箍,我揣著相機與錄音,去河邊找鬼牵祟。 笑死深夯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的课舍。 我是一名探鬼主播塌西,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼他挎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捡需?” 一聲冷哼從身側響起办桨,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎站辉,沒想到半個月后呢撞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡饰剥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年殊霞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汰蓉。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡绷蹲,死狀恐怖,靈堂內的尸體忽然破棺而出顾孽,到底是詐尸還是另有隱情祝钢,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布若厚,位于F島的核電站拦英,受9級特大地震影響,放射性物質發(fā)生泄漏测秸。R本人自食惡果不足惜疤估,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霎冯。 院中可真熱鬧铃拇,春花似錦、人聲如沸肃晚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽关串。三九已至拧廊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晋修,已是汗流浹背吧碾。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留墓卦,地道東北人倦春。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親睁本。 傳聞我的和親對象是個殘疾皇子尿庐,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349