现在很多App都采用了混合开发,对于展示性强的界面,可以用H5去实现;功能性强的的可以在用native实现。在混合开发中可以说native和JS进行交互肯定是要涉及到的,当然如果你们项目不是混合开发,某些地方只是需要展示一下H5界面即可,也就涉及不到这块。说到交互,虽然Android系统为我们提供的@JavascriptInterface这种交互方式,但是由于兼容性与安全性的问题,我们基本不再使用。今天所说的JsBridge就是安全性和兼容性比较好的一种交互方式,在混合式的开发中非常受欢迎。
实现native层对JS的单向通信,只需要调用方法Webview.loadUrl("JavaScript:function()")即可。那如何实现H5层对native层的通信呢? JS层调用native层不是通过某一个方法就能直接调用的,他是一层一层调用的。为了方便理解,首先得提一下native层的WebChromClient这个类,这个类有三个方法分别是onJsAlert,onJsConfirm,onJsPrompt。另外还得提一下JS层的window这个对象也有三个方法window.alert,window.confirm,window.prompt,当JS层调用windown对象中的某个方法的时候,相应的就会触发WebChromClient对象中对应的方法。说到这里你可能应该猜到了,我们就是通过这种响应机制来实现JS层对native层的通信。这三个方法用哪个呢?一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。说到这,JS层对native层的通信的核心连接点已经说完了。你可能会疑惑,说好的JsBridge呢?毛都没看见,交互就结束了?千万别急啊,话说JSBridge就是一个协议,类似于http这种协议的东西。简单点说,这个协议就是native层和JS层约定好的一个协议,你JS层传给我的信息得按这个协议来进行包装,层层传递到native层。native层接收到这个信息的时候,也是根据这个协议进行解析获取我们需要的信息,比如你调我的哪个类,哪个方等等.这个后面会细说(jsbridge://className:port/methodName?jsonObj)
依赖:compile 'com.msj.javajsbridge:javajsbridge:1.0.4'
上面说的,大家只要知道JS调用native层的调用机制就行了。JS调用下面直接上代码分析流程,每次只要按这个流程一步一步的走,实现起来也不是什么难事。
1.从JS层看起(下面的代码)。JS交互时候,JS层首先调用的是call(obj,method,params,callback)这个方法。(obj:调用的类名。method:调用哪个方法。params:参数。 callback:回调处理native返回给JS的处理结果。 port:随机生成的一个值,它是与callback对应的)。通过getUri将信息按协议封装成uri,在调用window.prompt(uri,“”)方法,这时候native层就会相应的调用onJsPrompt方法,并且会接收JS层传来的uri信息。onFinish(port,jsonObj)方法回调的时候用,暂时不说,只要注意下有这么个东西就行。(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);
2.这里我们先自定义一个WebView类:主要用来进行一些设置。 第一,设置我们自定义的WebchromClient类。这里面为了扩展性,给自定义的WebchromClient加了个接口,方便在Activity或者Fragment中进行处理。 第二, register(“bridge”,CommonBridgeImp)反射获取native层CommonBridgeImp类中暴露给JS层的所有方法,并且存到一个HashMap
@Keep
public class CommonWebView extends WebView {
/**
* context of the webview page.
*/
Context context;
/**
* the webview that show the page.
*/
WebView webView;
/**
* activity of the webview.
*/
Activity activity;
/**
* the root url of the website. url include index.html.
*/
String rootUrl = "";
/**
* loading dialog.
*/
LoadingDialog dialog;
/**
* ICommonActivityImp
*/
ICommonActivityImp commonActivityImp;
@Keep
public CommonWebView(Context context,
String rootUrl,
ICommonActivityImp commonActivityImp) {
super(context);
this.context = context;
this.rootUrl = rootUrl;
this.activity = (Activity) context;
this.commonActivityImp = commonActivityImp;
}
/**
* init webview settings.
*/
public void initWebView(final LoadingDialog dialog) {
webView = this;
this.dialog = dialog;
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setGeolocationEnabled(true);//webview华为7.0定位
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);//解决echats插件图表无法截图问题
settings.setDomStorageEnabled(true);//设置允许本地存储
settings.setAppCacheEnabled(true);
settings.setAllowFileAccess(true);// 设置允许访问文件数据
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(0);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
settings.setMediaPlaybackRequiresUserGesture(true);
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
webView.getSettings().setAppCacheMaxSize(1024 * 1024 * 8);
}
settings.setAppCachePath(context.getCacheDir().getAbsolutePath());
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
CommonJSBridge.register("bridge", CommonBridgeImp.class);
webView.setWebChromeClient(new CommonJSBridgeWebChromeClient(activity, commonActivityImp));
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (dialog != null && !dialog.isShowing()) {
dialog.show();
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
}
});
}
3.自定义WebChromClient这个类,通过onJsPrompt方法接收JS传来的uri,即是这里的message。onJsPrompt方法会调用result.confirm()==>CommonJsBridge.callJava();这里就是将uri传入到CommonJsBridge类中了,进行下一步的解析处理,并进行native层的方法调用。
public class CommonJSBridgeWebChromeClient extends WebChromeClient {
Activity mActivity;
ICommonActivityImp commonActivityImp;
public CommonJSBridgeWebChromeClient(Activity activity, ICommonActivityImp commonActivityImp) {
this. mActivity = activity;
this. commonActivityImp=commonActivityImp;
}
/**
* 核心方法,整个原生和h5交互的方式都是通过获取h5页面调用原生时通过osJsPrompt方法的传值进行交互
* @param view
* @param url
* @param message (uri)
* @param defaultValue
* @param result
* @return
*/
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(CommonJSBridge.callJava(view, message,commonActivityImp));
return true;
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
mActivity.setProgress(newProgress);
super.onProgressChanged(view, newProgress);
}
}
我写的接口(仅供参考,根据自己业务需求定):
public interface ICommonImp{
}
public interface ICommonActivityImp extend ICommonImp {
void jumpNextBanner(WebView webView, String type, String value, Callback callback);
}
4.上面调用的callJava()方法,就在CommonJsBridge这个类里面,里面是根据JsBridge协议用来解析uri,可以看看下面的注释。获取将要调用的类名,方法名,以及Port ,然后去集合中找到此类名的此方法。 一般的,我们将这些方法统一写到一个类中,通过反射获取这个类中的所有方法并保存到HashMap集合exposeMethods中。
public class CommonJSBridge {
/**
* 此方法通过WebView的onJsPrompt方法调用,为js调用原生的方法,根据方法名从methodMap拿到方法,反射调用,并将参数传进去
*
* @param webView
* @param uriString 此参数为h5页面调用原生时的uri,格式如:(JSBridge://className:port/methodName?jsonObj),格式与JSBridge.js文件中定义一致
* @return
* @see CommonJSBridgeWebChromeClient# onJsPrompt(WebView, String, String, String, JsPromptResult)
*/
public static String callJava(WebView webView, String uriString, ICommonImp baseWebImp) {
String methodName = "";//暴露给js的类中的方法
String className = "";//暴露给js的类
String param = "{}";//json格式的参数,可是字符串,自己定义
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 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, param, new Callback(webView, port), baseWebImp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return null;
}
/**
* 暴露的类方法集合<类,此类的所有方法>
*/
private static Map> exposedMethods = new HashMap<>();
/**
* 注册暴露给js页面的类
*
* @param
*/
public static void register(String exposedName, Class clazz) {
if (!exposedMethods.containsKey(exposedName)) {
try {
exposedMethods.put(exposedName, getAllMethod(clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 反射获取暴露给js的类中的方法
* 暴露的方法必须是public static ,参数必须是(WebView,String,Callback),此处可修改,与BridgeImpl类中暴露的方法一致
*
* @param injectedCls
* @return
* @throws Exception
*/
private static HashMap getAllMethod(Class injectedCls) throws Exception {
HashMap 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 == 4) {
if (parameters[0] == WebView.class && parameters[1] == String.class && parameters[2] == Callback.class && parameters[3] == ICommonActivityImp.class) {
mMethodsMap.put(name, method);
}
}
}
return mMethodsMap;
}
}
4.下面这个类CommonBridgeImp这个类是用来写暴露给JS层的所有的方法,注意方法必须用static void 修饰,并且要实现空的接口IBridge主要是为了混淆的时候不会错。如果 需要Activity或者Fragment处理的调用上文自定义的ICommonBridgeImp这个接口回调。如果需要将数据返回给JS层或者再次调用JS层的方法,就通过Callback回调,将数据返回给JS层处理。
/**
* 暴露给JS调用的原生的方法
*/
public class CommonBridgeImp implements IBridge {
public static Handler handler = new Handler();
public Context context;
private CommonWebView commonWebView;
private CommonBridgeImp(Context context, CommonWebView commonWebView) {
this.context = context;
this.commonWebView = commonWebView;
}
/**
* 交互暴露的方法 fromJsFunction_test1
* @param webView
* @param param
* @param callback
*/
public static void fromJsFunction_test1(final WebView webView, String param, final Callback callback, final ICommonActivityImp commonActivityImp) {
ArrayList list = new ArrayList<>();
list.add("type");
list.add("value");
String[] strArray = CommonUtil.getStrArray(param, list);
final String type = strArray[0];
final String value = strArray[1];
handler.post(new Runnable() {
@Override
public void run() {
commonActivityImp.jumpNextBanner(webView, type, value, callback);
}
});
}
}
5.这个CallBack是个啥?为什么哪个里面都有它?返回给JS层为什么得经过他呢?这个得从JS层说起,简单理一下就明白了。JS层调用native层的时候,会先在JS层产生一个port值,然后构造一个Callback回调保存起来,并且将port值封装到Uri里面传给native层,native层会将这个port值封装到自定义Callback中。等到native处理完数据之后,再回调JS层的onFinish方法,并将参数和port值一起传递回去。JS层在回调会根据这个port值找到对应的Callback做进一步的处理,并将这个Callback删除。
package com.sgcc.cs.h5webviewpage.JSUtil;
import android.os.Handler;
import android.os.Looper;
import android.webkit.WebView;
import java.lang.ref.WeakReference;
/**********************************************************************************
* @Version : V4.0
* @Date: 2017/2/23 09:42
* @Description: 作用 回调,原生应用处理完成后,回调js中回调方法的类
* *********************************************************************************
* History: update past records
*
* 修改内容:涉及文件:
* *********************************************************************************
*/
public class Callback {
private static Handler mHandler = new Handler(Looper.getMainLooper());
/**
* JSON串格式化参数
*/
private static final String CALLBACK_JS_FORMAT_JSON = "javascript:JSBridge.onFinish('%s', %s);";
/**
* String串格式化参数,需要加引号,不然不会调用JSBridge.js中onFinish方法;
* 原因:JavaScrip参数是String类型时,前后需要加引号
*/
private static final String CALLBACK_JS_FORMAT_STRING = "javascript:JSBridge.onFinish('%s', \"%s\");";
private String mPort;
private WeakReference mWebViewRef;
public Callback(WebView view, String port) {
mWebViewRef = new WeakReference<>(view);
mPort = port;
}
/**
* 若需要回调js中的回调方法,在相应的地方调用 Callback.apply(jsonObject)
* @param jsonObject
*/
public void apply(String jsonObject) {
String resultStr;
if (!"".equals(jsonObject)&&jsonObject.startsWith("{")){//判断是否为JSON,若不是,需要加引号
resultStr = String.format(CALLBACK_JS_FORMAT_JSON, mPort, jsonObject);
}else {
resultStr = String.format(CALLBACK_JS_FORMAT_STRING, mPort, jsonObject);
}
final String execJs = resultStr;
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(execJs);
}
});
}
}
}
总结一下吧:
一:method.invoke(null, webView, param, new Callback(webView, port), baseWebImp);的使用场景:
1:如果你是打一个打的框架容器,用来加载H5页面的很多个业务模块可以把最后面的baseWebImp这个接口去掉,method.invoke(null, webView, param, new Callback(webView, port)。因为只需要接收JS层指令,并根据params中给的字段进行处理。如果需要将结果返回给Js层只需要调用callBack的apply即可。其实需要几个参数可以根据业务规范来定。个人还是喜欢按统一的规则来,舒心。
2:如果只是在某个页面,或者某几个页面中嵌入一个网页,回调需要在activity或者Fragment中进行处理。则method.invoke(null, webView, param, new Callback(webView, port), baseWebImp);这样更灵活一点。
二:如果涉及到加载失败的默认图片这种情况,可以再WebViewChromClient 或者 WebViewClient 的错误方法中进行捕获并处理。
三:如果是第一种情况,作为容器还会涉及到H5模块的下载,解压,存储等,还有跳转,返回键 等这些琐碎的但是又是不可忽略的功能补充。
本人菜鸟一枚,如果有不好的,希望不吝指教,和谐交流,共同进步!