HyBrid APP简单入门之JS与Java互调以及JSBridge的实现

前言

HyBrid俗称混合开发。使用Android提供的组件——WebView,去加载放在本地或者服务器用h5编写的UI界面;js与java之间的互调,使得HyBrid APP的体验更加趋向原生APP。本章内容主要讲述在h5页面调用Java方法、在Android里调用h5里的js方法和jsbridge的简单实现。因此,拥有h5、css和js更容易上手。

三种APP开发方式比较

HyBrid APP简单入门之JS与Java互调以及JSBridge的实现_第1张图片
三种app比较.png

安全漏洞

在Android 4.2以下的WebView有个安全漏洞,外部网页通过得到Runtime对象,然后执行系统命令得到信息,原因出在addJavascriptInterface()方法。下面是漏洞的简单描述:
1、向WebView注册了一个叫“InterfaceName”的对象
2、js中可以访问到“InterfaceName”对象
3、js中通过“getClass”方法获取该对象的类型类
4、通过反射机制,得到该类的Runtime对象
5、调用静态方法执行系统命令
核心代码示例:


解决方案:

  • Android 4.2以上:@JavascriptInterface
  • Android 4.2以下:自定义js和Android交互方式

因此,在讲述js与java互调时基于Android 4.2以上。这个了解了解就好。

项目结构

HyBrid APP简单入门之JS与Java互调以及JSBridge的实现_第2张图片
js与java互调项目结构.png

JS调用Java方法

main目录,选择new->Folder->Assets Folder,完成assets目录创建。然后新建一个文件夹,命名为jscalljava。接着新建一个空白的html文件命名为index

新建一个空Activity,命名为JSAndJavaActivity,且设置为启动Activity,代码如下:

public class JSAndJavaActivity extends AppCompatActivity {

    private WebView webView;

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

        webView = findViewById(R.id.web_view);

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

        // 第二个参数可以简单理解为android表示AndroidAndrJsInterface的对象
        // 在js,通过它调用AndroidAndrJsInterface类下的方法
        // 名字可以自定义
        webView.addJavascriptInterface(new AndroidAndrJsInterface(), "android");
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("file:///android_asset/jscalljava/index.html");
    }

    class AndroidAndrJsInterface {

        // 该注解可以解决Android 4.2以上的安全漏洞,4.2以下没有这个注解
        @JavascriptInterface
        public void showToast() {
            Toast.makeText(JSAndJavaActivity.this, "我被js调用了", Toast.LENGTH_LONG).show();
        }

        @JavascriptInterface
        public void showToast(String info) {
            Toast.makeText(JSAndJavaActivity.this, "来自js的消息:" + info, Toast.LENGTH_LONG).show();
        }
    }
}

布局文件activity_jsand_java.xml的代码如下:




    
    

index.html添加如下代码:



    
        
        
        
    
    
        

Java调用JS方法

示例代码主要演示以下内容:

  • Android调用js的无参函数
  • Android调用js的有参函数
  • Android调用js的函数并获取返回值

在assets目录下新建文件夹,命名为javacalljs。然后新建一个空的html,命名为index

新建一个空Activity,命名为JavaAndJSActivity,且设置为启动Activity,代码如下:

public class JavaAndJSActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btnNoParamter;
    private Button btnYesParamter;
    private Button btnNoParamterAndReturn;

    // 加载网页或者说H5页面
    private WebView webView;


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

        btnNoParamter = findViewById(R.id.btn_no_parameter);
        btnYesParamter = findViewById(R.id.btn_yes_parmater);
        btnNoParamterAndReturn = findViewById(R.id.btn_no_parmater_return);

        btnNoParamter.setOnClickListener(this);
        btnYesParamter.setOnClickListener(this);
        btnNoParamterAndReturn.setOnClickListener(this);

        webView = new WebView(this);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true); // 设置支持js脚本语言
        settings.setUseWideViewPort(true); // 支持双击-前提是页面要支持才显示
        settings.setBuiltInZoomControls(true); // 支持缩放按钮-前提是页面要支持才显示

        webView.setWebViewClient(new WebViewClient()); // 不跳转到默认浏览器
        webView.setWebChromeClient(new WebChromeClient()); // 支持js弹窗

        webView.addJavascriptInterface(new GetJsResult(), "Result");

        // 加载本地文件:file:///android_asset/文件具体路径
        // 网络资源,如:http://www.baidu.com
        // 此处asset后面是没有s的
        webView.loadUrl("file:///android_asset/javacalljs/index.html"); // 加载网络资源(需要网络权限),也可以时assets目录下的资源

        // 加载h5写的页面,会替换当前原生页面,在这里不需要
//        setContentView(webView);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            // 格式:WebView.loadUrl("javascript:js方法")
            case R.id.btn_no_parameter:
                // 调用js无参函数
                webView.loadUrl("javascript:noParamter()");
                break;
            case R.id.btn_yes_parmater:
                String info = "Hello,I am come from java.";
                // 调用js有参参数
                // 传递字符串要加个单引号,数字可以不加;传递数组可以传递json格式的字符串
                webView.loadUrl("javascript:yesParamter('" + info + "')");
                break;
            case R.id.btn_no_parmater_return:
                webView.loadUrl("javascript:returnResult()");
                break;
            default:
                break;
        }
    }

    class GetJsResult {
        @JavascriptInterface
        public void getResult(String res) {
            Toast.makeText(JavaAndJSActivity.this, "js返回的结果:" + res, Toast.LENGTH_SHORT).show();
        }
    }

}

布局文件activity_java_and_js.xml的代码如下:





    

index.html添加如下代码:



    
        
        java调用js
        
    
    
    

小结:java和js互调的基本操作就讲完了,一些要注意地方已经在代码中注释了。

JSBridge的实现

JSBridge位置处于js和java之间,如下图所示:

HyBrid APP简单入门之JS与Java互调以及JSBridge的实现_第3张图片
jsbridge位置.png

在前面也曾提过,在Android 4.2以下的 addJavascriptInterface()方法存在漏洞,其解决方案是 JSBridge。简单来说就是自定义协议,暴漏对app没影响的信息,有影响的隐藏掉。当然,Android 4.2以上也可以使用这个方案。

java调用js依然采用WebView.loadUrl(),而js调用java就不能再采用addJavascriptInterface()了,需要换一个思路。WebChromeClient类,我们已经接触过了,它允许app显示js的弹窗。常见的弹窗有alert(警告框)、confirm(确认框)和prompt(提示框),前两个出现的频率相对后者更高,而prompt更加适合用来传递信息到Android,以下是它们在Android对应的源码实现:

/**
     * Tell the client to display a javascript alert dialog.  If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * dialog.  If the client returns {@code false}, it will continue execution.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param result A JsResult to confirm that the user hit enter.
     * @return boolean Whether the client will handle the alert dialog.
     */
    public boolean onJsAlert(WebView view, String url, String message,
            JsResult result) {
        return false;
    }

    /**
     * Tell the client to display a confirm dialog to the user. If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * confirm dialog and call the appropriate JsResult method. If the
     * client returns false, a default value of {@code false} will be returned to
     * javascript. The default behavior is to return {@code false}.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param result A JsResult used to send the user's response to
     *               javascript.
     * @return boolean Whether the client will handle the confirm dialog.
     */
    public boolean onJsConfirm(WebView view, String url, String message,
            JsResult result) {
        return false;
    }

    /**
     * Tell the client to display a prompt dialog to the user. If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * prompt dialog and call the appropriate JsPromptResult method. If the
     * client returns false, a default value of {@code false} will be returned to to
     * javascript. The default behavior is to return {@code false}.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param defaultValue The default value displayed in the prompt dialog.
     * @param result A JsPromptResult used to send the user's reponse to
     *               javascript.
     * @return boolean Whether the client will handle the prompt dialog.
     */
    public boolean onJsPrompt(WebView view, String url, String message,
            String defaultValue, JsPromptResult result) {
        return false;
    }

当app接受到要显示js的弹窗时,会根据弹窗的类型执行相应的方法,如prompt()对应onJsPrompt()。所以,我们可以重写onJsPrompt()方法,请求处理完后将其拦截,也就是返回true,那这个弹窗就不会显示了。换句话说,可以在这调用已经写好的Java方法。

接下就是要解决自定义协议了。我们可以模仿http的url格式,http://host:port/param=value,转换过来,JSBridge://className:callbackAddress/methodName?jsonObj。js向Android发送信息(url)必须按这个格式,而Java层只处理符合这个协议(格式)的请求,其它的一概不处理。下面对这个协议进行解释:

  • JSBridge:便于检验该url是否合格
  • className:要暴露出去的类的名字,但它不是js要调用的目标类,在本demo中是JSBridge
  • callbackAddress:js回调函数存在数组的位置,也就是下标
  • methodName:js要调用的方法,它的具体参数(比如个数)是无法得知的,在本demo中是showToast
  • jsonObj:真正传递给Android的信息,要求是json格式的字符串,至于具体是什么格式看需求了

最后,将协议转换成代码。

根据上述的项目结构图,在assets/jsbridge目录下新建空白的index.htmlJSBridge.js文件。新建CallBack类,负责将Java方法的执行结果通知js,其代码如下:

public class CallBack {

    private String mPort;

    private WebView mWebView;

    public CallBack(WebView webView, String mPort) {
        this.mPort = mPort;
        this.mWebView = webView;
    }

    /**
     * 通知js
     * @param jsonObject Java层处理完后返回给js层的信息
     */
    public void apply(JSONObject jsonObject) {
        if (mWebView != null) {
            mWebView.loadUrl("javascript:onAndroidFinished('" + mPort + "', " + String.valueOf(jsonObject) + ")");
        }
        Log.d("TAG", "CallBack:apply");
    }
}

新建Methods类,用于封装供js调用的方法且有以下约定:

  • 方法必须是publicstatic
  • 参数必须有3
  • 第一个参数必须是WebView,第二个参数必须是JSONObject,第三个参数必须是CallBack

只有满足以上三个条件的方法才能被js调用,才会暴露出去。其代码如下:

public class Methods {

    public static void showToast(WebView view, JSONObject param, CallBack callBack) {
        // 解析得到key=msg的值
        String message = param.optString("msg");

        Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();

        if (callBack != null) {
            try {
                JSONObject result = new JSONObject();
                result.put("key", "value");
                result.put("key1", "value1");
                callBack.apply(result);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

}

新建JSBridge类,负责管理暴露给js的类和方法,以及根据js传入的url内容找到对应的java类,并执行指定的Java方法,代码如下:

public class JSBridge {

    // 存储需要暴露给js的方法
    private static Map> exposedMethods = new HashMap<>();

    /**
     * 注册要暴露的类
     * @param exposeName JSBridge
     * @param classz 要暴露的类
     */
    public static void register(String exposeName, Class classz) {
        // 将符合要求的classz类中的所有方法添加到exposedMethods中
        if (!exposedMethods.containsKey(exposeName)) {
            exposedMethods.put(exposeName, getAllMethod(classz));
        }
        Log.d("TAG", "JSBridge:register");
    }

    private static HashMap getAllMethod(Class injectedCls) {
        HashMap methodHashMap = new HashMap<>();

        // 获取该类的所有方法
        Method[] methods = injectedCls.getDeclaredMethods();

        for (Method method : methods) {
            // 剔除不符合要求的方法
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || method.getName() == null) {
                continue;
            }

            // 方法的参数
            Class[] paramters = method.getParameterTypes();
            // 进一步寻找符合要求的方法
            if (paramters != null && paramters.length == 3) {
                if (paramters[0] == WebView.class && paramters[1] == JSONObject.class && paramters[2] == CallBack.class) {
                    methodHashMap.put(method.getName(), method);
                }
            }
        }

        return methodHashMap;
    }

    /**
     * 调用相应的java方法去处理js的请求
     * @param webView WebView
     * @param urlString 根据协议,js层给java传递的信息
     * @return null
     */
    public static String callJava(WebView webView, String urlString) {
        String className = "";
        String methodName = "";
        String param = "";
        String port = "";

        // 验证该urlString是否符合协议的基本要求
        if (!urlString.equals("") && urlString != null && urlString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(urlString);
            className = uri.getHost();   // 要调用的类
            param = uri.getQuery();      // js层给Java层传递的信息(json格式)
            port = uri.getPort() + "";   // js层回调函数的地址
            methodName = uri.getPath().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 {
                            // 在这里真正处理js的请求,CallBack用于告诉js层我的活干完了,该你了
                            method.invoke(null, webView, new JSONObject(param), new CallBack(webView, port));
                            Log.d("TAG", "JSBridge:callJava");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

        return null;
    }

}

新建JSBridgeChromeClient类且继承WebChromeClient,在此类处理js的请求,代码如下:

public class JSBridgeChromeClient extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 在简单讲述是否调用result.confirm()的区别
        // 如果调用,js的回调函数才会被调用,参数的值是返回给js的
        // 如果没调用,js即使有回调函数也不会执行
        // 可以使用console.log()的方式来调式js,项目运行起来后可在run窗口查看
        result.confirm(JSBridge.callJava(view, message));
//        JSBridge.callJava(view, message);
        Log.d("TAG", "JSBridgeChromeClient");

        return true;
    }
}

新建JSBridgeActivity且设置为启动Activity,代码如下:

public class JSBridgeActivity extends AppCompatActivity {

    private WebView webView;

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

        webView = findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.setWebChromeClient(new JSBridgeChromeClient());
        webView.loadUrl("file:///android_asset/jsbridge/index.html");

        JSBridge.register("JSBridge", Methods.class);
        Log.d("TAG", "JSBridgeActivity");
    }
}

布局文件activity_jsbridge.xml的代码如下:




    
    

JSBridge.js的代码如下:

var callbacks = new Array();

/**
 * js层调用Android层方法
 * @param {Object} obj Android层的类
 * @param {Object} method 该obj中的那个方法
 * @param {Object} params 使用json的数据格式给Android传递信息
 * @param {Object} callback js层的回调方法,当Android层处理好了js层要如何处理
 */
function jsCallAndroid(obj, method, params, callback) {
    // 保存callback回调函数
    var port = callbacks.length;
    callbacks[port] = callback;
    
    // 组合出符合规则的url,并传递给Java层
    var url = 'JSBridge://' + obj + ':' + port + '/' + method + '?' + JSON.stringify(params);
    
    window.prompt(url);
}

/**
 * 当js调用完Android层时执行
 * @param {Object} port 回调函数的地址,也就是在数组中的位置
 * @param {Object} jsonObj 从Android层传过来的参数
 */
function onAndroidFinished(port, jsonObj) {
    // 从callbacks取出对应的回调函数
    var callback = callbacks[port];
    
    callback(jsonObj);
    
    // 从callbacks中删除callback
    delete callbacks[port];
}

index.html的代码如下:



    
        
        
        
    
    
        
    

对函数的调用过程进行概括:点击按钮,触发jsCallAndroid()方法,通过调用window.prompt(url)向Android发送请求(信息)。然后在JSBridgeChromeClient.onJsPrompt()方法对请求进行拦截处理,就是实现了js调用java,JSBridge.callJava(view, message)。执行到callJava()方法内部,会调用Methods.showToast()方法,紧接着会调用CallBack.apply(result)方法,最后是调用js的onAndroidFinished()方法。如果JsPromptResult.confirm()被调用了,js的回调函数会被同步调用。

总结

本章的内容就将完了。

HyBridDemo

你可能感兴趣的:(HyBrid APP简单入门之JS与Java互调以及JSBridge的实现)