写点什么

Android WebView 与 Native 通信总结,Android 中高级面试必知必会

发布于: 2021 年 11 月 08 日

}


  • WebView中注入这个类的实例


HybridAPI hybridAPI = new HybridAPI();webview.addJavascriptInterface(hybridAPI, "HybridAPI")


在网页中直接用如下代码便可以将数据发送到 native 端


HybridAPI.sendToNative('Hello');

iframe

我们还可以利用iframe进行请求伪造向 native 端发送数据的。思路是向网页中添加一个iframe控件,通过修改其src属性,触发 native 端的shouldOverrideUrlLoading方法的执行, 同样,native 端通过重写该方法,去拿到 js 端传过来的数据。具体操作方式如下:


var iframe = document.createElement('iframe');iframe.style.display = 'none';document.documentElement.appendChild(iframe);iframe.src="native://getUserInfo?id=1";


在操作完成后,我们再从当前的 dom 结构中移除这个组件。


setTimeout(function() {iframe && iframe.parentNode && iframe.parentNode.removeChild(iframe);}, 100);

具体实践

在前面总结了 WebView 和 Native 交互的几种方案。但距离实际项目使用还有一段距离,在实际项目开发中还有很多问题需要考虑。如:


  • 交互的规则如何定义

  • 数据如何传递

  • 调用之后,如何拿到回调的结果

  • 对于 Javascript 的请求,native 端应该如何设计?

  • ....


native 端向 JavaScript 发送消息只有loadUrl, evaluateJavascript这两种方式。Javascript 向 native 端发送信息可以利用onJsPrompt, @JavascriptInterface, shouldOverrideUrlLoading等几种方案,以下 我们通过采用@JavascriptInterface这种方式(也就是大家通常说的注解方案)为例来看看如何解决实际项目开发中碰到的问题。

交互的规则

首先我们来定义两端的交互规则。

Javascript 向 native 发数据:

我们约定在 H5 中采用HybridAPI.sendToNative方法向 native 端发送数据,于是我们需要在 native 端做如下支持:


  • 定义一个HybridAPI类,并向 WebView 中注册


HybridAPI hybridAPI = new HybridAPI(this);webview.addJavascriptInterface(hybridAPI, "HybridAPI");


  • HybridAPI类中定义一个方法sendToNative, 该方法暴露给 Javascript 用来给 native 发送数据


@JavascriptInterfacepublic void sendToNative(final String message) {Log.i(TAG, "get data from js------------>" + message);


}

native 层向 Javascript 发数据:

public final String TO_JAVASCRIPT_PREFIX = "javascript:HybridAPI.onReceiveData('%s')";


public void sendToJavaScript(Map<String, Object> message) {String str = new Gson().toJson(message);final String jsCommand = String.format(TO_JAVASCRIPT_PREFIX, escapeString(str));


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {evaluateJavascript(jsCommand, null);} else {loadUrl(jsCommand);}}


在 H5 中,我们这样写, 当 native 向 Javascript 发送数据时,便会触发 Javascript 中的Hybrid.onReceiveData方法, 该方法就能接收到 native 层传过来的数据


HybridAPI.onReceiveData = function(message) {console.log('[response from native]' + message);}

数据结构的定义

在上面我们已经基于@JavascriptInterface方案完成了 native 与 WebView 间通信机制的实现,双方可以交换数据,但开发的时候需要考虑更多问题。比如,如果是 Javascript 向 native 发送数据,需要将数据转换成一个字符串,然后再将字符串发给 native, native 再去解析这个字符串,找到对应的处理方法,提取出相关的业务参数,再进行相应的处理。所以我们需要定义这个字符串的数据结构。


在上面我们已经约定了,H5 端可以采用HybridAPI.sendToNative向 native 发送数据,该方法只有一个字符串参数, 以获取用户信息这个业务功能为例,我们的字符串参数是native://getUserInfo?id=1,这个字符串中的getUserInfo表示当前通信的目的或行为(为了拿用户信息), ? 后面的id=1 表示的是参数(用户 id 为 1), 如果参数多了,这个字符串会更长,再如果上面涉及到中文的转码,其可读性会大大降低,所以这种交互方式不够直观和友好,我们期望用户采用下面这个方法去与 native 通信:


HybridAPI.invoke(methodName, params, callbackFun)


  • methodName: 当前通信的行为

  • params: 传递的参数

  • callbackFun: 接收 native 端的返回数据


于是,我们在 js 层面进行一层的封装


var callbackId = 0;var callbackFunList = {}HybridAPI.invoke = function(method, params, callbackFun) {var message = {method,params}if (callbackFun) {callbackId = callbackId + 1;message.id = 'Hybrid_CB_' + callbackId;callbackFunList[callbackId] = callbackFun}HybridAPI.sendToNative(JSON.stringify(message));}


最终还是调用的是sendToNative与 native 层进行通信,但是采用HybridAPI.invoke方法对开发者更加友好。


由于需要在执行成功后调用回调函数。为此在发送消息的时候先把callbackFun保存起来,在执行成功后再响应。 当 Javascript 请求发送到 native 层时,会触发sendToNative方法,在该方法中, 我们来解析前端的数据:


@JavascriptInterfacepublic void sendToNative(final String message) {JSONObject object = DataUtil.str2JSONObject(message);if (object == null) {return;}final String callbackId = DataUtil.getStrInJSONObject(object, "id");final String method = DataUtil.getStrInJSONObject(object, "method");final String params = DataUtil.getStrInJSONObject(object, "params");


handleAPI(method, params, callbackId);}


private void handleAPI(String method, String params, String callbackId) {if ("getDeviceInfo".equals(method)) {getDeviceInfo();} else if ("getUserInfo".equals(method)) {getUserInfo();} else if ('login'.equals(method)) {login();}....}


native 端在处理完成后,再调用evaluateJavascriptloadUrl方法,反馈给前端。操作流程示例:


//指定了 js 端的接收入口 public final String TO_JAVASCRIPT_PREFIX = "javascript:HybridAPI.onReceiveData('%s')";


public void callJs() {Map<String, Object> responseData = new HashMap<>();responseData.put("error", error);responseData.put("data", result);//回调函数的 id 标识,返回给 js,这样才能找到对应的回调函数 responseData.put("id", callbackId);sendToJavaScript(responseData);}


public void sendToJavaScript(Map<String, Object> message) {String str = new Gson().toJson(message);final String jsCommand = String.format(TO_JAVASCRIPT_PREFIX, escapeString(str));


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {evaluateJavascript(jsCommand, null);} else {loadUrl(jsCommand);}}


// 转义 private String escapeString(String javascript) {String result;result = javascript.replace("\", "\\");result = result.replace(""", "\"");result = result.replace("'", "\'");result = result.replace("\n", "\n");result = result.replace("\r", "\r");result = result.replace("\f", "\f");return result;}


在上面的callJs方法中组织好相关的数据,然后利用Gson进行序列化,再转进行字符串的转义,最终调用evaluateJavascript或者loadUrl来传递给 js。于是 js 端便可以利用HybridAPI.onReceiveData来接收到。


还记得这段代码中定义的callbackFunList吗?在上面 native 给 js 返回数据的时候,会带上一个id, 我们可以根据这个 id 找到本次通信的回调函数,然后将数据回调过去。


var callbackId = 0;var callbackFunList = {} //看这里 HybridAPI.invoke = function(method, params, callbackFun) {var message = {method,params}if (callbackFun) {callbackId = callbackId + 1;message.id = 'Hybrid_CB_' + callbackId;callbackFunList[callbackId] = callbackFun}HybridAPI.sendToNative(JSON.stringify(message));}


所以,我们 js 端接收数据,可能是这样子:


HybridAPI.onReceiveData = function(message) {var callbackFun = this.callbackFunList[message.id];if (callbackFun) {callbackFun(message.error || null, message.data);}delete this.callbackFunList[message.id];}


再回到我们上面的获取用户信息这个业务功能,我们的写法就会是这样子了:


HybridAPI.invoke('getUserInfo', {"id": "1"}, function(error, data) {if (error) {console.log('获取用户信息失败');} else {console.log('username:' + data.username + ', age:' + data.age);}});


至此,我们就将一具完整的数据通信流程实现了,由 js 端用HybridAPI.invoke(method, params, callbackFun)来向 native 端来发送数据,native 处理完毕后,js 端通过callbackFun来接收数据。

改进

在上面的 java 代码中,我们可以看到,native 层的入口是sendToNative方法,该方法中解析传入的字符串,再交给handleAPI方法来处理


@JavascriptInterfacepublic void sendToNative(final String message) {JSONObject object = DataUtil.str2JSONObject(message);if (object == null) {return;}final String callbackId = DataUtil.getStrInJSONObject(object, "id");final String method = DataUtil.getStrInJSONObject(object, "method");final String params = DataUtil.getStrInJSONObject(object, "params");


handleAPI(method, params, callbackId);}


private void handleAPI(String method, String params, String callbackId) {if ("getDeviceInfo".equals(method)) {getDeviceInfo();} else if ("getUserInfo".equals(method)) {getUserInfo();} else if ('login'.equals(method)) {login();}....}


我们会发现,随着业务的发展,项目的迭代,js 端可能会需要 native 提供越来越多的能力,所以我们的handleAPI方法中就会有越来越多的if...else if...了。


于是,我们可以按业务来划分,新建一个UserController类来处理getUserInfo, login, logout这种与用户相关的 native 接口。新建一个DeviceController来处理类似于getDeviceInfo, getDeviceXXX,... 等与设备信息相关的接口。然后我们再维护一个 controller list, 每次调用 js api 的时候从这个 list 里面去找对应的 controller 中的方法处理。


这样,就可以把具体的业务处理方法抽取出来。然而即便这样,还是避免不了在每个 Controller 中去写一段这个if...else if ...这种代码。于是,其实我们可以很自然的想到用反射来做点事。


我们和 H5 开发约定好了,如果需要获取用户的信息,就调用getUserInfo方法,这个方法名始终不变。同时,我们在 Java 端这样定义UserController:


public class UserController implements IController{


private volatile static UserController instance;private UserController() {}


public static UserController getInstance() {if (i


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


nstance == null) {synchronized(UserController.class) {if (instance == null) {instance = new UserController();}}}return instance;}


@APIMethodpublic UserInfo getUserInfo(Map<String, Object> params, String callbackId) {//TODO}


@APIMethodpublic void login(Map<String, Object> params, INativeCallback callback) {//TODO}


@APIMethodpublic boolean logout(Map<String, Object> params, INativeCallback callback) {//TODO}}


我们将该UserController添加到上面提到的 controller list 中,然后我们在 handleAPI 方法中:


private void handleNativeAPI(String methodName, String params, String callback) {

评论

发布
暂无评论
Android WebView与Native通信总结,Android中高级面试必知必会