简化Android与JS交互,JsBridge框架全面解析

简化Android与JS交互,JsBridge框架全面解析




今日科技快讯



近日,滴滴顺风车披露了一组数字,预测春运前后,跨城出行以7天为一个周期,呈“潮汐式”变化。2月13号,有将近100万人乘滴滴顺风车踏上归途;除夕当天,还有近40万在回家的顺风车上。返程巅峰集中在2月23日前后,23日预计有超过110万人乘坐滴滴顺风车返回工作岗位。



作者简介



本篇文章来自 

juexingzhe 

的投稿。主要介绍了Android与JS的交互框架JsBridge源码分析的相关知识

,希望对大家有所帮助!

juexingzhe

 的博客地址

http://www.jianshu.com/u/ea71bb3770b4



前言



在Android开发中,由于Native开发的成本较高,H5页面的开发更灵活,修改成本更低,因此前端网页JavaScript(下面简称JS)与Java之间的互相调用越来越常见。

JsBridge就是一个简化Android与JS通信的框架,源码地址:

http://github.com/lzyzsd/JsBridge



正文



我们今天通过一个简单例子来分析下开源框架JsBridge的源码。例子的代码我也放在Github,有需要的可以seesee:

http://github.com/juexingzhe/Android_JS

例子很简单,随便输入信息登陆,会加载一个H5页面,在H5界面点击按钮,Java执行getUserInfo()然后将UserInfo回传给JS,H5页面再显示UserInfo。

简化Android与JS交互,JsBridge框架全面解析

1.png

简化Android与JS交互,JsBridge框架全面解析

2.png

简化Android与JS交互,JsBridge框架全面解析

3.png

JS调用Android基本有下面三种方式

  • webView.addJavascriptInterface()

  • WebViewClient.shouldOverrideUrlLoading()

  • WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分别回调拦截JS对话框alert()、confirm()、prompt()消息

  • Android调用JS

  • webView.loadUrl();

  • webView.evaluateJavascript()

  • 常用方法的使用后面例子中会用到,更细节的介绍各位同学可以去网上搜搜看看。

    JsBridge使用

    我们先来看下Java层的代码,首先引入依赖和仓库

    dependencies

    {   ……    

    compile

    "com.github.lzyzsd:jsbridge:1.0.4"

       

    compile

    "com.google.code.gson:gson:2.7"

    }

    repositories

    {    jcenter()    maven { url

    "http://jitpack.io"

    } }

    准备工作就是这样,下面可以开始撸代码,首先就是点击按钮登陆,这个简单:

    Intent intent =

    new

    Intent(LoginActivity.

    this

    , WebActivity.class); intent.putExtra(

    "email"

    , mEmailView.getText().toString()); startActivity(intent);

    布局文件中要使用BridgeWebView:

    <

    LinearLayout

    xmlns:android

    =

    "http://schemas.android.com/apk/res/android"

       

    android:layout_width

    =

    "match_parent"

       

    android:layout_height

    =

    "match_parent"

       

    android:orientation

    =

    "vertical"

    >

       

    <

    com.github.lzyzsd.jsbridge.BridgeWebView

           

    android:id

    =

    "@+id/web_view"

           

    android:layout_width

    =

    "match_parent"

           

    android:layout_height

    =

    "match_parent"

    />



    </

    LinearLayout

    >

    在跳转后的页面,获取登陆信息并存储,再通过loadUrl加载H5页面:

    Intent intent =

    this

    .getIntent(); String email = intent.getStringExtra(

    "email"

    ); mUserInfo =

    new

    UserInfo(email); mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view); mBridgeWebView.setDefaultHandler(

    new

    DefaultHandler()); mBridgeWebView.loadUrl(

    "file:///android_asset/getuserinfo.html"

    ); registerHandler();

    主要是要注册Handler,供JS调用,getUserInfo就是注册供JS调用的Handler的id,data是JS传过来的参数,CallBackFunction 函数中需要把JS需要的response返回给JS

    private

    void

    registerHandler

    ()

    {    mBridgeWebView.registerHandler(

    "getUserInfo"

    ,

    new

    BridgeHandler() {        

    @Override

           

    public

    void

    handler

    (String data, CallBackFunction function)

    {            Log.i(TAG,

    "handler = getUserInfo, data from web = "

    + data);            function.onCallBack(

    new

    Gson().toJson(mUserInfo));        }    }); }

    Java层的代码就这么简单,下面看下JS层工作:

    首先需要一个js文件,我们写一个getuserinfo.html文件放在assets目录下,文件内容,不建议把js代码直接放在html文件中,我为了方便直接就写在这了。代码放了两个段落,一个类似于TextView用来显示用户信息,一个Button。点击按钮会调用callHandler,三个参数和Java层一一对应,在Java层返回的时候,会调用function(responseData)函数,显示用于信息。

    <

    html

    >



    <

    head

    >

       

    <

    meta

    content

    =

    "text/html; charset=utf-8"

    http-equiv

    =

    "content-type"

    >

       

    <

    title

    >

           js调用java    

    </

    title

    >



    </

    head

    >



    <

    body

    >



    <

    p

    >

       

    <

    xmp

    id

    =

    "show"

    >

       

    </

    xmp

    >



    </

    p

    >



    <

    div

    align

    =

    "center"

    >

       

    <

    p

    >

           

    <

    input

    type

    =

    "button"

    id

    =

    "enter"

    value

    =

    "获取用户信息"

    onclick

    =

    "getUserInfo();"

           />

       

    </

    p

    >



    </

    div

    >



    </

    body

    >



    <

    script

    >

       

    function

    getUserInfo

    ()

    {        

    window

    .WebViewJavascriptBridge.callHandler(            

    "getUserInfo"

    ,            {

    "info"

    :

    "I am JS, want to get UserInfo from Java"

    },            

    function

    (responseData)

    {                

    document

    .getElementById(

    "show"

    ).innerHTML =

    "repsonseData from java,\ndata = "http://www.gunmi.cn/v/

    + responseData;            }        )    }

    </

    script

    >



    </

    html

    >

    使用基本就是这样了,可以看出来JsBridge通过封装,JS和Java之间的通信只需要实现两个步骤,使用起来很方便。

    JsBridge源码分析

    分析之前我把JS调用Java画了个简易交互图,Java调用JS的过程类似:

    简化Android与JS交互,JsBridge框架全面解析

    4.png

    是不是感觉反而更复杂了???其实只要捉住主要的三点,JsBridge就原形毕露:

  • Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象

  • JS调用Android是通过shouldOverrideUrlLoading

  • JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输

  • 接下来我们来一步一步跟踪上面例子的调用过程:

  • JS层点击按钮调用callHandler

  • handlerName,Java和JS要一致,data是Java层handlerName函数执行的参数。responseCallback是Java执行完handlerName返回时,JS回调的接口,是JS执行

    onclick=

    "getUserInfo();"





    function

    getUserInfo

    ()

    {    

    window

    .WebViewJavascriptBridge.callHandler(            

    "getUserInfo"

    ,            {

    "info"

    :

    "I am JS, want to get UserInfo from Java"

    },            

    function

    (responseData)

    {                

    document

    .getElementById(

    "show"

    ).innerHTML =

    "repsonseData from java,\ndata = "http://www.gunmi.cn/v/

    + responseData;            }    ) }

    callHandler会调用_doSend。如果JS需要回调,就将回调的callbackId放进message中,Java执行完会传回callbackId,这里是cb_1_1495182409011,构造完message放进队列sendMessageQueue。通过iframe属性给Java发送通知消息,消息结构为:

    yy://QUEUE_MESSAGE/

    function

    callHandler

    (handlerName, data, responseCallback)

    {    _doSend({        handlerName: handlerName,        data: data    }, responseCallback); }

    function

    _doSend

    (message, responseCallback)

    {        

    if

    (responseCallback) {            

    var

    callbackId =

    "cb_"

    + (uniqueId++) +

    "_"

    +

    new

    Date

    ().getTime();            responseCallbacks[callbackId] = responseCallback;            message.callbackId = callbackId;        }        sendMessageQueue.push(message);        messagingIframe.src = http://www.gunmi.cn/v/CUSTOM_PROTOCOL_SCHEME +

    "://"

    + QUEUE_HAS_MESSAGE; }
  • Java收到通知消息

  • WebView在shouldOverrideUrlLoading拦截到url:yy://QUEUE_MESSAGE/

    然后会执行webView.flushMessageQueue(),在主线程执行loadUrl通知JS层推送队列到Java;

    void

    flushMessageQueue

    ()

    {      

    if

    (Thread.currentThread() == Looper.getMainLooper().getThread()) {           loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,

    new

    CallBackFunction() {                

    @Override

                   

    public

    void

    onCallBack

    (String data)

    {                    

    //

                   });      } }

    public

    void

    loadUrl

    (String jsUrl, CallBackFunction returnCallback)

    {      

    this

    .loadUrl(jsUrl);      responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback); }
  • JS 发送Request Queue

  • 执行_fetchQueue,将sendMessageQueue转化成JSON。通过iframe属性给Java发送通知消息。

    function

    _fetchQueue

    ()

    {    

    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 = http://www.gunmi.cn/v/CUSTOM_PROTOCOL_SCHEME +

    "://return/_fetchQueue/"

    +

    encodeURIComponent

    (messageQueueString); }
  • Java收到调用通知

  • 进行处理并发送Response Queue到JS,WebView在shouldOverrideUrlLoading会拦截到url,执行webView.handlerReturnData(url);根据函数名_fetchQueue拿到之前注册的回调函数CallBackFunction returnCallback,执行回调函数,并且从注册中移除。

    void

    handlerReturnData

    (String url)

    {      String functionName = BridgeUtil.getFunctionFromReturnUrl(url);      CallBackFunction f = responseCallbacks.get(functionName);      String data = http://www.gunmi.cn/v/BridgeUtil.getDataFromReturnUrl(url);

    if

    (f !=

    null

    ) {           f.onCallBack(data);           responseCallbacks.remove(functionName);          

    return

    ;      } }

    接下来就是对Request Queue的解析然后找到JS希望调用Handler并且执行,代码中我写了注释,可以直接看:

    //回调接口执行onCallBack函数

    //其中data [{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]

    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

    {                      

    //将JSON数组转化成Java list

                         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++) {                      

    //从list中取出Message

                         Message m = list.get(i);                      

    //在我们的例子中没有responseId,因此到else分支

                         String responseId = m.getResponseId();                       

    // 是否是response

                         

    if

    (!TextUtils.isEmpty(responseId)) {                           CallBackFunction function = responseCallbacks.get(responseId);                           String responseData = http://www.gunmi.cn/v/m.getResponseData(); function.onCallBack(responseData); responseCallbacks.remove(responseId); }

    else

    {                           CallBackFunction responseFunction =

    null

    ;                          

    // if had callbackId

                             

    //如果有callbackId就说明JS需要回调,因此Java层需要构造responseMsg



                               

    //从message中取出callbackId,放进responseMsg

                             

    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;                          

    //从message中取出Handler名字,再从messageHandlers中取

                             

    //如果没有就使用默认的Handler

                             

    if

    (!TextUtils.isEmpty(m.getHandlerName())) {                                handler = messageHandlers.get(m.getHandlerName());                           }

    else

    {                                handler = defaultHandler;                           }                            

    if

    (handler !=

    null

    ){                                

    //执行handler

                                    handler.handler(m.getData(), responseFunction);                           }                              }                         }                    }               });          } }

    那么这个handler是什么?就是Java调用registerHandler注册的getUserInfo

    private

    void

    registerHandler

    ()

    {        mBridgeWebView.registerHandler(

    "getUserInfo"

    ,

    new

    BridgeHandler() {            

    @Override

               

    public

    void

    handler

    (String data, CallBackFunction function)

    {                Log.i(TAG,

    "handler = getUserInfo, data from web = "

    + data);                function.onCallBack(

    new

    Gson().toJson(mUserInfo));            } }

    上面的function就是在flushMessageQueue 解析时构造的responseFunction,在message中包括JS层需要回调的函数Id,然后就是getUserInfo执行的结果。调用queueMessage

    responseFunction =

    new

    CallBackFunction() {          

    @Override

             

    public

    void

    onCallBack

    (String data)

    {                    Message responseMsg =

    new

    Message();                    responseMsg.setResponseId(callbackId);                    responseMsg.setResponseData(data);                    queueMessage(responseMsg);          } };

    通过构造String指令,然后loadUrl执行JS代码,注意对象也是通过这样方式传递过去的,就类似调用本地函数,不发起网络请求

    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);        } }

    其中

    BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA =

    "javascript:WebViewJavascriptBridge._handleMessageFromNative("%s");"



    javascriptCommand = javascript:WebViewJavascriptBridge._handleMessageFromNative(

    "{\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182558893\"}"

    );

    //data = http://www.gunmi.cn/v/{/"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182409011\"}

  • JS收到Response JSON

  • 来到_handleMessageFromNative

    function

    _handleMessageFromNative

    (messageJSON)

    {        

    console

    .log(messageJSON);        

    if

    (receiveMessageQueue && receiveMessageQueue.length >

    0

    ) {            receiveMessageQueue.push(messageJSON);        }

    else

    {            _dispatchMessageFromNative(messageJSON);        } }

    最后都会调用到_dispatchMessageFromNative,由于是JS主动调用Java,因此有responseId,执行registerHandler时传入的CallBack,也就是显示用户信息。我在代码里加了注释,很容易看懂。

    function

    _dispatchMessageFromNative

    (messageJSON)

    {        setTimeout(

    function

    ()

    {            

    //将数据解析成JSON

               

    var

    message =

    JSON

    .parse(messageJSON);            

    var

    responseCallback;            

    //java call finished, now need to call js callback function

               

    //根据responseId:cb_1_1495182409011拿到responseCallback,就是我们前门注册的alert

               

    if

    (message.responseId) {                responseCallback = responseCallbacks[message.responseId];                

    if

    (!responseCallback) {                    

    return

    ;                }                responseCallback(message.responseData);                

    delete

    responseCallbacks[message.responseId];            }

    else

    {                

    //直接发送

                   

    if

    (message.callbackId) {                    

    var

    callbackResponseId = message.callbackId;                    responseCallback =

    function

    (responseData)

    {                        _doSend({                            responseId: callbackResponseId,                            responseData: responseData                        });                    };                }                

    var

    handler = WebViewJavascriptBridge._messageHandler;                

    if

    (message.handlerName) {                    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);                    }                }            }        }); }

    源码的分析就到这结束了,代码不多,但是封装的接口很是好用。



    总结



    最后总结下,使用上很方便主要两个步骤

    被调用方注册Handler

    registerHandler(String handlerName, BridgeHandler handler)

    调用方调用Handler

    callHandler(String handlerName, String data, CallBackFunction callBack)

    原理上还是那三句话,请原谅我从上面直接copy过来:

  • Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象

  • JS调用Android是通过shouldOverrideUrlLoading

  • JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输

  • 好了,今天我们JsBridge的使用和源码分析就到这了,谢谢!

    文中例子的链接:

    http://github.com/juexingzhe/Android_JS



    欢迎长按下图 -> 识别图中二维码

    或者 扫一扫 关注我的公众号

    简化Android与JS交互,JsBridge框架全面解析

    简化Android与JS交互,JsBridge框架全面解析