1. 程式人生 > >Android native code和Javascript通訊

Android native code和Javascript通訊

如果android和js互動的話,那就是要通過一個控制元件WebView。如果js要調android中方法的話,要通過JavascriptInterface,百度一下就能看到很多資料,這裡不詳述。
在API17之前的話,是不需要加@JavascriptInterface註釋的,但是在17版本後,需要在方法上加上@JavascriptInterface。
看個簡單的例子:
這是html的程式碼,點選按鈕會觸發JS方法:showAndroidToast,在這個方法中會呼叫android的方法。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN"> <head> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> <script type="text/javascript"> function showAndroidToast(toast) { window.Android.showToast(toast); }
</script> </head> <body> <input onclick="showAndroidToast('Hello world!')" type="button" value="Say hello"/> </body> </html>

看下android的程式碼:

public class MainActivity extends Activity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super
.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mWebView = (WebView) findViewById(R.id.webview); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.addJavascriptInterface(new JavaScriptInterface(this), "Android"); mWebView.loadUrl("file:///android_asset/test.html"); } }
public class JavaScriptInterface {
    private Context mContext;

    public JavaScriptInterface(Context c) {
        mContext = c;
    }

    /**
     * Show a toast from the web page
     */
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

執行起來的效果:
這裡寫圖片描述

js呼叫Java
呼叫格式為window.jsInterfaceName.methodName(parameterValues) 此例中我們使用的是control作為注入介面名稱。注意,java提供給js的方法不能混淆!!

function toastMessage(message) {
  window.control.toastMessage(message)
}

function sumToJava(number1, number2){
   window.control.onSumResult(number1 + number2)
}

Java呼叫JS
webView呼叫js的基本格式為
webView.loadUrl(“javascript:methodName(parameterValues)”)

大多數人都知道WebView存在一個漏洞,見WebView中介面隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了,即使用@JavascriptInterface代替addJavascriptInterface,但是由於相容性和安全性問題,基本上我們不會再利用Android系統為我們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,所以我們只能另闢蹊徑,去尋找既安全,又能實現相容Android各個版本的方案。

那這裡就要用到JSBridge。首先我們來了解一下為什麼要使用JSBridge,在開發中,為了追求開發的效率以及移植的便利性,一些展示性強的頁面我們會偏向於使用h5來完成,功能性強的頁面我們會偏向於使用native來完成,而一旦使用了h5,為了在h5中儘可能的得到native的體驗,我們native層需要暴露一些方法給js呼叫,比如,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網路請求放著native去完成,而JSBridge做得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等。
下面這篇文章是介紹如何利用window.prompt來建立兩端通訊的:
Android JSBridge的原理與實現
該文章裡面的程式碼。。。。真是慘不忍睹賭,所以我都貼出程式碼吧。
裡面定的協議是:jsbridge://className:callbackAddress/methodName?jsonObj

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);
        //此方法用來註冊哪些類可以供JS呼叫
        JSBridge.register("bridge", BridgeImpl.class);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebChromeClient(new JSBridgeWebChromeClient());
        //把myWebView載入html
        webView.loadUrl("file:///android_asset/one.html");
    }
}
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>JSBridge</title>
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>
    <script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>
    <script type="text/javascript"></script>
    <style></style>
</head>
<body>
<div><h3>JSBridge 測試</h3></div>
<ul class="list">
    <li>
        <div>
            <button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">
                測試showToast
            </button>
        </div>
    </li>
    <br/></ul>
<ul class="list">
    <li>
        <div>
            <button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">
                測試子執行緒回撥
            </button>
        </div>
    </li>
    <br/></ul>
</body>
</html>
(function (win) {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var JSBridge = win.JSBridge || (win.JSBridge = {});
    var JSBRIDGE_PROTOCOL = 'JSBridge';
    //然後有一個Inner類,裡面有我們的call和onFinish方法。
    var Inner = {
        callbacks: {},
        call: function (obj, method, params, callback) {
            console.log(obj+" "+method+" "+params+" "+callback);
            var port = Util.getPort();
            console.log(port);
            this.callbacks[port] = callback;
            var uri=Util.getUri(obj,method,params,port);
            console.log(uri);
            window.prompt(uri, "");
        },
        onFinish: function (port, jsonObj){
            var callback = this.callbacks[port];
            callback && callback(jsonObj);
            delete this.callbacks[port];
        },
    };
    //一個Util類,裡面有三個方法,getPort()用於隨機生成port,getParam()用於生成json字串
    //getUri()用於生成native需要的協議uri,裡面主要做字串拼接的工作
    var Util = {
        getPort: function () {
            return Math.floor(Math.random() * (1 << 30));
        },
        getUri:function(obj, method, params, port){
            params = this.getParam(params);
            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
            return uri;
        },
        getParam:function(obj){
            if (obj && typeof obj === 'object') {
                return JSON.stringify(obj);
            } else {
                return obj || '';
            }
        }
    };
    for (var key in Inner) {
        if (!hasOwnProperty.call(JSBridge, key)) {
            JSBridge[key] = Inner[key];
        }
    }
})(window);
public class JSBridge {
    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();
            }
        }
    }

    private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
        HashMap<String, Method> mMethodsMap = new HashMap<>();
        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] == Callback.class) {
                    mMethodsMap.put(name, method);
                }
            }
        }
        return mMethodsMap;
    }


    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;
    }
}
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;
    }
}
public interface IBridge {
}
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);
                }
            });
        }
    }
}
public class BridgeImpl implements IBridge {

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

    public static void testThread(WebView webView, JSONObject param, final Callback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    JSONObject object = new JSONObject();
                    object.put("key", "value");
                    callback.apply(getJSONObject(0, "ok", object));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private static JSONObject getJSONObject(int code, String msg, JSONObject result) {
        JSONObject object = new JSONObject();
        try {
            object.put("code", code);
            object.put("msg", msg);
            object.putOpt("result", result);
            return object;
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return null;
    }
}

程式碼都上了,效果的話就和那篇文章裡是一樣的。

當然,除了上面這種方法以外,還有其他的方法,通過攔截WebViewClient的shouldOverrideUrlLoading方法,主要是用來攔截url,看下程式碼:

public class MainActivity extends Activity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = (WebView) findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new MyWebViewClient());
        //如果不設定webchromeClient的話,會導致js的alert無法彈出
        webView.setWebChromeClient(new WebChromeClient());
        //把myWebView載入html
        webView.loadUrl("file:///android_asset/ios.html");
    }
}

ios.html:

<html>
<head>
    <title>WebView和Native通訊</title>
    <meta charset="UTF8">

    <script type="text/javascript">
        function nativeCallback(strJSON) {

            var description = "";
            console.log("-------------------------");

            for(var i in strJSON){
                var property=strJSON[i];
                description+=i+" = "+property+"\n";
            }
            alert(description);
            return "this";

        }
        function callNative () {
            var msgIframe = document.createElement('iframe');
            msgIframe.style.display = 'none';
            var msgBody = {"params" : {"id":"hello"},"callback" : ""};
            var strBody = JSON.stringify(msgBody);
            msgIframe.src = "epaysdk://paysuccess?msg=" + strBody;
            document.documentElement.appendChild(msgIframe);
            setTimeout(callback(), 0);
            function callback() {
                alert('dssdsdsdsd');
            }
        }

    </script>
</head>
<body>
<label id="log"></label>
<div style="margin-top:90px">
    <button onclick=callNative()>呼叫原生方法</button>
</div>
</body>
</html>
public class MyWebViewClient extends WebViewClient {

    private JSONObject obj;

    @Override
    public WebResourceResponse shouldInterceptRequest(final WebView view, WebResourceRequest request) {
        Log.d("tag", "shouldInterceptRequest--------url:" + request.getUrl().toString() + " , " + request.getUrl().getScheme());
        try {
            obj = new JSONObject();
            obj.put("code", "1");
            obj.put("msg", "success");
            view.post(new Runnable() {
                @Override
                public void run() {
                    Log.d("tag","evaluateJavascript");
                    view.evaluateJavascript("nativeCallback(" + obj.toString() + ")", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {
                            Log.d("tag","onReceiveValue:"+value);
                        }
                    });
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.shouldInterceptRequest(view, request);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.d("tag", "shouldOverrideUrlLoading-------url:" + url);
        return super.shouldOverrideUrlLoading(view, url);
    }
}