前言
最近接觸android中js與java交互的東西很多,當然它們之間的交互方式有幾種,但是我覺得這幾種交互方式都存在一定的不足应役,這是我決定編寫SimpleJavaJsBridge這個庫的關(guān)鍵原因赴魁。
我會按以下順序進行本文章:
- 現(xiàn)有js與java通信方案及不足
- js與java完美通信方案設計
- SimpleJavaJsBridge
現(xiàn)在進入正題
1. 現(xiàn)有js與java通信方案及不足
先來說明一點js與java通信,指的是js既可以給java發(fā)送消息叙赚,同時java也可以給js發(fā)送消息。那就來屢屢它們之間的通信方案。
1.1 java給js發(fā)送消息
官方唯一指定方法是通過webview的loadUrl(String)方法進行的饥脑,看下的偽代碼:
//例子:調(diào)用js的test(param)方法
webView.loadUrl("javascript:test(1)");
調(diào)用方法非常的簡單恳邀,"javascript:"+js方法的名字+方法的參數(shù)值拼接成一個字符串就可以給js發(fā)送消息了,猶如是在直接調(diào)用js的方法灶轰。
1.2 js給java發(fā)送消息
js給java發(fā)送消息實際上只有2種方案谣沸,依次來分析下這2種方案。
1.2.1 官方方法
先看下偽代碼:
//該類封裝了提供給js調(diào)用的方法
public class JSBridge{
//提供給js的方法
public void invokeByJs(String msg){
}
}
//把JSBridge對象注入WebView中笋颤,同時起一個別名
webView.addJavascriptInterface(new JSBridge(),"jsBridge");
//js調(diào)用java方法
window.jsBridge.invokeByJs('hello java');
這種方法其實是把一個對象注入到WebView中乳附,js給java發(fā)送消息的方式是
window.注入WebView的java對象的所對應name值.javaMethod(param...);
這其實也猶如在java代碼中調(diào)用java的方法,因為java提供給js的方法名伴澄,方法參數(shù)是啥赋除,js在發(fā)送消息時,方法名與參數(shù)必須保持一致非凌,這也是這些java代碼不能進行混淆的原因举农。
但是這種方法存在一個嚴重的漏洞,雖然官方在android4.4的時候給出了相應的解決方案敞嗡,但是android4.4以下的版本還得解決該漏洞颁糟,因此一些巨人們就開始琢磨著解決這個坑,第二種方法由此誕生喉悴。
1.2.2 js傳遞約定好的字符串給java
這種方案的主要原理是:
- 找到一個js可以給java發(fā)送消息的入口(這個入口有onJsPrompt,onJsAlert等等)
- js通過入口把消息按既定好的規(guī)則拼接成字符串傳遞給java
- java按照既定好的規(guī)則對字符串進行解析
- 根據(jù)解析數(shù)據(jù)棱貌,通過反射來調(diào)用自己相應方法
這種方法使用起來要比官方方法(第一種方法)麻煩。
1.3 存在的不足
上面介紹了js與java的通信方法箕肃,那我就來分析下我認為存在的不足婚脱。
1.3.1 java給js發(fā)送消息方法和js給java發(fā)送消息的官方方法存在的不足
1.3.1.1 強依賴
java給js發(fā)送消息的方法,和js給java發(fā)送消息的官方方法都存在著強依賴的問題勺像,都要高度依賴對方的方法名字障贸,方法參數(shù)。強依賴發(fā)生于同一模塊內(nèi)吟宦,個人覺得不是問題甚至是高內(nèi)聚的體現(xiàn)惹想。但是java與js可以說是處于兩個不同的模塊或者叫兩個不同世界,只要js提供給java的方法發(fā)生變化督函,java也得改動嘀粱,同理java提供給js的方法也如此。處于兩個不同模塊知道對方的細節(jié)越少越好辰狡,這樣耦合性就會降低锋叨,耦合性降低的好處就不用說了。
1.3.1.2 強依賴導致js需要兼容不同的系統(tǒng)
先看段偽代碼:
function location(){
//是ios系統(tǒng)宛篇,采用給ios發(fā)送消息的方法
if(isIOS){
給ios發(fā)送消息娃磺;
}else if(isAndroid){
給android發(fā)送消息;
}
}
上面的代碼展示的是js使用native的定位功能的代碼叫倍,因為js在給不同的系統(tǒng)發(fā)送消息的方式不一樣偷卧,就會出現(xiàn)if else if 這樣的兼容語句豺瘤。當前js代碼只被ios和android使用,假如還會被wp或pc來使用听诸,那if else if豈不是要惡心死坐求。產(chǎn)生該問題的主要原因是:js代碼在針對不同的系統(tǒng)自己獨有的通信方式進行通信。
1.3.1.3 給不存在的接口發(fā)送消息沒反饋
java在給js的一個不存在接口發(fā)送消息時晌梨,java根本不知道該接口不存在桥嗤,java只會傻傻的等待。同理js在給java的一個不存在接口發(fā)送消息時仔蝌,js是可以通過捕獲異常來知道該接口不存在泛领,但是這不是最好的解決方案。
給不存在接口發(fā)送消息沒反饋會導致js代碼充斥著if else if語句敛惊,看段偽代碼:
//調(diào)用java的定位方法
function location(){
//1.1版本以上才會調(diào)用定位功能
if(androidAppVersion > '1.1'){
發(fā)送消息給java渊鞋;
}else{
給用戶提示,暫不支持定位功能瞧挤;
}
}
這是一段調(diào)用java進行定位的js代碼篓像,android app在版本1.1的時候才增加了定位的功能,因此對于1.1以下版本是不支持這功能的皿伺,因此js代碼里面非常有必要根據(jù)版本號進行判斷。這只是由于版本問題導致if else if的一個小小的縮影盒粮。還有一些其他情況導致if else if的產(chǎn)生比如一份js代碼被多個業(yè)務使用鸵鸥。
1.3.2 js給java發(fā)送消息的第二種方法存在不足
上文提到的js給java發(fā)送消息的第二種方法,它解決了存在的漏洞丹皱,但是這種方法妒穴,使用起來要比第一種方法復雜,java會多做以下工作:
- 解析js傳遞給java的字符串摊崭,把調(diào)用的接口讼油,參數(shù)解析出來
- 把調(diào)用的接口,參數(shù)映射到相應的方法
不論js傳遞給java的字符串是json格式還是其他格式呢簸,解析這樣的字符串肯定是一件無趣的重復的體力勞動矮台。
若想解決以上的問題,我們有必要設計一套完美的通信方案根时。
2. js與java完美通信方案設計
2.1 一套完美的js與java的通信方案應滿足以下幾點:
js與java知道對方的細節(jié)越少越好瘦赫,越少它們的耦合性越低。那到底多少為好呢蛤迎?我個人覺得互相暴漏給對方一個接口足矣确虱。這樣js與native的通信就類似于通過一個管道通信或者說類似于socket通信(降低強依賴)
js與java之間通信,需要定義好一套通信協(xié)議或者叫通信規(guī)則替裆,在管道之間傳遞通信協(xié)議校辩。這樣它們之間的通信是針對一套定義好的協(xié)議進行的窘问,而不是針對每個系統(tǒng)自己獨有的通信方式(好處js就不會出現(xiàn)兼容不同的系統(tǒng)的if else if代碼)
主動發(fā)送消息給對方時,對方必須對該消息予以反饋宜咒,即使主動發(fā)送消息者對反饋消息不感興趣惠赫,(反饋信息可以去掉由于版本兼容等帶來的if else if兼容代碼)
2.2 那我們就開始設計js與java之間的通信方案
2.2.1 互相暴漏給對方一個接口
- js為java提供一個唯一的接口,這個接口可以是在java端寫死的荧呐,也可以是js傳遞過來的(這樣更靈活)汉形。所有發(fā)送給js的消息(請求消息和反饋消息)都通過該接口
- java為js提供的一個唯一的接口,因為官方的方法存在漏洞倍阐,我們采用在onJsPrompt方法中接收js發(fā)送的所有消息概疆,當然大家還可以選擇其他方法來接收js的消息,這不是重點峰搪。
2.2.2 js與java之間通信協(xié)議的制定
js與java之間的通信特別類似于網(wǎng)絡請求岔冀,主動發(fā)起消息的行為可以稱為request(請求消息),對該消息的反饋可以稱為response(響應消息)概耻。
request
一個request封裝了請求對方的哪個接口使套,以及該接口所需要的參數(shù)。
response
一個response封裝了狀態(tài)信息(可以知道處理的結(jié)果是成功還是失斁媳)和處理結(jié)果侦高。
如何接收對方發(fā)送的response消息?
大家都應該都會想到,在發(fā)送消息的時候傳遞一個回調(diào)接口就行了厌杜,但是因為js與java之間是跨語言的奉呛,尤其是java是不可能把回調(diào)接口傳遞給js,js雖然可以傳遞過來但是會有問題夯尽,所以這時候有一種解決辦法:
- 在給對方發(fā)送request消息時瞧壮,為回調(diào)接口生成一個唯一的id值,把id值存入request中發(fā)出匙握。
- 同時把回調(diào)接口緩存起來咆槽。
- 在接收到response時,從response解析這個id值圈纺,根據(jù)id值查找到回調(diào)接口秦忿。
因此request和response中還得包含回調(diào)id這個值。
通信協(xié)議的格式
request數(shù)據(jù)格式:
{
//接口名稱
"interfaceName":"test",
//回調(diào)id值
"callbackId":"c_111111",
//傳遞的參數(shù)
"params":{
....
}
}
response數(shù)據(jù)格式:
{
//回調(diào)id蛾娶,同時這也是response的標志
"responseId":"c_111111",
//response數(shù)據(jù)
"data":{
//狀態(tài)數(shù)據(jù)
"status":"1",
"msg":"ok",
//response的處理結(jié)果數(shù)據(jù)
"values":{
......
}
}
}
到此通信協(xié)議就已經(jīng)定義好了小渊。
2.2.3 讓繁瑣的無趣的重復的苦力活兒不再有
大家可以看到通信協(xié)議request和response都是json格式,從json中解析數(shù)據(jù)或者把數(shù)據(jù)封裝為json都是重復的苦力活兒茫叭。
這也是我一直想著力解決的痛點酬屉,解決之道是從retrofit中獲得啟發(fā)的,應用注解來解決以上問題。
關(guān)于js與java完美通信的設計思想到此為止呐萨,這也是SimpleJavaJsBridge這個庫的核心思想杀饵,那我們就來看下SimpleJavaJsBridge。
3. SimpleJavaJsBridge
SimpleJavaJsBridge我為什么要起一個這樣的名字谬擦,首先它解決了上文中提到的讓繁瑣的無趣的重復的苦力活兒不再有的問題切距,對于不管是從json中解析數(shù)據(jù)還是把數(shù)據(jù)封裝成json,使用者都不需要關(guān)心惨远,讓使用者很省心谜悟;并且它使用起來也非常的簡單,在稍后的例子中大家會體會到北秽,所以用了simple這個詞兒葡幸。通過它java可以給js發(fā)送消息,并且接收js的響應消息贺氓;同時js也可以給java發(fā)送消息蔚叨,同樣接收java的響應消息。因此它是java與js之間通信的橋梁辙培,因此它的名字叫為SimpleJavaJsBridge蔑水。
3.1 如何解決繁瑣的無趣的重復的苦力活兒?
解決這個問題思路來自于鼎鼎有名的Retrofit扬蕊,Retrofit通過注解的方式解決了構(gòu)建request和解析response的問題搀别,因此注解也可以解決我現(xiàn)在遇到的問題。那我們就來認識下這些注解尾抑。
InvokeJSInterface
用來標注java給js發(fā)送消息的方法歇父,它的value值代表js提供的功能的接口名字
JavaCallback4JS
用來標注java提供給js的回調(diào)方法的
JavaInterface4JS
用來標注java提供給js的接口,它的value值代表功能的接口名字
Param
用來標注參數(shù)或者類的實例屬性蛮穿,它的value值代表參數(shù)被存入json中的key值,它的needConvert代表當前的參數(shù)是否需要進行轉(zhuǎn)換毁渗,因為通過JsonObject類往json中存放的數(shù)據(jù)是有要求的践磅,JsonObject中只能存放基本數(shù)據(jù)和JsonObject和JsonArray這些數(shù)據(jù)類型,對于其他的類型就得進行轉(zhuǎn)換了灸异。因此只要是不能直接通過JsonObject存放的類型該值必須為true
ParamCallback
用來標注回調(diào)類型的參數(shù)府适,比如發(fā)送request給js的方法中,需要有一個回調(diào)參數(shù)肺樟,那這個參數(shù)必須用它來標注
ParamResponseStatus
用來標注響應狀態(tài)類型的參數(shù)檐春,比如:statusCode,StatusMsg這些參數(shù)么伯,它的value值是json中的key值疟暖。
3.2 SimpleJavaJsBridge使用
3.2.1 構(gòu)建一個SimpleJavaJsBridge實例
SimpleJavaJsBridge instance = new SimpleJavaJsBridge.Builder()
.addJavaInterface4JS(javaInterfaces4JS)
.setWebView(webView)
.setJSMethodName4Java("_JSBridge._handleMessageFromNative")
.setProtocol("niu","receive_msg").create();
通過SimpleJavaJsBridge.Builder來構(gòu)建一個SimpleJavaJsBridge對象,
- addJavaInterface4JS用來添加java提供給js的接口
- setWebView 設置WebView這是必須進行設置的
- setJSMethodName4Java 設置js為java唯一暴漏的方法名字
- setProtocol設置協(xié)議字段,這也是必須的俐巴,這個字段主要是為了ios而設置的
當然還可以調(diào)用其他的一些方法對SimpleJavaJsBridge進行設置
3.2.2 java給js提供接口
java給js提供一個無參的接口
/** * 給js發(fā)送響應消息的接口*/
public interface IResponseStatusCallback {
void callbackResponse(@ParamResponseStatus("status") int status, @ParamResponseStatus("msg") String msg);
}
//java提供給js的"tes4"接口骨望,@ParamCallback標注的是給js發(fā)送消息的回調(diào)
@JavaInterface4JS("test4")
public void test3(@ParamCallback IResponseStatusCallback jsCallback) {
進行相應處理...;
//給js發(fā)送響應消息
jsCallback.callbackResponse(1, "ok");
}
//下面是js代碼,js給java的"test4"接口發(fā)送消息
_JSNativeBridge._doSendRequest("test4", {}, function(responseData){
});
java給js提供帶有參數(shù)的接口
/** * 給js發(fā)送響應消息的接口*/
public interface IResponseStatusCallback {
void callbackResponse(@ParamResponseStatus("status") int status, @ParamResponseStatus("msg") String msg);
}
/** * 必須有無參構(gòu)造函數(shù) ,只有被@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) {
對收到的數(shù)據(jù)進行處理....;
jsCallback.callback(1, "ok");
}
//下面是js代碼,js給java的"test1"接口發(fā)送消息
_JSNativeBridge._doSendRequest("test1", {"name":"niu","age":10}, function(responseData){
});
3.2.3 給js發(fā)送消息
//給js發(fā)送消息的方法要定義在一個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”接口發(fā)送數(shù)據(jù),參數(shù)是需要傳遞的數(shù)據(jù)
@InvokeJSInterface("exam")
void exam(@Param("test") String testContent, @Param("id") int id,@ParamCallback IJavaCallback2JS iJavaCallback2JS);
//給js的“exam1”接口發(fā)送數(shù)據(jù)糟把,參數(shù)同樣也是需要傳遞的數(shù)據(jù)
@InvokeJSInterface("exam1")
void exam1(@Param(needConvert = true) City city, @ParamCallback IJavaCallback2JS iJavaCallback2JS);
}
//使用绢涡,使用方式和Retrofit一樣,先使用SimpleJavaJsBridge的
//createInvokJSCommand實例方法生成一個IInvokeJS實例
IInvokeJS invokeJs = simpleJavaJsBridge.createInvokJSCommand(IInvokeJS.class);
//給js的"exam"發(fā)送消息糊饱,發(fā)送的是基本數(shù)據(jù)類型
invokeJs.exam("hello js",20, new IJavaCallback2JS{
//接收js發(fā)送的響應數(shù)據(jù)的回調(diào)方法垂寥,該方法的名字可以任意,但必須用@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”發(fā)送消息另锋,city是一個復雜對象
invokeJs.exam1(city, new IJavaCallback2JS{
@JavaCallback4JS
public void callback(@ParamResponseStatus("msg")String statusMsg,@Param("msg") String msg) {
}
});
總結(jié)
SimpleJavaJsBridge庫在js與java的通信中帶來以下優(yōu)點:
- js代碼中不再有由于系統(tǒng)或者app版本甚至業(yè)務原因產(chǎn)生的if else if的兼容語句
- java不需要再關(guān)心數(shù)據(jù)封裝為json或者從json中解析數(shù)據(jù)的繁瑣工作
- 讓js與java之間的通信更簡單
若你動心了可以下載來試用下:SimpleJavaJsBridge