本文介绍SimpleJavaJsBridge库,旨在解决Android中JS与Java双向通信的痛点。通过分析现有方案在解耦、兼容性和反馈机制上的不足,提出基于注解和统一协议的通信设计。该库简化了JSON解析与封装过程,消除了版本兼容代码,实现低耦合的高效交互。
前言
前言
近期在Android开发中频繁涉及JavaScript与Java的交互场景。尽管现有的交互方式有多种,但各自均存在局限性,这正是我决定开发SimpleJavaJsBridge库的核心动机。
本文将按照以下逻辑展开:
- 现有js与java通信方案及不足
- js与java完美通信方案设计
- SimpleJavaJsBridge
现在进入正题
现有js与java通信方案及不足
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发送消息实际上仅有两种方案,下面将逐一分析。
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');
该方法的核心是将一个Java对象注入到WebView中。JS向Java发送消息时,实际上是通过调用注入对象的方法来触发。
window.注入WebView的java对象的所对应name值.javaMethod(param...);
这类似于在Java代码中直接调用Java方法。由于JS调用Java时,方法名和参数必须与Java端定义的一致,这导致Java代码无法进行混淆处理。
然而,这种方法存在一个严重漏洞。虽然Android 4.4提供了相应的解决方案,但在Android 4.4以下的版本中仍需解决此漏洞。因此,各大厂商开始探索解决这一问题的方法,第二种方案由此诞生。
1.2.2 js传递约定好的字符串给java
这种方案的主要原理如下:
- 找到一个JS可以向Java发送消息的入口(如onJsPrompt、onJsAlert等)。
- JS通过该入口,将消息按照既定规则拼接成字符串传递给Java。
- 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的方法发生变化时,JS端也需调整。处于不同模块的双方,了解对方细节越少越好,这样能降低耦合度,其好处不言而喻。
1.3.1.2 强依赖导致js需要兼容不同的系统
先看段伪代码:
function location(){
//是ios系统,采用给ios发送消息的方法
if(isIOS){
给ios发送消息;
}else if(isAndroid){
给android发送消息;
}
}
上述代码展示了JS调用原生定位功能的实现。由于JS向不同系统发送消息的方式各异,导致代码中出现大量的if-else-if兼容语句。当前JS代码仅用于iOS和Android,若未来还需支持WP或PC,这些兼容语句将变得极其繁琐。产生该问题的主要原因是:JS代码针对不同的系统采用了各自独有的通信方式。
1.3.1.3 给不存在的接口发送消息没反馈
当Java向JS发送一个不存在的接口消息时,Java无法得知该接口不存在,只会被动等待。同理,当JS向Java发送一个不存在的接口消息时,JS虽然可以通过捕获异常来知晓接口不存在,但这并非最佳解决方案。
给不存在的接口发送消息且无反馈,会导致JS代码中充斥大量的if-else-if语句。参考以下伪代码:
//调用java的定位方法
function location(){
//1.1版本以上才会调用定位功能
if(androidAppVersion > '1.1'){
发送消息给java;
}else{
给用户提示,暂不支持定位功能;
}
}
这是一段调用Java进行定位的JS代码。Android应用在1.1版本才增加了定位功能,因此1.1以下版本不支持该功能,JS代码中必须根据版本号进行判断。这仅是因版本问题导致if-else-if的一个缩影。其他情况,如一份JS代码被多个业务使用,也会产生类似的兼容代码。
1.3.2 js给java发送消息的第二种方法存在不足
上文提到的JS向Java发送消息的第二种方案,虽然解决了前述漏洞,但使用复杂度高于第一种方案。Java端需额外执行以下操作:
- 解析JS传递给Java的字符串,提取调用的接口名及参数。
- 将提取的接口名和参数映射到相应的方法。
无论JS传递给Java的字符串是JSON格式还是其他格式,解析此类字符串无疑是一项无趣且重复的体力劳动。
若要解决以上问题,有必要设计一套完美的通信方案。
js与java完美通信方案设计
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。
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){
});
Java给JS提供带有参数的接口:
/** * 给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之间的通信更简单。
源码下载地址:https://github.com/niuxiaowei/SimpleJavaJsBridge
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!