Android中JSBridge的原理与实现

首先咱们来了解一下什么是JSBridge和为何要使用JSBridge?javascript

在开发中,为了追求开发的效率以及移植的便利性,一些展现性强的页面咱们会偏向于使用h5来完成,功能性强的页面咱们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽量的获得native的体验,咱们native层须要暴露一些方法给js调用,好比,弹Toast提醒,弹Dialog,分享等等,有时候甚至把h5的网络请求放到native去完成。前端

JSBridge作得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了不少微信native层的方法,好比支付,定位等。java

本文将对js和Native的通讯原理和实现方法的一些探讨。web

实现JSBridge关键点的原理剖析

Android中的JSBridge是H5与Native通讯的桥梁,其做用是实现H5与Native间的双向通讯。要实现H5与Native的双向通讯,解决以下四个问题便可:面试

一、Java如何调用JavaScriptjson

二、JavaScript如何调用Java安全

三、方法参数以及回调如何处理性能优化

四、通讯协议的制定微信

下面从以上问题依次开始讨论网络

Java如何调用JavaScript

在WebView中,若是java要调用js的方法,是很是容易作到的,使用WebView.loadUrl(“javascript:function()”)便可,这样,就作到了JSBridge的native层调用h5层的单向通讯

WebView.loadUrl("javascript:function()");

JavaScript如何调用Java

js调用Android的方法有如下四种:

一、WebView 的 andJavascriptInterface

二、WebViewClient.shouldOverrideUrlLoading()

三、WebChromeClient.onConsoleMessage()

四、WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()

咱们先对此四种方案进行一个详细的描述,最后选择一个方案便可。本文章中采用了第四种方案。

JavascriptInterface

JavascriptInterface是Android官方提供的js和Native通讯方案。其实现以下:

一、实现一个java类,供js调用

public class MyJavascriptInterface {
    @JavascriptInterface
    public void showToast(String toast) {             
       Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    }
}

二、在webView中注册这个类

webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");

三、在js中直接调用这个接口:

function showToast(text){
    window.javascriptInterface.showToast(text);
}

四、总结

大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了(即便用@JavascriptInterface代替addJavascriptInterface),可是因为兼容性和安全性问题,基本上咱们不会再利用Android系统为咱们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,因此咱们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。

WebViewClient.shouldOverrideUrlLoading()

这个方法是拦截全部webView的跳转,页面能够构造一个特殊格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,而后Native就能执行自身的逻辑了。

public class CustomWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      if (isJsBridgeUrl(url)) {
        // JSbridge的处理逻辑
        return true;
      }
      return super.shouldOverrideUrlLoading(view, url);
    }
}

WebChromeClient.onConsoleMessage()

在js中执行console.log(), 会进入Android的WebChromeClient.consoleMessage()回调。

public class CustomWebChromeClient extends WebChromeClient {
  @Override
  public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
    super.onConsoleMessage(consoleMessage);
    String msg = consoleMessage.message();//Javascript输入的Log内容
  }
}

WebChromeClient.onJsPrompt()

一、在WebView有一个方法,叫setWebChromeClient,能够设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,那这三个方法到底要使用哪一个呢?

二、这三个方法的区别,能够详见w3c JavaScript 消息框 。

三、通常来讲,咱们是不会使用onJsAlert的,为何呢?由于js中alert使用的频率仍是很是高的,一旦咱们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来讲,则更低一点。

四、那么究竟是选择confirm仍是prompt呢,其实confirm的使用频率也是不低的,好比你点一个连接下载一个文件,这时候若是须要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,相似这种场景仍是不少的,所以不能占用confirm。

五、而prompt则不同,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,因此咱们彻底可使用这个方法。该方法就是弹出一个输入框,而后让你输入,输入完成后返回输入框中的内容。所以,占用prompt是再完美不过了。

public class CustomWebChromeClient extends WebChromeClient {
  @Override
  public boolean onJsPrompt() {
    super.onJsPrompt();
    ...
  }
}

myWebView.setWebChromClient(new CustomWebChromeClient());

方法参数以及回调处理

一、任何IPC通讯都涉及到参数序列化的问题,同理,Java与JavaScript之间只能传递基础类型(包括基本类型和字符串),不包括其余对象或者函数。因此能够采用json格式来传递数据。

二、为了实现异步返回结果,因此JavaScript与Java相互调用不能直接获取返回值,只能经过回调的方式来获取返回结果。

通讯协议的制定

要进行正常的通讯,通讯协议的制定是必不可少的。咱们回想一下熟悉的http请求url的组成部分。形如http://host:port/path?param=value, 咱们参考http,制定JSBridge的组成部分

jsbridge://className:callbackAddress/methodName?jsonObj

// className: 表示java的类名
// callbackAddress: js回调的标识
// methodName: java中的方法名
// jsonObj: 接口数据

具体实践

调用流程:

一、在js中,能够采用以下方法调用java方法

var JSBridge = {
  call: function(className, method, params, callback) {
    var uri = 'jsbridge://' + className + ':' + callback + '/' + method + '?' + params;
    window.prompt(uri, "");
  }
}

// 下面会调用java中的 bridge.showToast方法
JSBridge.call('bridge', 'showToast', {'msg':'Hello JSBridge'}, function(res) {
          alert(JSON.stringify(res))
      });

二、在java中, 能够以下实现:

// 进入prompt回调
  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;
    }
  }

  // 调用java逻辑
  public class JSBridge {
    ...
    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("/", "");
            }
        }

        // 基于上面的className、methodName和port path调用对应类的方法
        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;
    }
}

  // 直接进入showToast函数的实现
  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();
        }
     }
  }

  // 上述程序的callback.apply方法实现以下: 即经过webView.loadUrl实现java调用js的方法
  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);
                }
            });

        }

    }
}

安全性及其它

JSBridge类管理暴露给前端方法,前端调用的方法应该在此类中注册才可以使用。register的实现是从Map中查找key是否存在,不存在则反射取得对应class中的全部方法,具体方法是在BridgeImpl中定义的,方法包括三个参数分别为WebView、JSONObject、CallBack。若是知足条件,则将全部知足条件的方法put到map中。

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();
            }
        }
    }

JSBridge类中的callJava方法就是将js传递过来的URL解析,根据将要调用的类名从刚刚创建的Map中找出,根据方法名调用具体的方法,并将解析出的三个参数传递进去。

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中回调方法的Java对应类。Java层处理好的返回结果是经过CallBack类来实现的。在这个回调类中传递的参数是JSONObject(返回结果)、WebView和port,port应与js传递过来的port相对应。

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);
                }
            });
        }
    }

在java层的JSBridge中注册方法,例如

JSBridge.register("bridge", BridgeImpl.class);
更多资料分享欢迎Android工程师朋友们加入安卓开发技术进阶互助:856328774免费提供安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于Android面试的题目汇总。
相关文章
相关标签/搜索