Android原生与JS交互总结
本文由 小茗同学 发表于 2016-07-05 浏览(11518)
最后修改 2016-09-06 标签:android javascript 原生 交互 总结
[TOC]

前言

这里说的交互,指的是采用官方提供的方法,其它实现方式(如拦截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有,那么注入到jsjava方法是否支持重载呢?

下面以获取versionCodeversionName为例来验证注入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. 由于参数个数不正确导致的,上面的例子也提到了,假如方法只有1个参数,但是你传了2个参数,或者定义了2个参数,但是你只传了1个参数,那么就会报这个错误;
  2. 代码必须运行在UI线程中(如调用mWebView.goBack()),即mActivity.runOnUiThread
  3. 原生方法内部出错,比如常见的空指针异常等,都会引起这个错误;

切勿使用包装类型

假设有如下方法:

@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 这里还有待完善