Android native和h5混合开发几种常见的hybrid通信方式

Posted 唐大麦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android native和h5混合开发几种常见的hybrid通信方式相关的知识,希望对你有一定的参考价值。

传统的JSInterface

首先先介绍一下最普通的一种通信方式,就是使用android原生的javascriptInterface来进行js和Java的通信。具体方式如下:
首先先看一段html代码

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" dir="ltr">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <script type="text/javascript">
        function showToast(toast) 
            javascript:control.showToast(toast);
        
        function log(msg)
            console.log(msg);
        
    </script>

</head>

<body>
<input type="button" value="toast"
       onClick="showToast('Hello world')" />
</body>
</html>

很简单,一个button,点击这个button就执行js脚本中的showToast方法。

而这个showToast方法做了什么呢?

function showToast(toast) 
    javascript:control.showToast(toast);

可以看到control.showToast,这个是什么我们等下再说,下面看我们java的代码。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="zjutkz.com.tranditionaljsdemo.MainActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </WebView>

</LinearLayout>
public class MainActivity extends AppCompatActivity 

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = (WebView)findViewById(R.id.webView);

        WebSettings webSettings = webView.getSettings();

        webSettings.setJavaScriptEnabled(true);

        webView.addJavascriptInterface(new JsInterface(), "control");

        webView.loadUrl("file:///android_asset/interact.html");
    

    public class JsInterface 

        @JavascriptInterface
        public void showToast(String toast) 
            Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
            log("show toast success");
        

        public void log(final String msg)
            webView.post(new Runnable() 
                @Override
                public void run() 
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                
            );
        
    

首先界面很简单,一个WebView。在对应的activity中做的事也就几件,首先打开js通道。

WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);

然后通过WebView的addJavascriptInterface方法去注入一个我们自己写的interface。

webView.addJavascriptInterface(new JsInterface(), "control");
public class JsInterface 

        @JavascriptInterface
        public void showToast(String toast) 
            Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
            log("show toast success");
        

        public void log(final String msg)
            webView.post(new Runnable() 
                @Override
                public void run() 
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                
            );
        
    

可以看到这个interface我们给它取名叫control。
最后loadUrl。

webView.loadUrl("file:///android_asset/interact.html");

好了,让我们再看看js脚本中的那个showToast()方法。

function showToast(toast) 
     javascript:control.showToast(toast);

这里的control就是我们的那个interface,调用了interface的showToast方法

@JavascriptInterface
public void showToast(String toast) 
    Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    log("show toast success");

可以看到先显示一个toast,然后调用log()方法,log()方法里调用了js脚本的log()方法。

function log(msg)
    console.log(msg);

js的log()方法做的事就是在控制台输出msg。
这样我们就完成了js和java的互调,是不是很简单。但是大家想过这样有什么问题吗?如果你使用的是AndroidStudio,在你的webSettings.setJavaScriptEnabled(true);这句函数中,AndroidStudio会给你一个warning。

这个提示的意思呢,就是如果你使用了这种方式去开启js通道,你就要小心XSS攻击了,具体的大家可以参考wooyun上的这篇文章。
虽然这个漏洞已经在Android 4.2上修复了,就是使用@JavascriptInterface这个注解。但是你得考虑兼容性啊,你不能保证,尤其在中国这样碎片化严重的地方,每个用户使用的都是4.2+的系统。所以基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现js和java的通信了。那怎么办呢?方法都是人想出来的嘛,下面让我们看解决方案。

JSBridge

JSBridge,顾名思义,就是和js沟通的桥梁。其实这个技术在Android中已经不算新了,相信有些同学也看到过不少实现方案,这里说一种我的想法吧。其实说是我的想法,实际是公司里的大牛实现的,我现在做的就是维护并且扩展,不过这里还是拿出来和大家分享一下。

思路

首先先说思路,有经验的同学可能都知道Android的WebView中有一个WebChromeClient类,这个类其实就是用来监听一些WebView中的事件的,我们发现其中有三个这样的方法。

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) 
    return super.onJsPrompt(view, url, message, defaultValue, result);


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

这三个方法其实就对应于js中的alert(警告框),comfirm(确认框)和prompt(提示框)方法,那这三个方法有什么用呢?
前面我们说了JSBridge的作用是提供一种js和java通信的框架,其实我们可以利用这三个方法去完成这样的事。比如我们可以在js脚本中调用alert方法,这样对应的就会走到WebChromeClient类的onJsAlert()方法中,我们就可以拿到其中的信息去解析,并且做java层的事情。那是不是这三个方法随便选一个就可以呢?其实不是的,因为我们知道,在js中,alert和confirm的使用概率还是很高的,特别是alert,所以我们最好不要使用这两个通道,以免出现不必要的问题。

好了,说到这里我们前期的准备工作也就做好了,其实就是通过重写WebView中WebChromeClient类的onJsPrompt()方法来进行js和java的通信。

有了实现方案,下面就是一些具体的细节了,大家有没有想过,怎么样才能让java层知道js脚本需要调用的哪一个方法呢?怎么把js脚本的参数传递进来呢?同步异步的方式又该怎么实现呢?下面提供一种我的思路。
首先大家都知道http是什么,其实我们的JSBridge也可以效仿一下http,定义一个自己的协议。比如规定sheme,path等等。下面来看一下一些的具体内容:

hybrid://JSBridge:1538351/method?“message”:”msg”

是不是和http协议有一点像,其实我们可以通过js脚本把这段协议文本传递到onPropmt()方法中并且进行解析。比如,sheme是hyrid://开头的就表示是一个hybrid方法,需要进行解析。后面的method表示方法名,message表示传递的参数等等。
有了这样一套协议,我们就可以去进行我们的通信了。
先看一下我们html和js的代码

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <script src="file:///android_asset/jsBridge.js" type="text/javascript"></script>
</head>

<body>
<div class="blog-header">
  <h3>JSBridge</h3>
</div>
<ul class="entry">

    <br/>
    <li>
        toast展示<br/>
        <button onclick="JsBridge.call('JSBridge','toast','message':'我是气泡','isShowLong':0,function(res));">toast</button>
    </li>

    <br/>
    <li>
        异步任务<br/>
        <button onclick="JsBridge.call('JSBridge','plus','data':1,function(res)console.log(JSON.stringify(res)));">plus</button>
    </li>

    <br/>
    <br/>
</ul>

</body>
</html>
(function (win, lib) 
    var doc = win.document;
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var JsBridge = win.JsBridge || (win.JsBridge = );
    var inc = 1;
    var LOCAL_PROTOCOL = 'hybrid';
    var CB_PROTOCOL = 'cb_hybrid';
    var CALLBACK_PREFIX = 'callback_';

    //核心功能,对外暴露
    var Core = 

        call: function (obj, method, params, callback, timeout) 
            var sid;

            if (typeof callback !== 'function') 
                callback = null;
            

            sid = Private.getSid();

            Private.registerCall(sid, callback);
            Private.callMethod(obj, method, params, sid);

        ,

        //native代码处理 成功/失败 后,调用该方法来通知js
        onComplete: function (sid, data) 
            Private.onComplete(sid, data);
        
    ;

    //私有功能集合
    var Private = 
        params: ,
        chunks: ,
        calls: ,

        getSid: function () 
            return Math.floor(Math.random() * (1 << 50)) + '' + inc++;
        ,

        buildParam: function (obj) 
            if (obj && typeof obj === 'object') 
                return JSON.stringify(obj);
             else 
                return obj || '';
            
        ,

        parseData: function (str) 
            var rst;
            if (str && typeof str === 'string') 
                try 
                    rst = JSON.parse(str);
                 catch (e) 
                    rst = 
                        status: 
                            code: 1,
                            msg: 'PARAM_PARSE_ERROR'
                        
                    ;
                
             else 
                rst = str || ;
            

            return rst;
        ,

        //根据sid注册calls的回调函数
        registerCall: function (sid, callback) 
            if (callback) 
                this.calls[CALLBACK_PREFIX + sid] = callback;
            
        ,

        //根据sid删除calls对应的回调函数,并返回call对象
        unregisterCall: function (sid) 
            var callbackId = CALLBACK_PREFIX + sid;
            var call = ;

            if (this.calls[callbackId]) 
                call.callback = this.calls[callbackId];
                delete this.calls[callbackId];
            

            return call;
        ,

        //生成URI,调用native功能
        callMethod: function (obj, method, params, sid) 
            // hybrid://objectName:sid/methodName?params
            params = Private.buildParam(params);

            var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;

            var value = CB_PROTOCOL + ':';
            window.prompt(uri, value);
        ,

        onComplete: function (sid, data) 
            var callObj = this.unregisterCall(sid);
            var callback = callObj.callback;

            data = this.parseData(data);

            callback && callback(data);
        
    ;

    for (var key in Core) 
        if (!hasOwnProperty.call(JsBridge, key)) 
            JsBridge[key] = Core[key];
        
    
)(window);

有前端经验的同学应该能很轻松的看懂这样的代码,对于看不懂的同学我来解释一下,首先看界面。

可以看到有两个按钮,对应着html的这段代码

<br/>
<li>
    toast展示<br/>
    <button onclick="JsBridge.call('JSBridge','toast','message':'我是气泡','isShowLong':0,function(res));">toast</button>
</li>

<br/>
<li>
    异步任务<br/>
    <button onclick="JsBridge.call('JSBridge','plus','data':1,function(res)console.log(JSON.stringify(res)));">toast</button>
</li>

<br/>

点击按钮会执行js脚本的这段代码

call: function (obj, method, params, callback, timeout) 
    var sid;
    if (typeof callback !== 'function') 
        callback = null;
    
    sid = Private.getSid();
    Private.registerCall(sid, callback);
    Private.callMethod(obj, method, params, sid);

它其实就是一个函数,名字叫call,括号里的是它的参数(obj, method, params, callback, timeout)。那这几个参数是怎么传递的呢?回过头看我们的html代码,点击第一个按钮,会执行这个语句

<button onclick="JsBridge.call('JSBridge','toast','message':'我是气泡','isShowLong':0,function(res));">toast</button>

其中括号(‘JSBridge’,’toast’,‘message’:’我是气泡’,’isShowLong’:0,function(res))里的第一个参数’JSBridge’对应着前面的obj,’toast’对应着method,以此类推。第二个按钮也是一样。
然后在call这个方法内,会执行Private类的registerCall和callMethod,我们来看callMehod()。

//生成URI,调用native功能
callMethod: function (obj, method, params, sid) 
    // hybrid://objectName:sid/methodName?params
    params = Private.buildParam(params);

    var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;

    var value = CB_PROTOCOL + ':';
    window.prompt(uri, value);

注释说的很清楚了,就是通过传递进来的参数生成uri,并且调用window.prompt()方法,这个方法大家应该很眼熟吧,没错,在调用这个方法之后,程序就会相应的走到java代码的onJsPrompt()方法中。而生成的uri则是我们上面说过的那个我们自己定义的协议格式。
好了,我们总结一下这两个前端的代码。其实很简单,以界面的第一个按钮toast为例,点击这个按钮,它会执行相应的js脚本代码,然后就会像我们前面所讲的那样,走到onJsPrompt()方法中,下面让我们看看对应的java代码。

public class InjectedChromeClient extends WebChromeClient 
    private final String TAG = "InjectedChromeClient";

    private JsCallJava mJsCallJava;

    public InjectedChromeClient() 
        mJsCallJava = new JsCallJava();
    

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) 
        result.confirm(mJsCallJava.call(view, message));
        return true;
    

这是对应的WebChromeClient类,可以看到在onJsPrompt()方法中我们只做了一件事,就是丢给JsCallJava类去解析,再看JsCallJava类之前,我们可以先看看onJsPrompt()这个方法到底传进来了什么。

可以看到,我们传给JsCallJava类的那个message,就像我们前面定义的协议一样。sheme是hybrid://,表示这是一个hybrid方法,host是JSBridge,方法名字是toast,传递的参数是以json格式传递的,具体内容如图。不知道大家有没有发现,这里我有一个东西没有讲,就是JSBridge:后面的那串数字,这串数字是干什么用的呢?大家应该知道,现在我们整个调用过程都是同步的,这意味着我们没有办法在里面做一些异步的操作,为了满足异步的需求,我们就需要定义这样的port,有了这串数字,我们在java层就可以做异步的操作,等操作完成以后回调给js脚本,js脚本就通过这串数字去得到对应的callback,有点像startActivity中的那个requestCode。大家没听懂也没关系,后面我会在代码中具体讲解。
好了,下面我们可以来看JsCallJava这个类的具体代码了。

public class JsCallJava 
    private final static String TAG = "JsCallJava";

    private static final String BRIDGE_NAME = "JSBridge";

    private static final String SCHEME="hybrid";

    private static final int RESULT_SUCCESS=200;
    private static final int RESULT_FAIL=500;


    private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();

    private JSBridge mWDJSBridge = JSBridge.getInstance();

    public JsCallJava() 
        try 
            ArrayMap<String, Class<? extends IInject>> externals = mWDJSBridge.getInjectPair();
            if (externals.size() > 0) 
                Iterator<String> iterator = externals.keySet().iterator();
                while (iterator.hasNext()) 
                    String key = iterator.next();
                    Class clazz = externals.get(key);
                    if (!mInjectNameMethods.containsKey(key)) 
                        mInjectNameMethods.put(key, getAllMethod(clazz));
                    
                
            
         catch (Exception e) 
            Log.e(TAG, "init js error:" + e.getMessage());
        
    

    private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception 
        ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
        //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
        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]==JsCallback.class)
                   mMethodsMap.put(name, method);
               
           
        
        return mMethodsMap;
    


    public String call(WebView webView, String jsonStr) 
        String methodName = "";
        String name = BRIDGE_NAME;
        String param = "";
        String result = "";
        String sid="";
        if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) 
            Uri uri = Uri.parse(jsonStr);
            name = uri.getHost();
            param = uri.getQuery();
            sid = getPort(jsonStr);
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) 
                methodName = path.replace("/", "");
            
        

        if (!TextUtils.isEmpty(jsonStr)) 
            try 
                ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

                Object[] values = new Object[3];
                values[0] = webView;
                values[1] = new JSONObject(param);
                values[2]=new JsCallback(webView,sid);
                Method currMethod = null;
                if (null != methodMap && !TextUtils.isEmpty(methodName)) 
                    currMethod = methodMap.get(methodName);
                
                // 方法匹配失败
                if (currMethod == null) 
                    result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");
                else
                    result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));
                
             catch (Exception e) 
                e.printStackTrace();
            
         else 
            result = getReturn(jsonStr, RESULT_FAIL, "call data empty");
        

        return result;
    



    private String getPort(String url) 
        if (!TextUtils.isEmpty(url)) 
            String[] arrays = url.split(":");
            if (null != arrays && arrays.length >= 3) 
                String portWithQuery = arrays[2];
                arrays = portWithQuery.split("/");
                if (null != arrays && arrays.length > 1) 
                    return arrays[0];
                
            
        
        return null;
    

    private String getReturn(String reqJson, int stateCode, Object result) 
        String insertRes;
        if (result == null) 
            insertRes = "null";
         else if (result instanceof String) 
            //result = ((String) result).replace("\\"", "\\\\\\"");
            insertRes = String.valueOf(result);
         else if (!(result instanceof Integer)
                && !(result instanceof Long)
                && !(result instanceof Boolean)
                && !(result instanceof Float)
                && !(result instanceof Double)
                && !(result instanceof JSONObject))     // 非数字或者非字符串的构造对象类型都要序列化后再拼接
            insertRes = result.toString();//mGson.toJson(result);
         else   //数字直接转化
            insertRes = String.valueOf(result);
        
        //String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);
        Log.d(TAG, " call json: " + reqJson + " result:" + insertRes);
        return insertRes;
    

有点长,不过其实逻辑很好理解。首先我们调用的是call这个方法。它里面做了什么呢

public String call(WebView webView, String jsonStr) 
    String methodName = "";
    String name = BRIDGE_NAME;
    String param = "";
    String result = "";
    String sid="";
    if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) 
        Uri uri = Uri.parse(jsonStr);
        name = uri.getHost();
        param = uri.getQuery();
        sid = getPort(jsonStr);
        String path = uri.getPath();
        if (!TextUtils.isEmpty(path)) 
            methodName = path.replace("/", "");
        
    

    if (!TextUtils.isEmpty(jsonStr)) 
        try 
            ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

            Object[] values = new Object[3];
            values[0] = webView;
            values[1] = new JSONObject(param);
            values[2]=new JsCallback(webView,sid);
            Method currMethod = null;
            if (null != methodMap && !TextUtils.isEmpty(methodName)) 
                currMethod = methodMap.get(methodName);
            
            // 方法匹配失败
            if (currMethod == null) 
                result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");
            else
                result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));
            
         catch (Exception e) 
            e.printStackTrace();
        
     else 
        result = getReturn(jsonStr, RESULT_FAIL, "call data empty");
    

    return result;

可以看到其实就是通过js脚本传递过来的参数得到了方法名字,sid(前面说的那串数字)等等内容。下面看这段代码

ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

通过name去得到一个map,这里的name是我们刚刚解析得到了,对应实际情况就是JSBridge,那这个mInjectNameMethods又是什么呢?

private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();

private JSBridge mJSBridge = JSBridge.getInstance();

public JsCallJava() 
    try 
        ArrayMap<String, Class<? extends IInject>> externals = mJSBridge.getInjectPair();
        if (externals.size() > 0) 
            Iterator<String> iterator = externals.keySet().iterator();
            while (iterator.hasNext()) 
                String key = iterator.next();
                Class clazz = externals.get(key);
                if (!mInjectNameMethods.containsKey(key)) 
                    mInjectNameMethods.put(key, getAllMethod(clazz));
                
            
        
     catch (Exception e) 
        Log.e(TAG, "init js error:" + e.getMessage());
    


private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception 
    ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
    //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
    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]==JsCallback.class)
               mMethodsMap.put(name, method);
           
       
    
    return mMethodsMap;

可以看到我们有一个JSBridge类,在JsCallJava的构造函数中,我们通过JSBridge这个类的getInjectPair()方法得到了一个String和class的映射关系,并且把class中符合标准的方法拿出来存放到mInjectNameMethods中,以便我们在call方法中调用。下面来看看JSBridge类。

public class JSBridge 
    public static final String BRIDGE_NAME = "JSBridge";

    private static JSBridge INSTANCE = new JSBridge();

    private boolean isEnable=true;

    private ArrayMap<String, Class<? extends IInject>> mClassMap = new ArrayMap<>();

    private JSBridge() 
        mClassMap.put(BRIDGE_NAME, JSLogical.class);
    

    public static JSBridge getInstance() 
        return INSTANCE;
    

    public boolean addInjectPair(String name, Class<? extends IInject> clazz) 
        if (!mClassMap.containsKey(name)) 
            mClassMap.put(name, clazz);
            return true;
        
        return false;
    

    public boolean removeInjectPair(String name,Class<? extends IInject> clazz) 
        if (TextUtils.equals(name,BRIDGE_NAME)) 
            return false;
        
        Class clazzValue=mClassMap.get(name);
        if(null!=clazzValue && (clazzValue == clazz))
            mClassMap.remove(name);
            return true;
        
        return false;

    


    public ArrayMap<String, Class<? extends IInject>> getInjectPair() 
        return mClassMap;
    

它的getInjectPair方法其实就是得到了mClassMap,这个map在JSBridge类初始化的时候就有一个默认的值了。

public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() 
    mClassMap.put(BRIDGE_NAME, JSLogical.class);


key是”JSBridge”,value是我们的JSLogincal类。
public class JSLogical implements IInject 

    /**
     * toast
     *
     * @param webView 浏览器
     * @param param   提示信息
     */
    public static void toast(WebView webView, JSONObject param, final JsCallback callback) 
        String message = param.optString("message");
        int isShowLong = param.optInt("isShowLong");
        Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
        if (null != callback) 
            try 
                JSONObject object = new JSONObject();
                object.put("result", true);
                invokeJSCallback(callback, object);
             catch (Exception e) 
                e.printStackTrace();
            
        
    

    /**
     * 加一
     *
     * @param webView
     * @param param
     * @param callback
     */
    public static void plus(WebView webView, final JSONObject param, final JsCallback callback) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                try 
                    Thread.sleep(2000);
                    int original = param.optInt("data");
                    original = original + 1;
                    if (null != callback) 
                        JSONObject object = new JSONObject();
                        object.put("after plussing", original);
                        invokeJSCallback(callback, object);
                    
                 catch (Exception e) 
                    e.printStackTrace();
                
            
        ).start();
    

    private static void invokeJSCallback(JsCallback callback, JSONObject objects) 
        invokeJSCallback(callback, true, null, objects);
    

    public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) 
        try 
            callback.apply(isSuccess, message, objects);
         catch (JsCallback.JsCallbackException e) 
            e.printStackTrace();
        
    

对这个类上面的两个方法有没有很眼熟?名字和js脚本中的那两个方法一样有木有。我们调用链最后就会走到相应的同名方法中!
上面就是js调js的整个过程了,其实吧,不应该放这么多的代码的,搞得像是源码分析一样,不过我觉得这样还是有一定好处的,至少跟着代码走一遍能加深印象嘛。
我们还是来捋一捋整个过程。
在js脚本中把对应的方法名,参数等写成一个符合协议的uri,并且通过window.prompt方法发送给java层。
在java层的onJsPrompt方法中接受到对应的message之后,通过JsCallJava类进行具体的解析。
在JsCallJava类中,我们解析得到对应的方法名,参数等信息,并且在map中查找出对应的类的方法。
这里多说一句,还记得我们定义的协议中的host是什么吗?

hybrid://JSBridge:875725/toast?“message”:”我是气泡”,”isShowLong”:0

是JSBridge,而我们在JsCallJava类中是通过这个host去查找对应的类的,我们可以看到在JSBridge类中

public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() 
    mClassMap.put(BRIDGE_NAME, JSLogical.class);

这意味着,如果你可以更换你的host,叫aaa都没关系,只要你在对应的map中的key也是aaa就可以了。
可能有的同学会说何必这么麻烦,直接在JsCallJava类中定义方法不就好了,这样还省的去写那么多的逻辑。可是大家有想过如果你把所有js脚本想要调用的方法都写在JsCallJava类中,这个类会有多难扩展和维护吗?而像我这样,如果你的js脚本处理的是登录相关逻辑,你可以写一个LoginLogical.class,如果是业务相关,你可以写一个BizLogical.class,这样不仅清晰,而且解耦。
当然,如果你仔细的看过代码,会发现其实在native层的那些同名函数其实是有规范的。
首先必须要是public static的,因为这样调用会更方便。
其次参数也有要求,有且仅有三个参数,WebView,JsonObject和一个Callback。WebView用来提供可能需要的context,另外java执行js方法也需要WebView对象。JsonObject是js脚本传递过来的参数。而Callback则是java用于回调js脚本的。
可能你会发现JSBridge里处处都是规范,协议需要规范,参数需要规范。这些其实都是合理的,因为规范所以安全。
在得到对应的方法之后,就去调用它,以我们的toast为

/**
 * toast
 *
 * @param webView 浏览器
 * @param param   提示信息
 */
public static void toast(WebView webView, JSONObject param, final JsCallback callback) 
    String message = param.optString("message");
    int isShowLong = param.optInt("isShowLong");
    Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
    if (null != callback) 
        try 
            JSONObject object = new JSONObject();
            object.put("result", true);
            invokeJSCallback(callback, object);
         catch (Exception e) 
            e.printStackTrace();
        
    

拿到对应的信息,直接makeToast就好了。
以上就是全部js调用java的过程,那我们java执行完逻辑以后,怎么回调js呢?这里我们以另外一个按钮的例子来说。

原生项目如何从零开始集成 React Native

H5混合开发中android终端和ios终端常见的兼容问题1

Hybrid混合开发知识点(一)

混合开发之DSBridge(同时支持Android和iOS)

androd H5混合开发 当无网络下,android怎么加载H5界面

H5混合开发