Android webView與js 交互以及jsbridge框架源碼分析

原文鏈接:http://blog.csdn.net/qq_22329521/article/details/73610277

最近在處理android webView與js的通信上的問題买猖,作為總結(jié)

1.簡單篇

如何實現(xiàn)簡單的android 調(diào)用js 與js調(diào)用android

   讓webview做一下操作
  private void init(Context context){
        WebSettings setting =getSettings();
        setting.setJavaScriptEnabled(true);//支持js
        setWebViewClient(new WebClient(this));
        setWebChromeClient(new WebChromeClient());
        //添加js調(diào)用android的方法這是關(guān)鍵 前者是個對象玉控,后者是個字符串 在js中是在window.android可以直接獲取到
        addJavascriptInterface(new JavaScriptinterface(context,this),
                "android");
        //這是開啟js的調(diào)試下面再講
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            setWebContentsDebuggingEnabled(true);
        }
    }


  public class JavaScriptinterface{
    Context context;

    public JavaScriptinterface(Context c) {
        context = c;
    }
    //這個注解可以點擊進去看官方描述 是 帶有此注釋的標記可用于JavaScript代碼
      @JavascriptInterface
      public void toastMessage(String message) {
          Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
      }
  }

如果js 要調(diào)用這個方法調(diào)用window.anroid.toastMessage('即可')
<script>
function showToast() {
   
    android.toastMessage('hhh');
}
</script>

注意這里的toastMessage 是JavaScriptinterface 中被@JavascriptInterface注解的toastMessage方法

這是js調(diào)用android中的方法

android 調(diào)用js 的方法


<script>
function jsalert(data) {
    alert(data);
}
</script>
//java這樣調(diào)用即可 方法前需要加javascript
webview.loadUrl('javascript:jsalert('111')')

注意點

  1. 需要在主線程調(diào)用才會生效
  2. 如果在oncreate中直接調(diào)用loadurl不會生效原因是 webview需要加載完成才能調(diào)用webview加載完成的回調(diào)函數(shù)是在webclinet 中
  3. 如果alert沒效果高诺,需要在setWebChromeClient(new WebChromeClient());
String javascriptCommand='javascript:jsalert('111')';
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }else{
            ((Activity) getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //此時已在主線程中,可以更新UI了
                    loadUrl(javascriptCommand);
                }
            });
        }
    }
    
public class JsWebClient extends WebViewClient {
    public static final String TAG="JsWebClient";
    private JsWebView jsWebView;

    public JsWebClient(JsWebView jsWebView) {
        this.jsWebView = jsWebView;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.e(TAG,url);
      return super.shouldOverrideUrlLoading(view,url);
    }
    //當這個回調(diào)函數(shù)觸發(fā)了webview才可以發(fā)送消息
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

    }
}

簡單封裝

js與android中的通信還是通過字符串來處理的

//傳遞過來
基本的數(shù)據(jù)格式大致為
{
   type:type,//類型
   funName:funName,//當js調(diào)android 讓android是否有回調(diào)方法到j(luò)s中的某個方法
   option:option,//object牡拇,內(nèi)部帶個個參數(shù)
}

//回復js
{
    type:'success'/'fail'...
    data:data
}



 @JavascriptInterface
    public void postMessage(String data) {
        try {
            JSONObject jsonObject = new JSONObject(data);
            String type = jsonObject.getString("type");
            JSONObject option=jsonObject.getJSONObject("option");
            String funName=jsonObject.getString('funName');
            switch(type){
              case 'abc':
                 break;
              case 'bcd':
                 Object obj=....
                 ....
                 dispathMessage(funName,obj,'success')
                 break;
            }
        } catch (JSONException e) {
            e.printStackTrace();
            if(!TextUtils.isEmpt(type)){
              dispathMessage(funName,null,'fail')
            }
        }
    }
 //真正發(fā)送給js 的方法 loadurl是異步加載所以如果在web為加載完成發(fā)送無效果
    public void dispathMessage(String funName,Object obj,String type){
        if(TextUtils.isEmpt(type))return;
        JSONObject jsonObject = new JSONObject();
        try {
            if(obj!=null)
            jsonObject.put("data",new Gson().toJson(obj));
            jsonObject.put("type",type);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        final String javascriptCommand=String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, funName,jsonObject.toString());
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }else{
            ((Activity) getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //此時已在主線程中导俘,可以更新UI了
                    loadUrl(javascriptCommand);
                }
            });
        }
    }

public class BridgeUtil {
    final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:%s('%s')";
}

js的封裝還沒做但是大體是如上

框架分析

接下來看下github上的一個JsBridge這個框架 start約3000+的 他的實現(xiàn)比較完整

這里寫圖片描述

這是他的具體實現(xiàn) 代碼不多 其中assets中的js文件是為了注入他的封裝的js代碼 他的交互方式與上面不同具體實現(xiàn)

這是他封裝的webview
public class BridgeWebView extends WebView implements WebViewJavascriptBridge {

    private final String TAG = "BridgeWebView";

    //加載自己的js文件 趟畏,然后別人的html調(diào)用它的js來傳遞消息
    public static final String toLoadJs = "WebViewJavascriptBridge.js";
     //callId為鍵滩租,記錄回調(diào)的集合
    Map<String, CallBackFunction> responseCallbacks = new HashMap<String, CallBackFunction>();
    //本地注冊一個方法名讓js調(diào)用
    Map<String, BridgeHandler> messageHandlers = new HashMap<String, BridgeHandler>();
    BridgeHandler defaultHandler = new DefaultHandler();

    //發(fā)送消息的對象律想,因為webview加載要時間所有他這里先用隊里存儲
    private List<Message> startupMessage = new ArrayList<Message>();

    public List<Message> getStartupMessage() {
        return startupMessage;
    }

//發(fā)送消息
private void queueMessage(Message m) {
            //這邊只有只為null的時候才會進入發(fā)送消息,置為null是在BridgeWebViewClient的onPageFinished中
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }


public class BridgeWebViewClient extends WebViewClient {
    private BridgeWebView webView;

    public BridgeWebViewClient(BridgeWebView webView) {
        this.webView = webView;
    }

    //這就是他處理交互的另一個方式
    @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);
        }
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
    }

    //這里是是webview加載完成做到的操作
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

        if (BridgeWebView.toLoadJs != null) {
            //在當前webview中加載一段自己的js文件
            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
        }

        //
        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            //這個里webview加載完成將隊列之為null 之后就會走dispatchMessage這個方法
            webView.setStartupMessage(null);
        }
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
    }
}

我們觀察的他的封裝對象和交互方式

public class Message {
    //每個message都有一個callbackid 傳遞給js ,js吧這個返回給客戶端然后客戶端在responseCallbacks中查找返回id的方法
    private String callbackId; //callbackId
    //回復id
    private String responseId; //responseId
    private String responseData; //responseData
    private String data; //data of message
    private String handlerName; //name of handler
}

BridgeUtil 對象封裝了js與android之間的數(shù)據(jù)傳遞格式根據(jù)format 處理后做相應(yīng)的操作

他的交互方式放在了BridgeWebViewClient的shouldOverrideUrlLoading 這個方法

shouldOverrideUrlLoading是當網(wǎng)頁的超鏈接相應(yīng)是回調(diào)到WebClinet的shouldOverrideUrlLoading方法中return true本地處理
false是調(diào)整相應(yīng)的鏈接

然后我們看他的js代碼

我們直接看他發(fā)送消息的代碼
 //sendMessage add message, 觸發(fā)native處理 sendMessage
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }

        //這就是message隊列 隊列的原因后面會將
        sendMessageQueue.push(message);
        //這個messagingIframe.src 設(shè)置這一串東西
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


//messagingIframe 是iframe元素 添加到element中設(shè)置為不可見 然后在發(fā)送數(shù)據(jù)中設(shè)置src
   function _createQueueReadyIframe(doc) {
        messagingIframe = doc.createElement('iframe');
        messagingIframe.style.display = 'none';
        doc.documentElement.appendChild(messagingIframe);
    }

//在iframe的src屬性設(shè)置url webviewclient中的對url進行攔截做處理
   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);
        }
    }

_doSent發(fā)送的方法里面的src的內(nèi)容最終會進入 webView.flushMessageQueue(); 因為字符串匹配的原因


 //這里的_fetchQueue就是js中的_fetchQueue方法 實際調(diào)用了那個方法
    final static String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();";
void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    // deserializeMessage
                    //js發(fā)送過來的數(shù)據(jù)分離出來相應(yīng)的去處理
                    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
                        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
                            final String callbackId = m.getCallbackId();
                            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);
                                    }
                                };
                            } else {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        // do nothing
                                    }
                                };
                            }
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } else {
                                handler = defaultHandler;
                            }
                            if (handler != null){
                                handler.handler(m.getData(), responseFunction);
                            }
                        }
                    }
                }
            });
        }
    }

 function _fetchQueue() {
        //將之前的message都序列出來一次發(fā)送到客戶端
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //android can't read directly the return data, so we can reload iframe src to communicate with java
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
    }

    //將callback放到responseCallbacks
    public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl);
        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
    }
至于android 調(diào)用js代碼
//message中已經(jīng)包含了方法名
final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";
void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

查看js中


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

/提供給native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            //解析message
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接發(fā)送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }
                 
                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    //根據(jù)message中方法名去messageHandlers中去找,messageHandlers是js中注冊了的function集合
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }

android 調(diào)試js代碼

之前代碼中有一段

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            setWebContentsDebuggingEnabled(true);
        }

開啟后 在chrome瀏覽器中 輸入chrome://inspect 然后能找到當前應(yīng)用有個insert按鈕

這里寫圖片描述

然后就可以開始搞事了
以上是jsbridge具體實現(xiàn)的思路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市适刀,隨后出現(xiàn)的幾起案子煤蹭,更是在濱河造成了極大的恐慌,老刑警劉巖然遏,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吧彪,死亡現(xiàn)場離奇詭異姨裸,居然都是意外死亡怨酝,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門赡艰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斤葱,“玉大人慷垮,你說我怎么就攤上這事∽岫椋” “怎么了料身?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衩茸。 經(jīng)常有香客問我芹血,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任幔烛,我火速辦了婚禮啃擦,結(jié)果婚禮上饿悬,老公的妹妹穿的比我還像新娘令蛉。我一直安慰自己,他們只是感情好乡恕,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布言询。 她就那樣靜靜地躺著,像睡著了一般傲宜。 火紅的嫁衣襯著肌膚如雪运杭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天函卒,我揣著相機與錄音辆憔,去河邊找鬼。 笑死报嵌,一個胖子當著我的面吹牛虱咧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锚国,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼腕巡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了血筑?” 一聲冷哼從身側(cè)響起绘沉,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豺总,沒想到半個月后车伞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡喻喳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年另玖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片表伦。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡谦去,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蹦哼,到底是詐尸還是另有隱情哪轿,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布翔怎,位于F島的核電站窃诉,受9級特大地震影響杨耙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜飘痛,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一珊膜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宣脉,春花似錦车柠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至羊苟,卻和暖如春塑陵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜡励。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工令花, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凉倚。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓兼都,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稽寒。 傳聞我的和親對象是個殘疾皇子扮碧,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容