×
广告

自定义Android-JsBridge

96
javalong
2018.06.29 18:14* 字数 1821

github:https://github.com/javalong/jsbridge

整体架构图
初始化事件触发
相互调用流程
源码分析

下面我们就根据上面架构图的顺序,然后贴上源码分析。

如上图,源码分析,我们分2步


初始化事件触发
  1. 初始化
    第一步骤中我又分了几个小步

    1. 初始化BridgeHelper,对WebViewClient使用代理模式
         webView.webViewClient = object : WebViewClientProxy(webViewClient) {}
        
        //复写WebViewClient所有的方法
        open class WebViewClientProxy(val client: WebViewClient?) : WebViewClient() {
    
                override fun onPageFinished(view: WebView?, url: String?) {
                    if (client == null) {
                        super.onPageFinished(view, url)
                } else {
                      client.onPageFinished(view, url)
               }
              ....
              ....
        }
    

    这里使用代理模式的原因是,用户很可能设置了自己的WebViewClient,但是我们也需要在WebViewClient中复写相关方法,使用代理模式的话可以让用户和我定义自己的处理方法。

    如:

       override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                //先去加载js
                webView?.loadUrl("javascript:" + assetFile2Str(webView?.context, "bridge.js"))
            }
            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
                    ...
                   ...
                   ...
                 else {
                    return super.shouldOverrideUrlLoading(view, url)
                }
                return true
            }

这里2个方法就是在jsbridge框架中复写的2个方法。

  • onPageFinished,在页面加载结束后,去加载bridge.js
  • shouldOverrideUrlLoading,接收页面的跳转

但是用户很可能也会复写这2个方法,因为这是很通用的方法。所以这里采用代理模式,就可以破解这个难题。

  1. Webview加载对应的url,加载完成后,再去加载bridge.js

前面已经介绍了,会在页面加载成功后,再去载入bridge.js

   webView?.loadUrl("javascript:" + assetFile2Str(webView?.context, "bridge.js"))

这里其实很简单,就是直接读取assets中的bridge.js转化为字符串。但是这里有一个坑。因为我习惯性在写js的时候不在句尾加;,在这里会有问题,因为会被直接把整个js文件解析为一行。如下错误

image.png

就是没有加上;号。

  1. bridge.js中触发onBridgeLoaded事件,通知js端和native端。
    var Bridge = window.Bridge = {
         registerHandler: registerHandler,
         pushCallFunc: pushCallFunc,
         callSyncHandler: callSyncHandler,
         callAsyncHandler: callAsyncHandler,
         executeBridgeFunc: executeBridgeFunc,
         executeCallFunc: executeCallFunc
     };

     var evt = document.createEvent("Events");
     evt.initEvent("onBridgeLoaded");
     document.dispatchEvent(evt);
     //同时也发送信息告诉native端,bridge加载完成
     location.href = JSBRIDGE_LOADED;

创建Bridge对象后,创建onBridgeLoaded事件,并发送。
然后再调用location.href触发原生的shouldOverrideUrlLoading回调。

    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
          ...     
           if (url.startsWith(JSBRIDGE_LOADED)) {
                    //jsbridge加载成功
                    jsbridgeLoaeded = true
                    dealMessagesPreLoaded()
                    //todo 触发回调
                } else {
                    return super.shouldOverrideUrlLoading(view, url)
                }
                return true
            }

      //处理在jsbridge加载完成之前 所调用的方法
    private fun dealMessagesPreLoaded() {
        for (i in 0 until asyncMessages.size) {
            var msg = asyncMessages.poll()
            doSendMessageToJS(msg)
        }

        if (syncMessages.size > 0) {
            syncMessageLoop = true
            var msg = syncMessages.poll()
            doSendMessageToJS(msg)
        }
    }

为了用户使用的方便,所以在native端,无需等待onBridgeLoaded事件触发都可以调用call*Handler方法。所以当onBridgeLoaded事件触发后,需要在队列中找到已经加入的消息。然后从异步消息队列中一个个取出来直接发送。从同步消息队列中取出一个发送。这里所涉及到的同步消息队列和异步消息队列在后面会做详细的说明。


相互调用流程
  1. native/js相互调用
    如图所示
    native/js相互调用的过程个有4个步骤,也就是8个步骤,那么我们就按顺序分析下。

native调用js四步骤

  1. call*Handler调用js端注册的方法
    js端注册
 document.addEventListener(
     'onBridgeLoaded', function () {
         Bridge.registerHandler("testNative", function (data, callback) {
             console.log("native成功调用js中注册的方法testNative")
             callback(data);
         })
     })

native端 call*Handler调用

先以callAsyncHandler为例

   var mockData1 = JSONObject()
      mockData1.put("data1", "111")
      mockData1.put("data2", "222")
      //异步调用js方法
      helper.callAsyncHandler("testNative", mockData1, object : ResponseCallback {
          override fun call(data: JSONObject?) {
              Log.e(TAG, "native异步调用js中注册的方法testNative,js在执行注册方法时回调callback方法,并传入参数:" + data?.toJSONString())
          }
      })

第三个参数是ResponseCallback的子类,在js中调用callback(data)方法后会执行。后面会介绍,第一步主要介绍调用call*Handler做了哪些操作。

  fun callAsyncHandler(handlerName: String, param: JSONObject, callback: ResponseCallback) {
        var funcKey = pushCallFunc(callback)
        //回调方法对应的key需要放入Message对象传给js,因为js那么需要调用callback方法时需要再传回来
        var msg = Message(handlerName, param, funcKey, true)
        //这里主要是为了区分连续调用多次callSyncHandler时,时间上是相同的,这样会把前面的回调方法给覆盖了
        this.unique++
        //如果bridge还未加载完成,就先存入队列,如果完成了,就直接发送
        if (jsbridgeLoaeded) {
            //调用js方法
            doSendMessageToJS(msg)
        } else {
            asyncMessages.offer(msg)
        }
    }


    //如果回调方法不为null,就创建一个key存入map
    private fun pushCallFunc(callFunc: ResponseCallback): String {
        var funcKey = NO_CALL_FUNC_KEY
        if (callFunc != null) {
            funcKey = PREFEX_CALL_FUNCS_KEY + this.unique + System.currentTimeMillis()
            this.callFuncs[funcKey] = callFunc
        }
        return funcKey
    }

    //直接调用js方法Bridge.executeBridgeFunc('%s')
    private fun doSendMessageToJS(message: Message) {
        webView.loadUrl("javascript:" + String.format(CALL_JSBRIDGEMETHOD_FUNCNAME, URLEncoder.encode(JSONObject.toJSONString(message), "utf-8")))
    }

注释写的应该相对来说比较清楚了。这里再说明下。
因为调用js后,js还可以通过callback回调。所以这里采用了key-value的方式保存ResponseCallback对象。key是通过当前时间和unique拼成,确保唯一性。
doSendMessageToJS方法中才是真正发起调用js方法。这里有一个麻烦的地方,这里的Message对象会转成json字符串,当做参数调用js方法。
这里的方法是Bridge.executeBridgeFunc('%s'),这里使用了单引号传入参数。因为json字符串中有很多双引号,会造成冲突,当然也可以使用转义字符(某个著名的jsbridge框架是这样用的),但是我觉得我这样更方便。

  1. 调用js中的Bridge.executeBridgeFunc方法
        executeBridgeFunc = function (msg) {
            msg = JSON.parse(msg);
            //判断bridgeFuncs是否注册了对应的方法
            if (bridgeFuncs[msg.handlerName] != null) {
                //如果有就直接执行,并且传入一个回调方法
                bridgeFuncs[msg.handlerName](msg.data, function (responseParam) {
                    //当调用了callback方法后会进入这里
                    if (responseParam == null) {
                        responseParam = {};
                    }
                    //这里需要传入参数是否是同步的消息,为了native端能够继续执行同步消息
                    responseParam.sync = msg.sync;
                    //封装一个消息对象,传入native发送过来的消息的对应的回调方法的key
                    var callbackMsg = {
                        data: responseParam,
                        id: msg.callbackKey
                    };
    
                    //由于location.href频繁调用会造成消息丢失,只能接收到最后的一个location.href,所以这里需要把消息存入队列,一次性发送
                    callbackMessages.push(callbackMsg);
                    if (!callbackMessageWait) {
                        callbackMessageWait = true;
                        setTimeout(function () {
                            //执行完毕bridge后,回调native方法
                            location.href = CALL_BACK_URL + "?messages=" + encodeURI(JSON.stringify(callbackMessages));
                            //发送完成后需要清空数据,防止重复发送数据
                            callbackMessages = [];
                            callbackMessageWait = false;
                        }, WAIT_TIME_OUT);
                    }
                })
            }
        };
    

在js中调用Bridge.registerHandler会把对应的func存入bridgeFuncs中。当native调用方法是会从bridgeFuncs中取出调用。这里最关键的部分是。

  • callback的参数的拼装
    需要传入sync(是否是同步消息),id(native端的回调的方法对应的key)
  • 等待一个短时间,把这期间内的所有的callback的消息一起返回
    注释中也有提到,不能频繁的调用location.href。这样native中的shouldOverrideUrlLoading只会接收到最后一次的location.href的变化。

这里目前看来没有坑,但是我估计可能是有坑的,因为某个著名的jsbridge中没有用到location.href,而是创建了一个iframe然后不断改变他的src。以后我遇到问题了再改进。

  1. 调用native的callback
   location.href = CALL_BACK_URL + "?messages=" + encodeURI(JSON.stringify(callbackMessages));

把消息拼成了一个数组,然后转成json。调用location.href

  1. native中接收到location.href的变化,然后调用对应的callback方法
   override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
             ...
               } else if (url.startsWith(CALL_BACK_URL)) {
                   //调用方法后回调
                   var uri = Uri.parse(url)
                   var messages = uri.getQueryParameter("messages")
                   executeCallFunc(messages)
               }
             ...
           }

private fun executeCallFunc(messages: String) {

       var jsonArr = JSONObject.parseArray(messages)
       var sync = false
       for (i in 0 until jsonArr.size) {
           var json = jsonArr.getJSONObject(i)
           var data = json.getJSONObject("data")
           var id = json.getString("id")
           if (callFuncs.containsKey(id)) {
               callFuncs[id]?.call(json)
               //执行完毕回调后,就把方法移除
               callFuncs.remove(id)
           }
           if (data.getBoolean("sync")) {
               sync = true
           }
       }

       //每次执行完一次callback 就去同步消息中获取数据
       if (sync) {
           var nextMsg = syncMessages.poll()
           if (nextMsg == null) {
               syncMessageLoop = false
               return
           }
           webView.loadUrl("javascript:" + String.format(CALL_JSBRIDGEMETHOD_FUNCNAME, URLEncoder.encode(JSONObject.toJSONString(nextMsg), "utf-8")))
       }
   }

通过传递过来的id找到对应的callback方法并且调用,调用完毕后移除,不然会越来越大。如果messages中有一个消息sync=true,那么就证明是同步消息,需要继续从同步队列中获取消息,然后调用js中的方法。

总结:js调用native其实大同小异这里就不过多介绍了。

同步消息和异步消息

框架中,我把消息分为2类,一种是同步消息,一种是异步消息。

  1. 同步消息:调用后按顺序执行,只有当上一个的消息的callback方法调用后,这个消息才算处理完成,继续从队列中取出下一个消息执行。

  2. 异步消息:可以简单的理解为一起执行,没有顺序的。

不足
  1. 每个注册的方法,尽量都调用他的回调方法,因为我会在回调方法中做一些处理,比如把回调方法移除,判断是否是同步的方法,是同步就继续从队列中拿到下一个消息处理。可能会给用户造成不便。

  2. 代码上封装的不是很好,写的还是比较low

  3. 效率上还没有进行过测试,不知道是否存在一定问题

日记本
Web note ad 1