前言
这里说的交互,指的是采用官方提供的方法,其它实现方式(如拦截url,拦截prompt)不在本文描述范围之内。
JS调用原生
通过给方法添加@JavascriptInterface
注解,然后通过mWebView.addJavascriptInterface(object, name)
将刚才那个方法所在的类注入JS,然后js就可以直接通过name.方法名()
来调用刚才那个方法。
示例
获取包名:
mWebView.addJavascriptInterface(new Object()
{
@JavascriptInterface
public String getPackageName()
{
return mWebView.getContext().getPackageName();
}
}, "test");
然后在js里面执行test.getPackageName()就可以获取当前apk的包名了:
上面偷懒直接采用匿名内部类的方式,正常情况下一般不会这么写。
重载与参数类型转换
众所周知,js
不支持重载,Java
有,那么注入到js
的java
方法是否支持重载呢?
下面以获取versionCode
和versionName
为例来验证注入JS时的重载与类型转换问题。
测试代码
//TestJavaScript.java
public class TestJavaScript
{
private Activity mActivity;
public TestJavaScript(Activity mActivity)
{
this.mActivity = mActivity;
}
/**
* 获取版本号
* @return
*/
@JavascriptInterface
public String getVersion() throws NameNotFoundException
{
Log.i("info", "进入第1个getVersion方法");
return getVersion(null);
}
/**
* 获取版本号
* @return
*/
@JavascriptInterface
public String getVersion(String type) throws NameNotFoundException
{
Log.i("info", "进入第2个getVersion方法");
Log.i("info", "type:"+type);
if(type == null || "".equals(type)) type = "name";
PackageInfo info = mActivity.getPackageManager().getPackageInfo(mActivity.getPackageName(), 0);
if("name".equals(type)) return info.versionName;
else if("code".equals(type)) return info.versionCode+"";
else return "type error";
}
}
//WelcomeActivity.java
mWebView.addJavascriptInterface(new TestJavaScript(this), "test");
说明:type为name
时返回versionName
,type为code
时返回versionCode
,当然这里这样写仅仅是为了测试,实际环境中这样写的人肯定是找骂,哈哈。
开始测试
以上例子测试代码:
test.getVersion(); // 返回1.0 进入第1个getVersion方法
test.getVersion(null); // 返回1.0 进入第2个getVersion方法,获取到的type为null
test.getVersion(undefined); // 返回 type error,进入第2个方法,获取到的type为字符串形式的"undefined"
test.getVersion('name'); // 返回1.0,
test.getVersion('code'); // 返回1
test.getVersion('code', 1); //提示 Error calling method on NPObject 错误
test.getVersion(222); // 提示 type error,进入第二个方法,获取到的是字符串的"222"
假如只有下面这一个方法:
@JavascriptInterface
public String getVersion(int a)
{
Log.i("info", "进入int型getVersion方法,参数:" + a);
return "进入int型getVersion方法";
}
测试时:
test.getVersion(123); // 接收到参数为123
test.getVersion('abc'); // 依然可以进入这个int型的方法,但是接收到的参数为0
假如只有下面这2个方法:
@JavascriptInterface
public String getVersion(String a)
{
Log.i("info", "进入String型getVersion方法,参数:" + a);
return "进入String型getVersion方法";
}
@JavascriptInterface
public String getVersion(int a)
{
Log.i("info", "进入int型getVersion方法,参数:" + a);
return "进入int型getVersion方法";
}
测试时:
test.getVersion(123); // 进入int型方法,接收到参数为123
test.getVersion('abc'); // 进入string型方法,接收到参数为'abc'
再假设有如下代码:
@JavascriptInterface
public void testParam(String param1, String param2)
{
System.out.println(param1 == null ? "@NULL" : param1);
System.out.println(param2 == null ? "@NULL" : param2);
}
js测试代码:
test.testParam('abc'); // Error: Error calling method on NPObject.
test.testParam('abc', undefined); // js没报错,后台输出 abc undefined(注意这个undefined是加了双引号的)
test.testParam('abc', null); // js没报错,后台输出 abc @NULL
test.testParam('abc', 'cbd'); // js 没报错,后台输出 abc cbd
还有一些小测试,懒得逐一贴代码了,直接上结论。
重要结论
Java注入js时是区分重载的,但是由于存在类型转换,所以并不会完全像Java那样严格区分,换句话说,对于参数个数的不同会严格区分(前后端参数个数不同甚至会报错),但是对于参数类型的不同则没那么严格,如果能够找到正确类型的方法,那么会优先进入这个方法,如果找不到,则进入参数个数相同、类型不同的其它方法,会自动进行类型转换。
参数类型转换规则
假如只有一个参数类型是String的方法:
null
自动转 java 的null
;undefined
自动转 java 的 字符串"undefined"
;123
自动转"123"
;true
自动转"true"
;'abc'
转换正常的"abc"
;
假如只有一个参数类型是int的方法:
null
自动转0
;undefined
自动转0
;'abc'
自动转0
;true/false
全部自动转0
;123
转换正常的123
;
其它类型,比如时间、对象等就没测试了,因为实际使用中,string和int这2个足够用了。
为避免一些不必要的麻烦,建议注入方法时少用重载,尽量避免不同类型参数自动转换,毕竟多一事不如少一事,多取几个名字就是了。
Error calling method on NPObject
错误如下:
Error: Error calling method on NPObject.
如果是在调用原生方法出现这个错误,一般有3个原因:
- 由于参数个数不正确导致的,上面的例子也提到了,假如方法只有1个参数,但是你传了2个参数,或者定义了2个参数,但是你只传了1个参数,那么就会报这个错误;
- 代码必须运行在UI线程中(如调用
mWebView.goBack()
),即mActivity.runOnUiThread
; - 原生方法内部出错,比如常见的空指针异常等,都会引起这个错误;
切勿使用包装类型
假设有如下方法:
@JavascriptInterface
public String testInt(Integer a)
{
return "int:"+a;
}
@JavascriptInterface
public String testBoolean(Boolean a)
{
return "boolean:"+a;
}
调用:
test.testInt(123); // 输出"int:null"
test.testBoolean(true); // 输出"boolean:null"
test.testBoolean(false); // 输出"boolean:null"
可以发现,如果Android这边参数使用了包装类型会导致参数接收不到,必须使用基本类型,把上面的Integer和Boolean换成int和boolean就没问题了。
注入有效期
只需要对webview全局注入一次,无需针对每个页面重新注入,一次注入“永久”生效。
有人会问,为啥cordova/phonegap、mui等都必须在某一事件触发之后才能调用原生提供的方法呢?也就是为啥它们是针对具体页面注入的?这个是因为它们实现机制和我们这里不一样,而且还有很多其它方面考虑,后续有机会再着重讨论这个问题。
JavascriptInterface注解漏洞
Android4.2开始增加@JavascriptInterface
注解,目的是为了解决一个 漏洞 ,在4.2版本之后,只有添加了这个注解的方法才会被注入JS。
iframe问题
对iframe的支持不同设备不一样,有的会注入到iframe里面,有的不会。
//TODO 本小节有待完善。
远程调试提示问题
即使对象注入成功了,在控制台输入的时候,test会有提示,但是test.
再往后面就没有提示了,这是正常现象,不要以为没有注入成功。
原生调用JS
一般都是通过mWebView.loadUrl('javascript:xxx')
来执行一段JS,据说这个方法有一个bug,就是执行的时候如果输入法是弹出的,执行后输入法会自动消失,我暂未亲测。
这个方法最大的缺点是无法优雅的解决异步回调问题,鉴于此,Android4.4开始增加了如下方法:
mWebView.evaluateJavascript(script, resultCallback)
有了这个方法就可以非常方便的实现js的异步回调,但是毕竟低于Android4.4的版本还是有比较大的份额,所以一般还是得自己另行解决异步回调的问题。
//TODO 这里还有待完善