分析分析jsBridge--Native调用js回调

817 阅读5分钟

为什么要写这篇文章

大家都知道,移动端混合开发时,少不了jsBridge来进行H5和端的通信。而且,jsBridge的运用时间已经很长了。那么有什么必要再拿出来翻炒呢? 其实我在想,大家知道jsBridge,甚至项目中一直在用。 那么作为前端开发者,知道js跟NA是怎么具体通信的么?比如js是怎么调用NA, NA是怎么执行Js方法的?

我们知道这些事后,脑子思维就会很清晰,自己明白,就不怕被别人忽悠。 开发人员使用最多的词汇可能就是"这个实现不了"、"这个实现成本太大"。。。 所以,明白了后,我们直接可以说: 不难,我来写都行。 哈哈

下面我以业界用的比较多的 jsBridge 来进行分析。

这是分析的第一篇。

一、从调用说起

1.1 前端html中的调用

我们的在前端项目中,会在需要跟端交互的页面中,会进行如下初始化:

function connectWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) {
        callback(WebViewJavascriptBridge)
    } else {
        document.addEventListener(
            'WebViewJavascriptBridgeReady'
            , function() {
                callback(WebViewJavascriptBridge)
            },
            false
        );
    }
}

connectWebViewJavascriptBridge(function(bridge) {
    bridge.init(function(message, responseCallback) {
        // 当native发送一个没有HandleName的方法时,执行这里的默认方法
        console.log('JS got a message', message);
        var data = {
            'Javascript Responds': '测试中文!'
        };

        if (responseCallback) {
            console.log('JS responding with', data);
            responseCallback(data);
        }
    });

    // 监听方法,native中执行
    bridge.registerHandler("functionInJs", function(data, responseCallback) {
        document.getElementById("show").innerHTML = ("data from Java: = " + data);
        if (responseCallback) {
            var responseData = "Javascript Says Right back aka!";
            responseCallback(responseData);
        }
    });
})

初始化时, 比较重要的是Bridge.init()

那么,咱们去Native中看看这个init方法是什么。

1.2 Native中init方法

Native中也放了一个js文件: WebViewJavascriptBridge.js

function init(messageHandler) {
    // 进这个逻辑就说明之前init一次了
    if (WebViewJavascriptBridge._messageHandler) {
        throw new Error('WebViewJavascriptBridge.init called twice');
    }
    
    WebViewJavascriptBridge._messageHandler = messageHandler;
    var receivedMessages = receiveMessageQueue;
    receiveMessageQueue = null;
    for (var i = 0; i < receivedMessages.length; i++) {
        _dispatchMessageFromNative(receivedMessages[i]);
    }
}

目前主要的是在这里: WebViewJavascriptBridge._messageHandler = messageHandler; 把我们传的init回调保存进来。

如果比较困惑Native中怎么也有javascript文件, 不用着急,目前知道有这个js文件就好了。

目前init的调用逻辑大体看到了, 下面再看1.1中的 bridge.registerHandler

1.3 前端中的 bridge.registerHandler

在上面1.1中看到了

bridge.registerHandler("functionInJs", function(data, responseCallback) {}

这里在前端js中注册了一个回调,目的是让NA能在某些时机下执行这个 functionInJs函数

那么再看看NA中是怎么处理的:

1.4 Native中的registerHandler方法实现

function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

比较简单,就是把这个回调放到messageHandlers对象中,以供后面某个时间把这个handle拿出来执行。

目前,我认为有这两个用法暂时就够了, 下面就开始深入到java逻辑中,看看这一切是怎么实现的。

二、Native中怎么实现的(android java代码为例)

需要知道的是,H5和NA之间的通信是使用webview来完成的 所以下面的分析会围绕着webview展开。

webview可以简单的理解为android的一个重要组件, 是android中自带的重要功能, 目的就是创建浏览器页面。

那么看看这个webview的使用。

大家如果不熟悉android, 也不要紧,简单看一看

这是native例子中的 MainActivity.java

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   // 获取webview
   webView = (BridgeWebView) findViewById(R.id.webView);

   button = (Button) findViewById(R.id.button);

   button.setOnClickListener(this);


   // 为WebView指定一个WebChromeClient对象,WebChromeClient专门用来辅助WebView处理js的对话框,网站title,网站图标,加载进度条等
   webView.setWebChromeClient(new WebChromeClient() {

      @SuppressWarnings("unused")
      public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType, String capture) {
         this.openFileChooser(uploadMsg);
      }

      @SuppressWarnings("unused")
      public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType) {
         this.openFileChooser(uploadMsg);
      }

      public void openFileChooser(ValueCallback<Uri> uploadMsg) {
         mUploadMessage = uploadMsg;
         pickFile();
      }

      @Override
      public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
         mUploadMessageArray = filePathCallback;
         pickFile();
         return true;
      }
   });

   // 通过WebView.addJavascriptInterface接口向Web页面注入Java对象,页面Javascript脚本可直接引用该对象并调用该对象的方法
   // 比如:  window.WebViewJavascriptBridge.callHandler("submitFromWeb", fn);
   webView.addJavascriptInterface(new MainJavascrotInterface(webView.getCallbacks(), webView), "android");
   webView.setGson(new Gson());
   // 加载具体url
   webView.loadUrl("file:///android_asset/demo.html");


   // 在js中注册了方法,然后java中可以直接执行js中的这个方法
       webView.callHandler("functionInJs", new Gson().toJson(user), new OnBridgeCallback() {
           @Override
           public void onCallBack(String data) {
         Log.d(TAG, "onCallBack: " + data);
           }
       });

       //先把数据保存到队列中,在合适的时候向js发送, bridge.init()能承接
       webView.sendToWeb("hello");

}

从上面看,在java中去调用了functionInJs方法, 结果是把方法名和callback放到队列中,后面会进行发送。 那么放到队列中的逻辑是:

下面的方法,就进入了核心逻辑 BridgeWebview.java

1.callHandler的定义:

public void callHandler(String handlerName, String data, OnBridgeCallback callBack) {
    
    doSend(handlerName, data, callBack);
}

2.doSend方法定义:

class JSRequest {

    public String callbackId;

    public String data;

    public String handlerName;
}
private void doSend(String handlerName, Object data, OnBridgeCallback responseCallback) {
    if (!(data instanceof String) && mGson == null){
        return;
    }
    JSRequest request = new JSRequest();
    if (data != null) {
        request.data = data instanceof String ? (String) data : mGson.toJson(data);
    }
    if (responseCallback != null) {
        String callbackId = String.format(BridgeUtil.CALLBACK_ID_FORMAT, (++mUniqueId) + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
        mCallbacks.put(callbackId, responseCallback);
        request.callbackId = callbackId;
    }
    if (!TextUtils.isEmpty(handlerName)) {
        request.handlerName = handlerName;
    }
    queueMessage(request);
}

3.queueMessage方法定义:

private void queueMessage(Object message) {
    // 如果mMessages是个Arraylist,那么把要请求的message信息先存起来
    if (mMessages != null) {
        mMessages.add(message);
    } else {
        // 正式发送
        dispatchMessage(message);
    }
}

走到这里, 为上面的流程做个小结:

  1. 我们前端代码js中中,在业务代码中对jsBridge进行init和注册了一个方法的监听, 方法监听用于Native执行方法后,执行js的回调,把数据传递给业务js层。

  2. 做了监听后,会把业务层的监听函数注册到Na中的 WebViewJavascriptBridge.js中。

  3. Native代码中,通过callHandler执行方法, 这时会把handleName和回调方法先存入mMessages这个ArrayList中; 同时,sendToWeb('hello') 也会执行 doSend 方法,并且存入mMessage中。注意,这里的handleName是null, 也就是说不会定向发送给某个注册监听函数。 也就是说这时并没有执行,只是先存储起来。

三、正式发送的时机来临

BridgeWebview.java中,因为继承了webview, 而webview有个生命周期钩子是 onLoadFinished:

@Override
public void onLoadFinished() {
    mJSLoaded = true;
    if (mMessages != null) {
        for (Object message : mMessages) {
            dispatchMessage(message);
        }
        mMessages = null;
    }
}

当webview加载完成时, 那么循环mMessages中的数据, 执行 dispatchMessage(message) 这个函数负责执行里面的各个JSRequest实例对象。

private void dispatchMessage(Object message) {
     if (mGson == null){
         return;
     }
     String messageJson = mGson.toJson(message);
     //escape special characters for json string  为json字符串转义特殊字符

     // 系统原生 API 做 Json转义,没必要自己正则替换,而且替换不一定完整
     messageJson = JSONObject.quote(messageJson);

     // BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA => "javascript:WebViewJavascriptBridge._handleMessageFromNative(%s);" 
     String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
     // 必须要找主线程才会将数据传递出去 --- 划重点
     if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT&&javascriptCommand.length()>=URL_MAX_CHARACTER_NUM) {
    // 把字符串command传进去进行解析
   this.evaluateJavascript(javascriptCommand,null);
}else {
   this.loadUrl(javascriptCommand);
}
     }
 }

通过webview组件自带的 evaluateJavascript 解析JavaScript代码 "javascript:WebViewJavascriptBridge._handleMessageFromNative(%s);" %s已经替换成实际字符串。

这时,就会执行WebViewJavascriptBridge.js中的_handleMessageFromNative方法:

function _handleMessageFromNative(messageJSON) {
    console.log('handle message: '+ messageJSON);
    if (receiveMessageQueue) {
        receiveMessageQueue.push(messageJSON);
    }
    _dispatchMessageFromNative(messageJSON);

}


而后执行 _dispatchMessageFromNative, 这个方法中去具体执行在我们前端js中存储的init和 registerHandler 中注册的回调函数,并把data传给回调方法。

function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
        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('response', responseData, callbackResponseId);
                };
            }
            
            var handler = WebViewJavascriptBridge._messageHandler;
            if (message.handlerName) {
                handler = messageHandlers[message.handlerName];
            }
            //查找指定handler
            try {
                // 执行回调,这时,我们前端写的js代码回调函数就执行了。
                handler(message.data, responseCallback);
            } catch (exception) {
                if (typeof console != 'undefined') {
                    console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                }
            }
        }
    });
}

对这个阶段再进行一次总结:

  1. onLoadFinished的时候,正式循环mMessages,执行dispatchMessage方法,通过evaluateJavascript执行对应的WebViewJavascriptBridge中的_handleMessageFromNative方法,把JSRequest实例传进去作为参数。

  2. 然后就到了Native中的那个js文件了(注意,这个问题件并不是我们的业务js文件,一定不要弄混了), 这里面去查找业务js文件中的所有注册回调,只要找到对应的handleName了,就去执行对应的回调, 如果没有找到,就去执行默认的回调(init方法中的回调).

  3. 至此, 我们业务js中的回调函数就执行了, 并且,获取到了端返回的data, 如果有需要,还可以执行JSRequest实例中的端回调函数, 通知端,js已经加载完成了。

至此, 端上主动调用业务js中的方法流程通畅了。