Android中極簡的js與java的交互庫-SimpleJavaJsBridge

按以下順序進行本文章:

  1. 現有js與java通信方案及不足
  2. js與java完美通信方案設計
  3. SimpleJavaJsBridge

現在進入正題

1. 現有js與java通信方案及不足

先來說明一點js與java通信,指的是js既可以給java發送消息,同時java也可以給js發送消息。那就來屢屢它們之間的通信方案。

1.1 java給js發送消息

官方唯一指定方法是通過webview的loadUrl(String)方法進行的,看下的偽代碼:

//例子:調用js的test(param)方法
        webView.loadUrl("javascript:test(1)");

調用方法非常的簡單,"javascript:"+js方法的名字+方法的參數值拼接成一個字符串就可以給js發送消息了,猶如是在直接調用js的方法。

1.2 js給java發送消息

js給java發送消息實際上只有2種方案,依次來分析下這2種方案。

1.2.1 官方方法

先看下偽代碼:

//該類封裝了提供給js調用的方法
        public class JSBridge{
                //提供給js的方法
                public void invokeByJs(String msg){

                }
        }

        //把JSBridge對象注入WebView中,同時起一個別名
        webView.addJavascriptInterface(new JSBridge(),"jsBridge");

        //js調用java方法
        window.jsBridge.invokeByJs('hello java');

這種方法其實是把一個對象注入到WebView中,js給java發送消息的方式是

window.注入WebView的java對象的所對應name值.javaMethod(param...);

這其實也猶如在java代碼中調用java的方法,因為java提供給js的方法名,方法參數是啥,js在發送消息時,方法名與參數必須保持一致,這也是這些java代碼不能進行混淆的原因。

但是這種方法存在一個嚴重的漏洞,雖然官方在android4.4的時候給出了相應的解決方案,但是android4.4以下的版本還得解決該漏洞,因此一些巨人們就開始琢磨著解決這個坑,第二種方法由此誕生。

1.2.2 js傳遞約定好的字符串給java

這種方案的主要原理是:

  • 找到一個js可以給java發送消息的入口(這個入口有onJsPrompt,onJsAlert等等)
  • js通過入口把消息按既定好的規則拼接成字符串傳遞給java
  • java按照既定好的規則對字符串進行解析
  • 根據解析數據,通過反射來調用自己相應方法

這種方法使用起來要比官方方法(第一種方法)麻煩。

1.3 存在的不足

上面介紹了js與java的通信方法,那我就來分析下我認為存在的不足。

1.3.1 java給js發送消息方法和js給java發送消息的官方方法存在的不足

1.3.1.1 強依賴

java給js發送消息的方法,和js給java發送消息的官方方法都存在著強依賴的問題,都要高度依賴對方的方法名字,方法參數。強依賴發生于同一模塊內,個人覺得不是問題甚至是高內聚的體現。但是java與js可以說是處于兩個不同的模塊或者叫兩個不同世界,只要js提供給java的方法發生變化,java也得改動,同理java提供給js的方法也如此。處于兩個不同模塊知道對方的細節越少越好,這樣耦合性就會降低,耦合性降低的好處就不用說了。

1.3.1.2 強依賴導致js需要兼容不同的系統

先看段偽代碼:

function location(){
        //是ios系統,采用給ios發送消息的方法
        if(isIOS){
                給ios發送消息;
        }else if(isAndroid){
                給android發送消息;
        }
  }

上面的代碼展示的是js使用native的定位功能的代碼,因為js在給不同的系統發送消息的方式不一樣,就會出現if else if 這樣的兼容語句。當前js代碼只被ios和android使用,假如還會被wp或pc來使用,那if else if豈不是要惡心死。產生該問題的主要原因是:js代碼在針對不同的系統自己獨有的通信方式進行通信。

1.3.1.3 給不存在的接口發送消息沒反饋

java在給js的一個不存在接口發送消息時,java根本不知道該接口不存在,java只會傻傻的等待。同理js在給java的一個不存在接口發送消息時,js是可以通過捕獲異常來知道該接口不存在,但是這不是最好的解決方案。

給不存在接口發送消息沒反饋會導致js代碼充斥著if else if語句,看段偽代碼:

//調用java的定位方法
  function location(){
        //1.1版本以上才會調用定位功能
        if(androidAppVersion > '1.1'){
                發送消息給java;
        }else{
                給用戶提示,暫不支持定位功能;
        }
  }

這是一段調用java進行定位的js代碼,android app在版本1.1的時候才增加了定位的功能,因此對于1.1以下版本是不支持這功能的,因此js代碼里面非常有必要根據版本號進行判斷。這只是由于版本問題導致if else if的一個小小的縮影。還有一些其他情況導致if else if的產生比如一份js代碼被多個業務使用。

1.3.2 js給java發送消息的第二種方法存在不足

上文提到的js給java發送消息的第二種方法,它解決了存在的漏洞,但是這種方法,使用起來要比第一種方法復雜,java會多做以下工作:

解析js傳遞給java的字符串,把調用的接口,參數解析出來

把調用的接口,參數映射到相應的方法

不論js傳遞給java的字符串是json格式還是其他格式,解析這樣的字符串肯定是一件無趣的重復的體力勞動。

若想解決以上的問題,我們有必要設計一套完美的通信方案。

2. js與java完美通信方案設計

2.1 一套完美的js與java的通信方案應滿足以下幾點:

js與java知道對方的細節越少越好,越少它們的耦合性越低。那到底多少為好呢?我個人覺得互相暴漏給對方一個接口足矣。這樣js與native的通信就類似于通過一個管道通信或者說類似于socket通信(降低強依賴)

js與java之間通信,需要定義好一套通信協議或者叫通信規則,在管道之間傳遞通信協議。這樣它們之間的通信是針對一套定義好的協議進行的,而不是針對每個系統自己獨有的通信方式(好處js就不會出現兼容不同的系統的if else if代碼)

主動發送消息給對方時,對方必須對該消息予以反饋,即使主動發送消息者對反饋消息不感興趣,(反饋信息可以去掉由于版本兼容等帶來的if else if兼容代碼)

2.2 那我們就開始設計js與java之間的通信方案

2.2.1 互相暴漏給對方一個接口

js為java提供一個唯一的接口,這個接口可以是在java端寫死的,也可以是js傳遞過來的(這樣更靈活)。所有發送給js的消息(請求消息和反饋消息)都通過該接口

java為js提供的一個唯一的接口,因為官方的方法存在漏洞,我們采用在onJsPrompt方法中接收js發送的所有消息,當然大家還可以選擇其他方法來接收js的消息,這不是重點。

2.2.2 js與java之間通信協議的制定

js與java之間的通信特別類似于網絡請求,主動發起消息的行為可以稱為request(請求消息),對該消息的反饋可以稱為response(響應消息)。

request

一個request封裝了請求對方的哪個接口,以及該接口所需要的參數。

response

一個response封裝了狀態信息(可以知道處理的結果是成功還是失敗)和處理結果。

如何接收對方發送的response消息?

大家都應該都會想到,在發送消息的時候傳遞一個回調接口就行了,但是因為js與java之間是跨語言的,尤其是java是不可能把回調接口傳遞給js,js雖然可以傳遞過來但是會有問題,所以這時候有一種解決辦法:

  • 在給對方發送request消息時,為回調接口生成一個唯一的id值,把id值存入request中發出。

  • 同時把回調接口緩存起來。

  • 在接收到response時,從response解析這個id值,根據id值查找到回調接口。

因此request和response中還得包含回調id這個值。

通信協議的格式

request數據格式:

{      
          //接口名稱
          "interfaceName":"test",
          //回調id值
          "callbackId":"c_111111",
          //傳遞的參數
          "params":{
                 ....
          }
     }

response數據格式:

{
            //回調id,同時這也是response的標志
           "responseId":"c_111111",
            //response數據
           "data":{
               //狀態數據
              "status":"1",
              "msg":"ok",
              //response的處理結果數據
              "values":{
                     ......
              }
          }
     }

到此通信協議就已經定義好了。

2.2.3 讓繁瑣的無趣的重復的苦力活兒不再有

大家可以看到通信協議request和response都是json格式,從json中解析數據或者把數據封裝為json都是重復的苦力活兒。

這也是我一直想著力解決的痛點,解決之道是從retrofit中獲得啟發的,應用注解來解決以上問題。

關于js與java完美通信的設計思想到此為止,這也是SimpleJavaJsBridge這個庫的核心思想,那我們就來看下SimpleJavaJsBridge。

3. SimpleJavaJsBridge

SimpleJavaJsBridge我為什么要起一個這樣的名字,首先它解決了上文中提到的讓繁瑣的無趣的重復的苦力活兒不再有的問題,對于不管是從json中解析數據還是把數據封裝成json,使用者都不需要關心,讓使用者很省心;并且它使用起來也非常的簡單,在稍后的例子中大家會體會到,所以用了simple這個詞兒。通過它java可以給js發送消息,并且接收js的響應消息;同時js也可以給java發送消息,同樣接收java的響應消息。因此它是java與js之間通信的橋梁,因此它的名字叫為SimpleJavaJsBridge。

3.1 如何解決繁瑣的無趣的重復的苦力活兒?

解決這個問題思路來自于鼎鼎有名的Retrofit,Retrofit通過注解的方式解決了構建request和解析response的問題,因此注解也可以解決我現在遇到的問題。那我們就來認識下這些注解。

InvokeJSInterface

用來標注java給js發送消息的方法,它的value值代表js提供的功能的接口名字

JavaCallback4JS

用來標注java提供給js的回調方法的

JavaInterface4JS

用來標注java提供給js的接口,它的value值代表功能的接口名字

Param

用來標注參數或者類的實例屬性,它的value值代表參數被存入json中的key值,它的needConvert代表當前的參數是否需要進行轉換,因為通過JsonObject類往json中存放的數據是有要求的,JsonObject中只能存放基本數據和JsonObject和JsonArray這些數據類型,對于其他的類型就得進行轉換了。因此只要是不能直接通過JsonObject存放的類型該值必須為true

ParamCallback

用來標注回調類型的參數,比如發送request給js的方法中,需要有一個回調參數,那這個參數必須用它來標注

ParamResponseStatus

用來標注響應狀態類型的參數,比如:statusCode,StatusMsg這些參數,它的value值是json中的key值。

3.2 SimpleJavaJsBridge使用

3.2.1 構建一個SimpleJavaJsBridge實例

SimpleJavaJsBridge instance = new SimpleJavaJsBridge.Builder()
          .addJavaInterface4JS(javaInterfaces4JS)                                       
          .setWebView(webView)                                   
           .setJSMethodName4Java("_JSBridge._handleMessageFromNative")                                   
          .setProtocol("niu","receive_msg").create();

通過SimpleJavaJsBridge.Builder來構建一個SimpleJavaJsBridge對象,

  • addJavaInterface4JS用來添加java提供給js的接口
  • setWebView 設置WebView這是必須進行設置的
  • setJSMethodName4Java 設置js為java唯一暴漏的方法名字
  • setProtocol設置協議字段,這也是必須的,這個字段主要是為了ios而設置的

當然還可以調用其他的一些方法對SimpleJavaJsBridge進行設置

3.2.2 java給js提供接口

java給js提供一個無參的接口

/** * 給js發送響應消息的接口*/
      public interface   IResponseStatusCallback { 
           void callbackResponse(@ParamResponseStatus("status") int status, @ParamResponseStatus("msg") String msg);
      }

      //java提供給js的"tes4"接口,@ParamCallback標注的是給js發送消息的回調
      @JavaInterface4JS("test4")
      public void test3(@ParamCallback IResponseStatusCallback jsCallback) {  
          進行相應處理...;
          //給js發送響應消息
           jsCallback.callbackResponse(1, "ok");
      }

      //下面是js代碼,js給java的"test4"接口發送消息
      _JSNativeBridge._doSendRequest("test4", {}, function(responseData){

      });
/** * 給js發送響應消息的接口*/
      public interface   IResponseStatusCallback { 
           void callbackResponse(@ParamResponseStatus("status") int status, @ParamResponseStatus("msg") String msg);
      }

      /** * 必須有無參構造函數 ,只有被@Param注解的屬性才會存入json中*/
      public static class Person {
           @Param("name") 
           String name;

           @Param("age") 
           public int age;  

           public Person() {    } 

           public Person(String name, int age) {  
              this.name = name;  
              this.age = age; 
           }
      }

      //java提供給js的“test1”接口,Person是無法直接往JsonObject中存放的,
      //所以needConvert必須為true,會自動把Person中用注解標注的屬性放入json中
      @JavaInterface4JS("test1")
      public void test(@Param(needConvert = true) Person personInfo, @ParamCallback IResponseStatusCallback jsCallback) {
             對收到的數據進行處理....;
             jsCallback.callback(1, "ok");
      }

      //下面是js代碼,js給java的"test1"接口發送消息
      _JSNativeBridge._doSendRequest("test1", {"name":"niu","age":10}, function(responseData){

      });

3.2.3 給js發送消息

//給js發送消息的方法要定義在一個interface中,因為這個過程是模仿Retrofit的
    public interface IInvokeJS {

          //復雜類型,只有用@Param標注的屬性才會放入json中
          public static class City{
                   @Param("cityName") 
                   public String cityName; 

                   @Param("cityProvince") 
                   public String cityProvince;

                   public int cityId;
          }

          //給js的“exam”接口發送數據,參數是需要傳遞的數據
          @InvokeJSInterface("exam")
          void exam(@Param("test") String testContent, @Param("id") int id,@ParamCallback IJavaCallback2JS iJavaCallback2JS);

          //給js的“exam1”接口發送數據,參數同樣也是需要傳遞的數據
          @InvokeJSInterface("exam1")
          void exam1(@Param(needConvert = true) City city, @ParamCallback IJavaCallback2JS iJavaCallback2JS);
    }


   //使用,使用方式和Retrofit一樣,先使用SimpleJavaJsBridge的
  //createInvokJSCommand實例方法生成一個IInvokeJS實例
  IInvokeJS invokeJs = simpleJavaJsBridge.createInvokJSCommand(IInvokeJS.class);

  //給js的"exam"發送消息,發送的是基本數據類型
  invokeJs.exam("hello js",20, new IJavaCallback2JS{
            //接收js發送的響應數據的回調方法,該方法的名字可以任意,但必須用@JavaCallback4JS標注
            @JavaCallback4JS
            public void callback(@ParamResponseStatus("msg")String statusMsg,@Param("msg") String msg) {

            }
  });

 City city = new City();
 city.cityName = "長治";
 city.cityId = 11;
 city.cityProvince = "山西";
 //給js的“exam1”發送消息,city是一個復雜對象
 invokeJs.exam1(city, new IJavaCallback2JS{
            @JavaCallback4JS
            public void callback(@ParamResponseStatus("msg")String statusMsg,@Param("msg") String msg) {

            }
  });

總結

SimpleJavaJsBridge庫在js與java的通信中帶來以下優點:

  • js代碼中不再有由于系統或者app版本甚至業務原因產生的if else if的兼容語句

  • java不需要再關心數據封裝為json或者從json中解析數據的繁瑣工作

  • 讓js與java之間的通信更簡單

 

來自:http://www.html5cn.org/article-9801-1.html

 

 本文由用戶 eefb 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!