主讲人:吴彬

要学习某个东西之前,我们首先要了解这个东西是什么?然后我们要了解这东西有什么用,有什么好处和弊端?最后我们要知道这东西怎么用? 简单点就是 ——是什么?有什么用?怎么用? 那么进入正题

一、什么是Hybrid 开发?

Hybrid App开发(混合模式移动应用开发)是指开发介于web-app、native-app这两者之间的app,这种app兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。

二、存在即合理,Hybrid开发的意义

首先,我们看下Hybrid app和web-app、native-app之间的一些区别

可见Hybrid app开发相比于其他开发还是好处多多的,那么它相比于Native开发的好处有哪些呢?

使用Native开发的方式人员要求高,只是一个简单的功能就需要iOS程序员和Android程序员各自完成;

使用Native开发的方式版本迭代周期慢,每次完成版本升级之后都需要上传到App Store并审核,升级,重新安装等,升级成本高;

使用hybrid开发的方式简单方便,同一套代码既可以在IOS平台使用,也可以在Android平台使用,提高了开发效率与代码的可维护性;

使用hybrid开发的方式升级简单方便,只需要服务器端升级一下就好了,对用户而言完全是透明了,免去了Native升级中的种种不便;

三、如何进行Hybrid 开发?

Hybrid 开发可以使用第三方框架(此处不细讲,有兴趣的朋友可以自行了解),也可以用Android自带的webView来加载H5 那么我们来讲讲android 自带的webView和Js之间的互调吧。

首先,别忘记加网络权限和引入webView.

Native调用JS

在4.4之前,调用的方式:

	//ui线程中运行,因为mWebView是UI控件
runOnUiThread(new Runnable() {
@Override
public void run() {
//缺点:无法取得返回值
mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");
Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
}
});

4.4以后(包括4.4),使用以下方式:

	mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//这里的value即为对应JS方法的返回值
}
});

传统的Native调用JS还有个缺点就是不适合传输大量数据(大量数据建议用接口方式获取)。

JS调用Native

Native中通过addJavascriptInterface添加暴露出来的JS桥对象,然后再该对象内部声明对应的API方法。

WebSettings webSettings = mWebView.getSettings();
//Android容器允许JS脚本
webSettings.setJavaScriptEnabled(true);
//Android容器设置侨连对象,api17前有可被hacker攻击的漏洞,api17(4.4)之后调用需要在调用方法加入加入@JavascriptInterface注解,
//如果代码无此申明,那么也就无法使得js生效,也就是说这样就可以避免恶意网页利用js对安卓客户端的窃取和攻击。
mWebView.addJavascriptInterface(this, "JSBridge");

首先看下这个方法

public void addJavascriptInterface(Object object, String name)

我是在Activity里 写的这段代码,那么这里第一个参数传入this,即Activity实例,第二个参数则是给第一个参数起的别名,方便JS里调用。

然后在Activity里暴露两个方法

		//在Android4.2以上(api17后),暴露的api要加上注解@JavascriptInterface,否则会找不到方法
@JavascriptInterface
public String getStudentName(){
return "张三";
} @JavascriptInterface
public String getMyName(final String name){
return "my name is:" + param;
}

然后我们在HTML里调用Native方法

	//调用方法一
window.JSBridge.getStudentName(); //返回:'张三'
//调用方法二
window.JSBridge.getMyName('韩梅梅');//返回:'my name is 韩梅梅'

OK,以上就是我们传统的调用方式,但是显而易见,传统的调用存在许多的不足,所以出现了许多好用的第三方框架,那么接下来,我们讲解一个常用框架JSBridge

四、JSBridge

OK,让我们提出我们的疑问,什么是JSBridge?JSBridge有什么好处(相比于传统的Native和JS互调)?JSBridge是怎么设计出来的呢?JSBridge怎么使用呢?

1、什么是JSBridge?

JSBridge就是js和Native之前的桥梁,是用来定义Native和JS通信的,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用Native。

2、为什么使用JSBridge?

Android4.2以下,addJavascriptInterface方式有安全漏掉 iOS7以下,JS无法调用Native JSBridge有一套现有的成熟方案,可以完美兼容各种版本,对以前老版本技术的兼容。

3、JSBridge的原理与实现

我们知道,Native要调用JS是非常简单的,只要使用WebView.loadUrl(“JavaScript:function()”)即可。那么JS怎么调Native呢?很明显,上面讲的已经行不通了,毕竟有兼容性问题和安全问题。那么怎么办呢?我们得寻找一个突破口吧,大家仔细回一下,WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,我们是不是可以利用这个机制,自己做一些处理?答案是肯定的。

至于js这三个方法的区别,可以详见w3c JavaScript 消息框 。一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,然后让你输入,输入完成后返回输入框中的内容。因此,占用prompt是再完美不过了。

OK,找到突破口后,我们就开始实现通信,那么怎么实现呢?既然要通信,那么我们是不是得有通信协议呢?我们可以参考http 的样式http://host:port/path?param=value 来 规定一个属于我们自己的协议:

jsbridge://className:port/methodName?jsonObj

这样我们Native拿到这串字符串后,通过解析,就可以知道调用哪个类的哪个方法,此处jsonObj用来封装请求的参数。

细心的朋友一点发现了里面有个port,那么这个port是拿来干什么的呢? 其实js层调用native层方法后,native需要将执行结果返回给js层,不过你会觉得通过WebChromeClient对象的onJsPrompt方法将返回值返回给js不就好了吗,其实不然,如果这么做,那么这个过程就是同步的,如果native执行异步操作的话,返回值怎么返回呢?这时候port就发挥了它应有的作用,我们在js中调用native方法的时候,在js中注册一个callback,然后将该callback在指定的位置上缓存起来,然后native层执行完毕对应方法后通过WebView.loadUrl调用js中的方法,回调对应的callback。那么js怎么知道调用哪个callback呢?于是我们需要将callback的一个存储位置传递过去,那么就需要native层调用js中的方法的时候将存储位置回传给js,js再调用对应存储位置上的callback,进行回调。

上面是js向native的通信协议,那么另一方面,native向js的通信协议也需要制定,一个必不可少的元素就是返回值,这个返回值和js的参数做法一样,通过json对象进行传递,该json对象中有状态码code,提示信息msg,以及返回结果result,如果code为非0,则执行过程中发生了错误,错误信息在msg中,返回结果result为null,如果执行成功,返回的json对象在result中。下面是两个例子,一个成功调用,一个调用失败。

	{
"code":500,
"msg":"method is not exist",
"result":null
} {
"code":0,
"msg":"ok",
"result":{
"key1":"returnValue1",
"key2":"returnValue2",
"key3":{
"nestedKey":"nestedValue"
"nestedArray":["value1","value2"]
}
}
}

Ok,这样我们在native里调用

mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");

即可,在调用JS暴露的方法时顺便把port带上,为什么要带port,可以参考上面一段解释。

好了,那么我们可以根据以上的内容先来设计JS了(看注释的重点)

	(function (win) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var JSBridge = win.JSBridge || (win.JSBridge = {});
var JSBRIDGE_PROTOCOL = 'JSBridge';
var Inner = {
callbacks: {},
call: function (obj, method, params, callback) {
console.log(obj+" "+method+" "+params+" "+callback);
var port = Util.getPort();
console.log(port);
//重点
this.callbacks[port] = callback;
var uri=Util.getUri(obj,method,params,port);
console.log(uri);
window.prompt(uri, "");
},
onFinish: function (port, jsonObj){
//重点
var callback = this.callbacks[port];
callback && callback(jsonObj);
delete this.callbacks[port];
},
};
var Util = {
getPort: function () {
return Math.floor(Math.random() * (1 << 30));
},
getUri:function(obj, method, params, port){
params = this.getParam(params);
var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
return uri;
},
getParam:function(obj){
if (obj && typeof obj === 'object') {
return JSON.stringify(obj);
} else {
return obj || '';
}
}
};
for (var key in Inner) {
if (!hasOwnProperty.call(JSBridge, key)) {
JSBridge[key] = Inner[key];
}
}
})(window);

我们在call()方法里随机生成领域各port后将callback存入callbacks数组中的port位置,根据之前我们定义的协议生成uri,调用window.prompt(uri,””); 我们还需要一个方法来处理js回调,所以有了onFinish方法,方法里面做的事:首先根据port从callbacks中取出callback执行,然后将callbacks的port位置元素删除。

好了,JS这边处理完了,那我们native这边是不是要暴露方法给JS调用呢? 没错,我们需要一个类似android 原生的@javascriptInterface引用,就是暴露给JS调用的方法,所以我们有了如下的方法

JSBridge.register("jsName",javaClass.class)

这个javaClass就是满足某种规范的类,该类中有满足规范的方法,我们规定这个类需要实现一个空接口,为什么呢?主要作用就混淆的时候不会发生错误,还有一个作用就是约束JSBridge.register方法第二个参数必须是该接口的实现类。那么我们定义这个接口

public interface IBridge{
}

类规定好了,类中的方法我们还需要规定,为了调用方便,我们规定类中的方法必须是static的,这样直接根据类而不必新建对象进行调用了(还要是public的),然后该方法不具有返回值,因为返回值我们在回调中返回,既然有回调,参数列表就肯定有一个callback,除了callback,当然还有前文提到的js传来的方法调用所需的参数,是一个json对象,在java层中我们定义成JSONObject对象;方法的执行结果需要通过callback传递回去,而java执行js方法需要一个WebView对象,于是,满足某种规范的方法原型就出来了。

public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){

}

OK,那我们看看这个register方法究竟做了些什么事吧

	public class JSBridge {
private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>(); public static void register(String exposedName, Class<? extends IBridge> clazz) {
if (!exposedMethods.containsKey(exposedName)) {
try {
exposedMethods.put(exposedName, getAllMethod(clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
} private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
HashMap<String, Method> mMethodsMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method : methods) {
String name;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if (null != parameters && parameters.length == 3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == Callback.class) {
mMethodsMap.put(name, method);
}
}
}
return mMethodsMap;
}
}

简单来说,我们这个方法做的事就是先判断这个类的别名(此处是jsName)是否存在,不存在的话就往里面添加该类暴露出来的方法(三个参数要满足上面的规范的方法)

好了,我们暴露出去方法了,那么这时候js调用后,从我们的突破口,没错,就是onJsPrompt方法里,我们要做的事就是调用我们的native方法,所以有了以下代码

	public class JSBridgeWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.callJava(view, message));
return true;
}
}

别忘了给webView设置

mWebView.setWebChromeClient(new JSBridgeWebChromeClient());

我们callJava方法里做了什么事呢?其实就是解析url,查找对应的暴露方法调用

	public static String callJava(WebView webView, String uriString) {
String methodName = "";
String className = "";
String param = "{}";
String port = "";
if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
Uri uri = Uri.parse(uriString);
className = uri.getHost();
param = uri.getQuery();
port = uri.getPort() + "";
String path = uri.getPath();
if (!TextUtils.isEmpty(path)) {
methodName = path.replace("/", "");
}
} if (exposedMethods.containsKey(className)) {
HashMap<String, Method> methodHashMap = exposedMethods.get(className);
//重点
if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method != null) {
try {
//重点
method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return null;
}

好了,最后,我们当然是来讲Callback啦,看重点注释就可以了,其实就是回调js的onFinish方法。

	public class Callback {
private static Handler mHandler = new Handler(Looper.getMainLooper());
//重点
private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
private String mPort;
//为了防止内存泄露,这里使用弱引用
private WeakReference<WebView> mWebViewRef; public Callback(WebView view, String port) {
mWebViewRef = new WeakReference<>(view);
mPort = port;
} public void apply(JSONObject jsonObject) {
final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
//重点
mWebViewRef.get().loadUrl(execJs);
}
}); } }
}

好了,最后我们来测测吧,也就是我们说的使用

	public class BridgeImpl implements IBridge {
public static void showToast(WebView webView, JSONObject param, final Callback callback) {
String message = param.optString("msg");
Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
if (null != callback) {
try {
JSONObject object = new JSONObject();
object.put("key", "value");
object.put("key1", "value1");
callback.apply(getJSONObject(0, "ok", object));
} catch (Exception e) {
e.printStackTrace();
}
}
} private static JSONObject getJSONObject(int code, String msg, JSONObject result) {
JSONObject object = new JSONObject();
try {
object.put("code", code);
object.put("msg", msg);
object.putOpt("result", result);
return object;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}

别忘了注册

JSBridge.register("bridge", BridgeImpl.class);

JS这边我只放重点的那行代码

<button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">

接着就是使用WebView将该index.html文件load进来测试了

mWebView.loadUrl("file:///android_asset/index.html");

点击按钮后的结果截图

好了,那我们来试试子线程回调,在在BridgeImpl中加入测试方法

	public static void testThread(WebView webView, JSONObject param, final Callback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
JSONObject object = new JSONObject();
object.put("key", "value");
callback.apply(getJSONObject(0, "ok", object));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}).start();
}

JS中加入


<button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">

点击后,3秒弹出

有些框架里面的突破口不在onJsPrompt方法里,在shouldOverrideUrlLoading方法里,不过原理应该差不多。

好了,完结,撒花,放鞭炮,收工!

Hybrid 开发的更多相关文章

  1. Android Hybrid开发

    参考:谈谈Android App混合开发 Html 5和Native的交互 WebView 本来就支持js和Java相互调用,你只需要开启 WebView 的JavaScript脚本执行, 然后通过代 ...

  2. 有关 Hybrid 开发模式实践总结

    前言 随着公司业务不断发展,移动开发项目越来越多,项目任务时间紧,我们内部开发流程是以项目为导向,有别于一般公司对产品不断迭代的做法,但移动端开发人员资源有限,需要在不同项目之间做业务场景切换开发,就 ...

  3. H5 hybrid开发-前端资源本地化方案纪要

    H5 hybrid-前端资源本地化方案纪要 就整个行业来说,大前端是趋势,现阶段,native方面除了一些偏CPU密集型工作与操作系统底层API方面的工作外,H5基本都可以满足需要. 目前的工作更偏向 ...

  4. zx-editor 移动端(HTML5)富文本编辑器,可与原生App混合(hybrid)开发

    ZxEditor 移动端HTML文档(富文本)编辑器,支持图文混排.引用.大标题.无序列表,字体颜色.加粗.斜体. 可用于独立web项目开发,也可以用于与原生App混合(hybrid)开发. 源码地址 ...

  5. hybrid开发设计

    hybrid方案背景 大部分业务都是在不停改变的,我们希望native不发布新版本就可以让线上用户使用新功能.我们要实现这样的方式,采用h5来实现就可以满足这一要求,准确说是native里提供一个装载 ...

  6. “榕树下·那年”移动app ( hybrid ) 开发总结

        榕树下网站本身的技术人员并不多,所以app开发的任务就到了母公司盛大文学这边.       盛大文学无线业务中心负责这次具体开发任务.       一如既往的,开发的情况是:时间紧,任务重,人 ...

  7. 【前端知识体系-JS相关】对移动端和Hybrid开发的理解?

    1.hybrid是什么,为何使用hybrid呢? 概念: hybrid就是前端和客户端的混合开发 需要前端开发人员和客户端开发人员配合完成 某些环节也可能会涉及到server端 大前端:网页.APP. ...

  8. 利用C#开发移动跨平台Hybrid App(一):从Native端聊Hybrid的实现

    0x00 前言 前一段时间分别读了两篇博客,分别是叶小钗兄的<浅谈Hybrid技术的设计与实现>以及徐磊哥的<从技术经理的角度算一算,如何可以多快好省的做个app>.受到了很多 ...

  9. Hybrid APP混合开发的一些经验和总结

    http://www.cnblogs.com/kingplus/p/5588339.html 写在前面: 由于业务需要,接触到一个Hybrid APP混合开发的项目.当时是第一次接触混合开发,有一些经 ...

随机推荐

  1. MFC_2.4 组合框和图片控件

    组合框和图片控件 1.拖控件 图片属性更改Type 为Bitmap 名字也要改,不能为IDC_STATIC 绑定变量控件,重命名. 2.初始化 // 设置一个定时器,用于更新图片 SetTimer(0 ...

  2. Linux终端常用快捷操作

    命令或文件名自动补全:在输入命令或文件名的前几个字母后,按Tab键,系统会自动补全或提示补全 上下箭头:使用上下箭头可以回溯之前的命令,增加命令的重用,减少输入工作量 !加之前输入过的命令的前几个字母 ...

  3. CAD得到所有组名(网页版)

    主要用到函数说明: _DMxDrawX::GetAllGroupName 得到所有组名. js代码实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ...

  4. java_线程类的基本功能

    Thread类是实现了Runnable接口 其方法有: start()开始:开始线程 run()跑:线程内容 currentThread()现在的线程:返回当前线程 getName():获取线程名 s ...

  5. vue-quill-editor + element-ui upload实现富文本图片上传

    代码贴上 <template> <div class="quill-editor-example"> <div class="box&quo ...

  6. C# Task详解

    1.Task的优势 ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便.比如: ◆ ThreadPool不支持线程的取消.完成.失败通知等交互性 ...

  7. Error LNK2019: unresolved external symbol C++模板类声明与定义链接错误问题

    编译器在编译模板时,并不会生成代码,只有遇到实例化的时候才会生成代码.因此,当我们只引用模板声明文件的时候,在实例化的对象时候,模板的定义问文件是不可见的,于是出现链接错误.例如: //A.h #pr ...

  8. Java对象序列化为什么要使用SerialversionUID

    1.首先谈谈为什么要序列化对象 把对象转换为字节序列的过程称为对象的序列化. 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通 ...

  9. 用php生成HTML文件的类

    目的 用PHP生成HTML文档, 支持标签嵌套缩进, 支持标签自定义属性 起因 这个东西确实也是心血来潮写的, 本来打算是输出HTML片段用的, 但后来就干脆写成了一个可以输出完整HTML的功能; 我 ...

  10. vim基础(一)

    今天看了下兄弟连的VIM讲解,又学了几个新命令,记录一下. 插入与删除 插入 首先还是插入,以前只知道i.今天发现原来还有a\A\i\I\o\O,下面具体说一下: 命令 含义 a 在光标后插入 A 在 ...