Android中JSBridge的原理与实现

论坛 期权论坛 期权     
Android技术杂货铺   2019-6-9 21:29   2107   0
点击上方“Android技术杂货铺”,选择“标星”
干货文章,第一时间送达!


封面/pixbay
本文来自 张志强
链接:http://zzqhost.com/2018/03/10/Android中JSBridge的原理与实现/
首先我们来了解一下什么是JSBridge和为什么要使用JSBridge?
在开发中,为了追求开发的效率以及移植的便利性,一些展示性强的页面我们会偏向于使用h5来完成,功能性强的页面我们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽可能的得到native的体验,我们native层需要暴露一些方法给js调用,比如,弹Toast提醒,弹Dialog,分享等等,有时候甚至把h5的网络请求放到native去完成。
JSBridge做得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了很多微信native层的方法,比如支付,定位等。
本文将对js和Native的通信原理和实现方法的一些探讨。
实现JSBridge关键点的原理剖析Android中的JSBridge是H5与Native通信的桥梁,其作用是实现H5与Native间的双向通信。要实现H5与Native的双向通信,解决如下四个问题即可:
1、Java如何调用JavaScript
2、JavaScript如何调用Java
3、方法参数以及回调如何处理
4、通信协议的制定
下面从以上问题依次开始讨论
Java如何调用JavaScript在WebView中,如果java要调用js的方法,是非常容易做到的,使用
  1. WebView.loadUrl(“javascript:function()”)
复制代码
即可,这样,就做到了
  1. JSBridge
复制代码
的native层调用h5层的单向通信
  1. WebView.loadUrl("javascript:function()");
复制代码
JavaScript如何调用Javajs调用Android的方法有以下四种:
1、WebView 的
  1. andJavascriptInterface
复制代码
2、
  1. WebViewClient.shouldOverrideUrlLoading()
复制代码
3、
  1. WebChromeClient.onConsoleMessage()
复制代码
4、
  1. WebChromeClient.onJsPrompt()
复制代码
  1. onJsAlert()
复制代码
  1. onJsConfirm()
复制代码
我们先对此四种方案进行一个详细的描述,最后选择一个方案即可。本文章中采用了第四种方案。
JavascriptInterfaceJavascriptInterface是Android官方提供的js和Native通信方案。其实现如下:
1、实现一个java类,供js调用
  1. public class MyJavascriptInterface {
  2.     @JavascriptInterface
  3.     public void showToast(String toast) {
  4.        Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
  5.     }
  6. }
复制代码
2、在webView中注册这个类
  1. webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
复制代码
3、在js中直接调用这个接口:
  1. function showToast(text){
  2.     window.javascriptInterface.showToast(text);
  3. }
复制代码
4、总结
大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了(即使用
  1. @JavascriptInterface
复制代码
代替
  1. addJavascriptInterface)
复制代码
,但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的
  1. addJavascriptInterface
复制代码
方法或者
  1. @JavascriptInterface
复制代码
注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。
WebViewClient.shouldOverrideUrlLoading()这个方法是拦截所有webView的跳转,页面可以构造一个特殊格式的Url跳转,
  1. shouldOverrideUrlLoading
复制代码
拦截Url后判断其格式,然后Native就能执行自身的逻辑了。
  1. public class CustomWebViewClient extends WebViewClient {
  2.     @Override
  3.     public boolean shouldOverrideUrlLoading(WebView view, String url) {
  4.       if (isJsBridgeUrl(url)) {
  5.         // JSbridge的处理逻辑
  6.         return true;
  7.       }
  8.       return super.shouldOverrideUrlLoading(view, url);
  9.     }
  10. }
复制代码
WebChromeClient.onConsoleMessage()在js中执行
  1. console.log()
复制代码
, 会进入Android的
  1. WebChromeClient.consoleMessage()
复制代码
回调。
  1. public class CustomWebChromeClient extends WebChromeClient {
  2.   @Override
  3.   public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
  4.     super.onConsoleMessage(consoleMessage);
  5.     String msg = consoleMessage.message();//Javascript输入的Log内容
  6.   }
  7. }
复制代码
WebChromeClient.onJsPrompt()1、在WebView有一个方法,叫
  1. setWebChromeClient
复制代码
,可以设置
  1. WebChromeClient
复制代码
对象,而这个对象中有三个方法,分别是
  1. onJsAlert
复制代码
,
  1. onJsConfirm
复制代码
,
  1. onJsPrompt
复制代码
,当js调用window对象的对应的方法,即
  1. window.alert
复制代码
  1. window.confirm
复制代码
  1. window.prompt
复制代码
  1. WebChromeClient
复制代码
对象中的三个方法对应的就会被触发,那这三个方法到底要使用哪个呢?
2、这三个方法的区别,可以详见
  1. w3c JavaScript
复制代码
消息框 。
3、一般来说,我们是不会使用
  1. onJsAlert
复制代码
的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而
  1. confirm
复制代码
  1. prompt
复制代码
的使用频率相对
  1. alert
复制代码
来说,则更低一点。
4、那么到底是选择
  1. confirm
复制代码
还是
  1. prompt
复制代码
呢,其实
  1. confirm
复制代码
的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用
  1. confirm
复制代码

5、而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,然后让你输入,输入完成后返回输入框中的内容。因此,占用
  1. prompt
复制代码
是再完美不过了。
  1. public class CustomWebChromeClient extends WebChromeClient {
  2.   @Override
  3.   public boolean onJsPrompt() {
  4.     super.onJsPrompt();
  5.     ...
  6.   }
  7. }
  8. myWebView.setWebChromClient(new CustomWebChromeClient());
复制代码
方法参数以及回调处理1、任何IPC通信都涉及到参数序列化的问题,同理,Java与JavaScript之间只能传递基础类型(包括基本类型和字符串),不包括其他对象或者函数。所以可以采用json格式来传递数据。
2、为了实现异步返回结果,所以JavaScript与Java相互调用不能直接获取返回值,只能通过回调的方式来获取返回结果。
通信协议的制定要进行正常的通信,通信协议的制定是必不可少的。我们回想一下熟悉的http请求url的组成部分。形如
  1. http://host:port/path?param=value
复制代码
, 我们参考http,制定
  1. JSBridge
复制代码
的组成部分
  1. jsbridge://className:callbackAddress/methodName?jsonObj
  2. // className: 表示java的类名
  3. // callbackAddress: js回调的标识
  4. // methodName: java中的方法名
  5. // jsonObj: 接口数据
复制代码
具体实践调用流程:
1、在js中,可以采用如下方法调用java方法
  1. var JSBridge = {
  2.   call: function(className, method, params, callback) {
  3.     var uri = 'jsbridge://' + className + ':' + callback + '/' + method + '?' + params;
  4.     window.prompt(uri, "");
  5.   }
  6. }
  7. // 下面会调用java中的 bridge.showToast方法
  8. JSBridge.call('bridge', 'showToast', {'msg':'Hello JSBridge'}, function(res) {
  9.           alert(JSON.stringify(res))
  10.       });
复制代码
2、在java中, 可以如下实现:
  1. // 进入prompt回调
  2.   public class JSBridgeWebChromeClient extends WebChromeClient {
  3.     @Override
  4.     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
  5.       result.confirm(JSBridge.callJava(view, message));
  6.       return true;
  7.     }
  8.   }
  9.   // 调用java逻辑
  10.   public class JSBridge {
  11.     ...
  12.     public static String callJava(WebView webView, String uriString) {
  13.         String methodName = "";
  14.         String className = "";
  15.         String param = "{}";
  16.         String port = "";
  17.         if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
  18.             Uri uri = Uri.parse(uriString);
  19.             className = uri.getHost();
  20.             param = uri.getQuery();
  21.             port = uri.getPort() + "";
  22.             String path = uri.getPath();
  23.             if (!TextUtils.isEmpty(path)) {
  24.                 methodName = path.replace("/", "");
  25.             }
  26.         }
  27.         // 基于上面的className、methodName和port path调用对应类的方法
  28.         if (exposedMethods.containsKey(className)) {
  29.             HashMap methodHashMap = exposedMethods.get(className);
  30.             if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
  31.                 Method method = methodHashMap.get(methodName);
  32.                 if (method != null) {
  33.                     try {
  34.                         method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
  35.                     } catch (Exception e) {
  36.                         e.printStackTrace();
  37.                     }
  38.                 }
  39.             }
  40.         }
  41.         return null;
  42.     }
  43. }
  44.   // 直接进入showToast函数的实现
  45.   public static void showToast(WebView webView, JSONObject param, final Callback callback) {
  46.     String message = param.optString("msg");
  47.     Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
  48.     if (null != callback) {
  49.         try {
  50.             JSONObject object = new JSONObject();
  51.             object.put("key", "value");
  52.             object.put("key1", "value1");
  53.             callback.apply(getJSONObject(0, "ok", object));
  54.         } catch (Exception e) {
  55.             e.printStackTrace();
  56.         }
  57.      }
  58.   }
  59.   // 上述程序的callback.apply方法实现如下: 即通过webView.loadUrl实现java调用js的方法
  60.   public class Callback {
  61.     private static Handler mHandler = new Handler(Looper.getMainLooper());
  62.     private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
  63.     private String mPort;
  64.     private WeakReference mWebViewRef;
  65.     public Callback(WebView view, String port) {
  66.         mWebViewRef = new WeakReference(view);
  67.         mPort = port;
  68.     }
  69.     public void apply(JSONObject jsonObject) {
  70.         final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
  71.         if (mWebViewRef != null && mWebViewRef.get() != null) {
  72.             mHandler.post(new Runnable() {
  73.                 @Override
  74.                 public void run() {
  75.                     mWebViewRef.get().loadUrl(execJs);
  76.                 }
  77.             });
  78.         }
  79.     }
  80. }
复制代码
安全性及其它JSBridge类管理暴露给前端方法,前端调用的方法应该在此类中注册才可使用。
  1. register
复制代码
的实现是从
  1. Map
复制代码
中查找
  1. key
复制代码
是否存在,不存在则反射取得对应class中的所有方法,具体方法是在
  1. BridgeImpl
复制代码
中定义的,方法包括三个参数分别为
  1. WebView
复制代码
  1. JSONObject
复制代码
  1. CallBack
复制代码
。如果满足条件,则将所有满足条件的方法
  1. put
复制代码
  1. map
复制代码
中。
[code]private static Map exposedMethods = new HashMap();
public static void register(String exposedName, Class
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP