JsBridge

简介

Android JsBridge 就是用来在 Android app的原生 java 代码与 javascript 代码中架设通信(调用)桥梁的辅助工具。

原文地址点这里

github点这里

使用方式戳这里

有问题请联系 xesam

原理概述

Javascript 运行在 WebView 中,而 WebView 只是 Javascript 执行引擎与页面渲染引擎的一个包装而已。

由于这种天然的隔离效应,我们可以将这种情况与 IPC 进行类比,将 Java 与 Javascript 的每次互调都看做一次 IPC 调用。

如此一来,我们可以模仿各种已有的 IPC 方式来进行设计,比如 RPC。本文模仿 Android 的 Binder 机制来实现一个 JsBridge。

首先回顾一一下基于 Binder 的经典 RPC 调用:

当然,client 与 server 只是用来区分通信双方责任的叫法而已,并不是一成不变的。

对于 java 与 javascript 互调的情况,当 java 主动调用 javascript 的时候,java 充当 client 角色,javascript 则扮演 server 的角色,

javascript 中的函数执行完毕后回调 java 方法,这个时候,javascript 充当 client 角色,而 javascript 则承担 server 的责任。

剩下的问题就是怎么来实现这个机制了,大致有这么几个需要解决的问题:

  1. java 如何调用 Javascript
  2. Javascript 如何调用 java
  3. 方法参数以及回调如何处理
  4. 通信的数据格式是怎样的

下面逐个讨论这些问题:

1. java 如何调用 Javascript

要实现 Java 与 Javascript 的相互调用,有两条途径可以考虑:

  1. 集成一个定制化的 Javascript 与 Html 渲染引擎,java 通过引擎底层与 Javascript 交互。这样可以获得完全的控制权。
  2. 使用 Android Sdk 提供的交互方法。

对于第一种途径,代价比较大,而且技术方案比较复杂,一般只有基于 Javascript 的跨平台开发方案才会这么做。

所以,现在着重考查第二种途径。

Android 的默认 Sdk 中, Java 与 Javascript 的一切交互都是依托于 WebView 的,大致有以下几个可用方法:

第一:

    webView.loadUrl("javascript:scriptString"); //其中 scriptString 为 Javascript 代码

第二,在 KITKAT 之后,又新增了一个方法:

    webView.evaluateJavascript(scriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) { }
});//其中 scriptString 为 Javascript 代码,ValueCallback 的用来获取 Javascript 的执行结果。这是一个异步掉用。

这个调用看起比上面的正常,而且更像是一个方法调用。

需要注意的是,ValueCallback 并不是在 UI 线程里面执行的。

2. Javascript 如何调用 java

要实现 Javascript 调用 java 方法,需要先在 Javascript 环境中注入一个 Java 代理:


class JavaProxy{
@JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 之后强制要求,不然无法从 Javascript 中发起调用
public void javaFn(){
//xxxxxx
};
} webView.addJavascriptInterface(new JavaProxy();, "java_proxy");

然后在 Javascript 环境中直接调用 obj_proxy 代理上的方法即可。


java_proxy.javaFn();

这里有两个方面需要统一:

  1. Javascript 的执行方法比较怪异,所以,我们需要将概念统一化。
  2. 如果需要执行的方法比较多,那么,代理对象上也需要定义非常多的方法,我们需要将各种方法定义统一起来管理。

所以,我们先将 Javascript 的执行包装成类似 java 一样的代理对象,然后通过在各自的 stub 上注册回调来增加功能支持。

比如,如果 java 想增加 getPackageName 方法,那么,直接在 JavaProxy 上注册即可:


javaProxy.register("getPackageName", new JavaHandler(){
@Override
public void handle(Object value){
//xxxxx
}
})

如图:

3. 方法参数以及回调如何处理

很显然,任何 IPC 通信都涉及到参数序列化的问题, 同理 java 与 Javascript 之间只能传递基础类型(注意,不单纯是基本类型),包括基本类型与字符串,不包括其他对象或者函数。

由于只涉及到简单的相互调用,这里就可以考虑采用 JSON 格式来传递各种数据,轻量而简洁。

Java 调用 Javascript 没有返回值(这里指 loadUrl 形式的调用),因此如果 java 端想从 Javascript 中获取返回值,只能使用回调的形式。

但是在执行完毕之后如何找到正确的回调方法信息,这是一个重要的问题。比如有下面的例子:

在 java 环境中,JavaProxy 对象有一个无参数的 getPackageName 方法用来获取当前应用的 PackageName。

获取到 packageName 之后,传递给 Javascript 调用者的对应回调中。

在 Javascript 环境中,获取当前应用的 PackageName 的大致调用如下:

    bridge.invoke('getPackageName', null, function(packageName){
console.log(packageName);
});

显然

    function(packageName){
console.log(packageName);
}

这个 Javascript 函数是无法传递到 java 环境中的,所以,可以采取的一个策略就是,

在 Javascript 环境中将所有回调统一管理起来,而只是将回调的 id 传递到 java 环境去,java 方法执行完毕之后,

将回调参数以及对应的回调 id 返回给 Javascript 环境,由 Javascript 来负责执行正确的回调。

这样,我们就可以实现一个简单的回调机制:

在 java 环境中


class JavaProxy{
public void onTransact(String jsonInvoke, String jsonParam){
json = new Json(jsonInvoke);
invokeName = json.getInvokeName(); // getPackageName
callbackId = json.getCallbackId(); // 12345678xx
invokeParam = new Param(jsonParam);// null ...
... JsProxy.invoke(callbackId, callbackParam); //发起 Javascript 调用,让 Javascript 去执行对应的回调
}
}

在 javascript 环境中


bridge.invoke = function(name, param, callback){
var callbackId = new Date().getTime();
_callbacks[callbackId] = callback;
var invoke = {
"invokeName" : name,
"callbackId" : callbackId
};
JavaProxy.onTransact(JSON.stringify(invoke), JSON.stringify(param));
} bridge.invoke('getPackageName', null, function(packageName){
console.log(packageName);
});

反之亦然。

4. 通信的数据格式是怎样的

问题都处理了,只需要设计对应的协议即可。

按照上面的讨论,

在 client 端,我们使用:

    Proxy.transact(invoke, callback);

来调用 server 端注册的方法。

在 server 端,我们使用:

    Stub.register(name, handler);

来注册新功能,使用

    Stub.onTransact(invoke, handler);

来处理接收到的 client 端调用。

其中,invoke 包含所要执行的方法以及回调的信息,因此,invoke 的设计如下:

{
_invoke_id : 1234,
_invoke_name : "xxx",
_callback_id : 5678,
_callback_name : "xxx"
}

注意 _invoke_id 与 _invoke_name 的区别:

如果当前 invoke 是一个直接方法调用,那么 _invoke_id 应该是无效的。
如果当前 invoke 是一个回调,那么 _invoke_id + _invoke_name 共同决定回调的具体对象

需要注意的问题

1. 回调函数需要及时删除,不然会引起内存泄漏。

由于我们使用一 Hash 来保存各自环境中的回调函数。如果某个回调由于某种原因没有被触发,那么,这个引用的对象就永远不会被回收。

针对这种问题,处理方案如下:

在 Java 环境中:

如果 WebView 被销毁了,应该手动移除所有的回调,然后禁用 javascript 。

另外,一个 WebView 可能加载多个 Html 页面,如果页面的 URL 发生了改变,这个时候也应该清理所有的回调,因为 Html 页面是无状态的,也不会传递相互数据。

这里有一点需要注意的是,如果 javascript 端是一个单页面应用,应该忽略 url 中 fragment (也就是 # 后面的部分) 的变化,因为并没有发生传统意义上的页面跳转,

所有单应用的 Page 之间是可能有交互的。

在 javascript 环境中:

javascript 端情况好很多,因为 WebView 会自己管理每个页面的资源回收问题。

使用

必要配置

请在对应的 html 页面中引入

    <script src="js-bridge.js"></script>

Java 环境

初始化 JsBridge:

    jsBridge = new JsBridge(vWebView);

加入 url 监控:

    vWebView.setWebViewClient(new WebViewClient() {

        @Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e("onPageFinished", url);
jsBridge.monitor(url);
}
});

Java 注册处理方法:

    jsBridge.register(new SimpleServerHandler("showPackageName") {
@Override
public void handle(String param, ServerCallback serverCallback) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
String packageName = getPackageName();
Tip.showTip(getApplicationContext(), "showPackageName:" + packageName);
}
});
}
});

Java 在处理方法中回调 Javascript:


@Override
public void handle(final String param, final ServerCallback serverCallback) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
User user = getUser();
Map<String, String> map = new Gson().fromJson(param, Map.class);
String prefix = map.get("name_prefix");
Tip.showTip(mContext, "user.getName():" + prefix + "/" + user.getName());
if ("standard_error".equals(prefix)) {
Map<String, String> map1 = new HashMap<>();
map1.put("msg", "get user failed");
String userMarshalling = new Gson().toJson(map1);
serverCallback.invoke("fail", new MarshallableObject(userMarshalling));
} else {
String userMarshalling = new Gson().toJson(user);
serverCallback.invoke("success", new MarshallableObject(userMarshalling));
}
}
});
}

Java 执行 Js 函数:


jsBridge.invoke("jsFn4", new MarshallableString("yellow"), new ClientCallback<String>() {
@Override
public void onReceiveResult(String invokeName, final String invokeParam) {
if ("success".equals(invokeName)) { new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Tip.showTip(getApplicationContext(), invokeParam);
}
});
}
} @Override
public String getResult(String param) {
return param;
}
});

销毁 JsBridge

    @Override
protected void onDestroy() {
super.onDestroy();
jsBridge.destroy();
}

Javascript 环境

Javascript 的灵活性比较高,所以要简单一些:

Javascript 注册处理函数:

    window.JavaBridge.serverRegister('jsFn4', function (transactInfo, color) {
log("jsFn4:" + color);
title.style.background = color;
log("jsFn4:callback");
transactInfo.triggerCallback('success', 'background change to ' + color);
});

Javascript 执行 Java 方法:


var sdk = {
getUser: function (params) {
var _invokeName = 'getUser';
var _invokeParam = params;
var _clientCallback = params;
window.JavaBridge.invoke(_invokeName, _invokeParam, _clientCallback);
}
}; sdk.getUser({
"name_prefix": "standard_error",
"success": function (user) {
log('sdk.getUser,success:' + user.name);
},
"fail": function (error) {
log('sdk.getUser,fail:' + error.msg);
}
})

详细 Demo 请参见 js-bridge-demo 工程

【Android】如何写一个JsBridge的更多相关文章

  1. 【Android】怎样写一个JsBridge

    JsBridge 简单介绍 Android JsBridge 就是用来在 Android app的原生 java 代码与 javascript 代码中架设通信(调用)桥梁的辅助工具. 原文地址点这里 ...

  2. android 开发 写一个RecyclerView布局的聊天室,并且添加RecyclerView的点击事件

    实现思维顺序: 1.首先我们需要准备2张.9的png图片(一张图片为左边聊天泡泡,一个图片为右边的聊天泡泡),可以使用draw9patch.bat工具制作,任何图片导入到drawable中. 2.需要 ...

  3. Android 自己写一个打开图片的Activity

    根据记忆中eoe的Intent相关视频,模仿,写一个打开图片的Activity 1.在主Activity的button时间中,通过设置action.category.data打开一个图片.这时代码已经 ...

  4. 在Android 下写一个检测软件版本号 以自动升级APP 的插件

    直接上图上代码: 1.插件类的编写 工程目录结构图: 代码如下: package org.apache.cordova.versionupdate; import org.apache.cordova ...

  5. Android学习--写一个发送短信的apk,注意布局文件的处理过程!!!

    刚开始写Android程序如图发现使用了findViewById方法之后输出的话居然是null(空指针错误),也就是说这个方法没有成功.网上说这样写是在activity_main .xml去找这个ID ...

  6. 用android去写一个小程序

    前言: 软工的一个小作业:实现"黄金分割小游戏", 需要结对编程,队友:陈乐云    共用时两天. 早期思路设计: 采用键值对的形式,以Map作为存储结构.优点:能够将数据与用户对 ...

  7. Android下写一个永远不会被KILL掉的进程/服务

    Android 系统对于内存管理有自己的一套方法,为了保障系统有序稳定的运信,系统内部会自动分配,控制程序的内存使用.当系统觉得当前的资源非常有限的时候,为了保证一些优先级高的程序能运行,就会杀掉一些 ...

  8. android开发 写一个自定义形状的按键

    步骤: 1.在drawable 文件夹中创建一个xml布局文件. 2.修改布局文件 3.在需要使用背景的按键中导入布局. 创建布局文件: 修改布局文件: <?xml version=" ...

  9. android怎样写一个循环文字滚动的TextView

    效果图: 在layout中这样来声明: <com.kaixin001.view.ScrollText android:id="@+id/news_statustxt" and ...

随机推荐

  1. 检查密码复杂度的C#正则表达式

    在用户注册与修改.重置密码时,强制密码达到一定的复杂度,是减少盗号的有效措施之一. 而在代码中检查密码复杂度就需要用到正则表达式,比如要求密码必须包含数字.小写或大写字母.特殊字符.字符数在8-30之 ...

  2. 一起来花5分钟写一个PHP入门Demo

    最近公司招了几个应届毕业生,他们对前端的了解还挺多,但是对后端的技术一无所知,我觉得,作为一个前端攻城狮,如果你有远大的抱负,就应该雨露均沾... 今天我就跟大家讲一讲PHP最基本的入门,至少别人问起 ...

  3. [ucgui] 对话框1——创建对话框

    >_<" 小工具和对话框的区别: 小工具可以创建并独立使用,因为它们本质上就是窗口.但是,通常需要使用对话框,它是包含一个或多个小工具的窗口. 对话框通常是一个窗口,它在出现时会 ...

  4. html中的图片直接使用字符串代替

    最近来了一个网页,里面有图片,但是却没有引用外部的图片资源,很好奇.查看代码后发现,里面的图片是使用base64编码后的字符串代替了,这个叫做Data URI scheme. Data URI sch ...

  5. centos6.4 64位下安装nfs文件共享系统

    不知道谁装的服务器,默认自带,以下内容摘自互联网,配置部分按教程执行成功 一.环境介绍: 服务器:centos 192.168.1.225 客户端:centos 192.168.1.226 二.安装: ...

  6. 常用dom对象

    DOM:文档对象模型 --树模型 文档:标签文档,对象:文档中每个元素对象,模型:抽象化的东西 一:window: window.shuxing; 属性(值或者子对象): opener:打开当前窗口的 ...

  7. atitit.html5 拼图游戏的解决之道.

    atitit.html5 拼图游戏的解决之道. 1. 拼图游戏的操作(点击法and 拖动法) 1 1. 支持键盘上.下.左.右键移动: 1 2. 支持点击空白模块中的上下左右箭头移动: 1 3. 支持 ...

  8. jQuery获取浏览器URL链接的值

    代码: 方法一: $.extend({ getUrlVars: function () { var vars = [], hash; ).split('&'); ; i < hashes ...

  9. Python之Django【进阶篇 】

    Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接数据库,并编写数据访问层代码 业务逻辑层去调用数据访问层执行 ...

  10. 安装 Autoconf 2.69版

    发生错误configure.ac:8: error: Autoconf version 2.64 or higher is required 1.检查版本 [root@localhost Deskto ...