美文网首页
HyBrid APP简单入门之JS与Java互调以及JSBrid

HyBrid APP简单入门之JS与Java互调以及JSBrid

作者: 大荒里种菜 | 来源:发表于2019-05-25 10:01 被阅读0次

    前言

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

    三种APP开发方式比较

    三种app比较.png

    安全漏洞

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

    <script type="text/javascript">
                function execute(cmd) {
                    return demo.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec(cmd);
                }
                
                execute(["ls", "/mnt/sdcard"]);
            </script>
    

    解决方案:

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

    因此,在讲述js与java互调时基于Android 4.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的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".JSAndJavaActivity">
    
        <WebView
            android:id="@+id/web_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        
    </android.support.constraint.ConstraintLayout>
    

    index.html添加如下代码:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title></title>
            <style type="text/css">
                div {
                    margin: 0 auto;
                    width: 200px;
                }
                button {
                    width: 150px;
                    font-weight: bolder;
                    font-family: "微软雅黑";
                }
            </style>
        </head>
        <body>
            <div>
                <p>
                    <button onclick="android.showToast()">调用Java无参方法</button>
                </p>
                <p>
                    <button onclick="android.showToast('I am come from js.')">调用Java有参方法</button>
                </p>
            </div>
        </body>
    </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的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
    
        <Button
            android:id="@+id/btn_no_parameter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="调用无参方法"
            app:layout_constraintBottom_toTopOf="@+id/btn_yes_parmater"
            app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
            app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater" />
    
        <Button
            android:id="@+id/btn_yes_parmater"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:text="调用有参方法"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/btn_no_parmater_return"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="调用无参且有返回值"
            app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
            app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater"
            app:layout_constraintTop_toBottomOf="@+id/btn_yes_parmater" />
    </android.support.constraint.ConstraintLayout>
    

    index.html添加如下代码:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title>java调用js</title>
            <script type="text/javascript">
                function noParamter() {
                    alert("我是js无参函数");
                }
                
                function yesParamter(info) {
                    alert("来自java的信息:" + info);
                }
                
                function returnResult() {
                    var a = "我处理完了。"
                    // 将结果返回给Android
                    window.Result.getResult(a);
                }
            </script>
        </head>
        <body>
        </body>
    </html>
    

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

    JSBridge的实现

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

    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<String, HashMap<String, Method>> 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<String, Method> getAllMethod(Class injectedCls) {
            HashMap<String, Method> 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<String, Method> 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的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".jsbridge.JSBridgeActivity">
    
        <WebView
            android:id="@+id/webView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        
    </android.support.constraint.ConstraintLayout>
    

    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的代码如下:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title></title>
            <script type="text/javascript" src="JSBridge.js" ></script>
        </head>
        <body>
            <button onclick="jsCallAndroid('JSBridge', 'showToast',
                {'msg':'hello I come from js.'},function(res) {alert(JSON.stringify(res))})">Js调用Android</button>
        </body>
    </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互调以及JSBrid

          本文链接:https://www.haomeiwen.com/subject/fkkvzqtx.html