Android webView与js 交互以及jsbridge框架源码分析

简书链接:http://www.jianshu.com/p/cdf12901c813
最近在处理android webView与js的通信上的问题,作为总结

1.简单篇

如何实现简单的android 调用js 与js调用android

   让webview做一下操作
  private void init(Context context){
        WebSettings setting =getSettings();
        setting.setJavaScriptEnabled(true);//支持js
        setWebViewClient(new WebClient(this));
        setWebChromeClient(new WebChromeClient());
        //添加js调用android的方法这是关键 前者是个对象,后者是个字符串 在js中是在window.android可以直接获取到
        addJavascriptInterface(new JavaScriptinterface(context,this),
                "android");
        //这是开启js的调试下面再讲
        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 要调用这个方法调用window.anroid.toastMessage('即可')

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

这是js调用android中的方法

android 调用js 的方法


<script>
function jsalert(data) {
    alert(data);
}
script>
//java这样调用即可 方法前需要加javascript
webview.loadUrl('javascript:jsalert('111')')

注意点

  1. 需要在主线程调用才会生效
  2. 如果在oncreate中直接调用loadurl不会生效原因是 webview需要加载完成才能调用webview加载完成的回调函数是在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);
    }
    //当这个回调函数触发了webview才可以发送消息
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

    }
}

简单封装

js与android中的通信还是通过字符串来处理的

//传递过来
基本的数据格式大致为
{
   type:type,//类型
   funName:funName,//当js调android 让android是否有回调方法到js中的某个方法
   option:option,//object,内部带个个参数
}

//回复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')
            }
        }
    }
 //真正发送给js 的方法 loadurl是异步加载所以如果在web为加载完成发送无效果
    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的封装还没做但是大体是如上

JsBridge框架分析

接下来看下github上的一个JsBridge这个框架 start约3000+的 他的实现比较完整

Android webView与js 交互以及jsbridge框架源码分析_第1张图片

这是他的具体实现 代码不多 其中assets中的js文件是为了注入他的封装的js代码 他的交互方式与上面不同具体实现

这是他封装的webview
public class BridgeWebView extends WebView implements WebViewJavascriptBridge {

    private final String TAG = "BridgeWebView";

    //加载自己的js文件 ,然后别人的html调用它的js来传递消息
    public static final String toLoadJs = "WebViewJavascriptBridge.js";
     //callId为键,记录回调的集合
    Map responseCallbacks = new HashMap();
    //本地注册一个方法名让js调用
    Map messageHandlers = new HashMap();
    BridgeHandler defaultHandler = new DefaultHandler();

    //发送消息的对象,因为webview加载要时间所有他这里先用队里存储
    private List startupMessage = new ArrayList();

    public List getStartupMessage() {
        return startupMessage;
    }

//发送消息
private void queueMessage(Message m) {
            //这边只有只为null的时候才会进入发送消息,置为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)) { // 如果是返回数据
            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之间的数据传递格式根据format 处理后做相应的操作

他的交互方式放在了BridgeWebViewClient的shouldOverrideUrlLoading 这个方法

shouldOverrideUrlLoading是当网页的超链接相应是回调到WebClinet的shouldOverrideUrlLoading方法中return true本地处理
false是调整相应的链接

然后我们看他的js代码

我们直接看他发送消息的代码
 //sendMessage add message, 触发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 设置这一串东西
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


//messagingIframe 是iframe元素 添加到element中设置为不可见 然后在发送数据中设置src
   function _createQueueReadyIframe(doc) {
        messagingIframe = doc.createElement('iframe');
        messagingIframe.style.display = 'none';
        doc.documentElement.appendChild(messagingIframe);
    }
//在iframe的src属性设置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)) { // 如果是返回数据
            webView.handlerReturnData(url);
            return true;
        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, url);
        }
    }

_doSent发送的方法里面的src的内容最终会进入 webView.flushMessageQueue(); 因为字符串匹配的原因


 //这里的_fetchQueue就是js中的_fetchQueue方法 实际调用了那个方法
    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发送过来的数据分离出来相应的去处理
                    List 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都序列出来一次发送到客户端
        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);
    }

查看js中


    //提供给native调用,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 {
                //直接发送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    //根据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);
                    }
                }
            }
        });
    }

以上是jsbridge具体实现的思路

页面加载成功会一次发送receiveMes..中的给个任务然后,_dispatchMessageFromNative的方提供message中已经包含了足够多的信息,然后在判断有没有回调id 调用回调,把保存在responseCallback中的方法移除,
如果没有,查看是否是注册的方法,这里客户端的回调信息是存到sendMessageQueue队列中,确实对于js来说是该发送是信息,所以sendMessageQueue队列中,所有这里他采用数组的形式,存放的不知是js主动发送的消息,而且还存储客户端需要的回调信息
内部容易让人混乱的是Message中的callbackId responseId  对于谁是发送者和接受者的转换,对于发送者需要回调的时候,
接受者将这个请求id设置为回调id,反之一样,两个id的转换在于发送者和接受者的区别

android 调试js代码

之前代码中有一段

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

开启后 在chrome浏览器中 输入chrome://inspect 然后能找到当前应用有个insert按钮Android webView与js 交互以及jsbridge框架源码分析_第2张图片

然后就可以开始搞事了

你可能感兴趣的:(Android)