美文网首页WebView
Android Hybrid App通信

Android Hybrid App通信

作者: Drc15H | 来源:发表于2016-09-20 12:25 被阅读444次

    在Android开发中,能够实现Web调用Native代码的方法主要有以下方法:
    1.Schema:WebView拦截页面跳转
    2.JavascriptInterface
    3.WebChromeClient.onJsPrompt()
    而native代码通信js只能使用WebView.loadUrl(“javascript:function()”)
    若需要Native与js进行双向通信,则可使用JSBridge,同时,还有一些开源框架如:safe-java-js-webview-bridge等
    以下就详细介绍这些通信方法:

    1.Schema:WebView拦截页面跳转

    这种方法实现相对简单,例如,在HTML界面中添加如下代码:
    <a href="myapp://tonative/param?id=123">gotoActivity</a>
    然后要在要跳转到的Activity中进行声明:
    <activity android:name=".Activity"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="tonative" android:scheme="myapp" /> </intent-filter> </activity>
    我们可以通过声明不同的host实现打开不同的Activity,在打开的Activity里可以通过如下代码获取html页面传过来的参数:

        Intent intent = getIntent();
        String action = intent.getAction();
        if(Intent.ACTION_VIEW.equals(action)){
            Uri uri = intent.getData();
            if(uri != null){
                String id = uri.getQueryParameter("id");
                Toast.makeText(this,id,Toast.LENGTH_LONG).show();
            }
        }
    

    但这样其实有个问题,我们一般会重写WebViewClient的shouldOverrideUrlLoading方法来实现在本页内的跳转都是由本Webview打开,而不是跳转到系统浏览器处理。这样设置后,‘href=”myapp://tonative/param?id=123”’这样的请求也被拦截到了本Webview里,从而失效,因此,我们需要做一个判断

    wv.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                String scheme = Uri.parse(url).getScheme();//还需要判断host
                if (TextUtils.equals("myapp", scheme)) {
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    startActivity(intent);
                    return true;
                }
                return false;
            }
    

    return true,表明这次请求交给系统来处理。

    2.JavascriptInterface

    首先Java代码要实现一个类,它的作用是提供给Javascript调用。

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

    然后把这个类添加到WebView的JavascriptInterface中。
    webView.addJavascriptInterface(new JavascriptInterface(), "javascriptInterface");
    在Javascript代码中就能直接通过“javascriptInterface”直接调用了该Native的类的方法。
    function showToast(toast) { javascript:javascriptInterface.showToast(toast); }

    3.WebChromeClient.onJsPrompt()

    其实除了WebChromeClient.onJsPrompt(),还有WebChromeClient.onJsAlert()和WebChromeClient.onJsConfirm()。顾名思义,这三个Js给Native代码的回调接口的作用分别是展示提示信息,展示警告信息和展示确认信息。鉴于,alert和confirm在Js的使用率很高,所以JSBridge的解决方案中都倾向于选用onJsPrompt()。
    Js中调用window.prompt(message, value)
    WebChromeClient.onJsPrompt()就会受到回调。onJsPrompt()方法的message参数的值正是Js的方法window.prompt()的message的值。

    public class CustomWebChromeClient extends WebChromeClient {
      @Override
      public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult 
    result) {
        // 处理JS 的调用逻辑
        result.confirm();
        return true;
      }
    }
    

    4.JSBridge

    JSBridge可以实现JavaScript与Native层间的通信,native层调用js方法可使用WebView.loadUrl(“javascript:function()”),js调用native层可使用prompt来实现。JSBridge实现的原理和过程如下图所示:

    JSBridge.PNG

    4.1.定义js和native层的通信协议

    首先需要定义js和native层的通信协议:
    jsbridge://className:callbackAddress/methodName?jsonObj
    className为js调用native的相应的类名,methodName为调用的该类的一个方法,jsonObj为该方法所传入的参数。
    我们在js中调用native方法的时候,在js中注册一个callback,然后将该callback在指定的位置上缓存起来,然后native层执行完毕对应方法后通过WebView.loadUrl调用js中的方法,回调对应的callback。协议中的callbackAddress即为即为js中对应的回调函数。

    4.2.JS调用native层

    js中要完成通信功能,要有两个方法call、onFinish,其中方法call首先要能够生成native需要的协议uri,还要将callback对象存储在callbacks数组中,存储的位置即为port,并调用window.prompt(uri, “”)将uri传递到native层。另一个方法onFinish用于接受native回传的port值和执行结果,根据port值从callbacks中得到原始的callback函数,执行callback函数,之后从callbacks中删除。最后将这两个函数暴露给外部的JSBrige对象,通过一个for循环一一赋值即可。实现代码如下:

    (function (win) {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var JSBridge = win.JSBridge || (win.JSBridge = {});
    var JSBRIDGE_PROTOCOL = 'JSBridge';
    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];
        },
    };
    var Util = {
        getPort: function () {      //随机生成port
            return Math.floor(Math.random() * (1 << 30));
        },
        getUri:function(obj, method, params, port){    //生成native需要的协议uri
            params = this.getParam(params);
            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
            return uri;
        },
        getParam:function(obj){    //生成json字符串
            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); 
    

    4.3.native层的实现

    首先要将js传来的uri获取到,可编写一个WebChromeClient子类:

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

    然后要将该对象设置给WebView:
    mWebView.setWebChromeClient(new JSBridgeWebChromeClient());

    之后就是JSBridgeWebChromeClient中调用的JSBridge类的实现,首先JSBridge类要有一个统一管理暴露给js调用的类和方法,并且能实时添加:
    JSBridge.register("jsName",javaClass.class)
    这个javaClass就是满足某种规范的类,我们规定这个类需要实现一个空接口,主要作用就混淆的时候不会发生错误,还有一个作用就是约束JSBridge.register方法第二个参数必须是该接口的实现类。那么我们定义这个接口
    public interface IBridge{}
    该类中有满足规范的方法,该方法不具有返回值,因为返回值我们在回调中返回,因此参数列表应含有一个callback,除了callback,还要有js传来的方法调用所需的参数,是一个json对象;方法的执行结果需要通过callback传递回去,而java执行js方法需要一个WebView对象,因此,暴露给js的方法应满足:
    public static void methodName(WebView webview,JSONObject jsonObj,Callback callback){}

    register方法的实现原理为,从一个Map中查找key是不是存在,不存在则反射拿到对应的Class中的所有方法,将方法是public static void 类型的,并且参数是三个参数,分别是Webview,JSONObject,Callback类型的,如果满足条件,则将所有满足条件的方法put进去。代码如下:

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

    JSBridge类中的callJava方法,功能是将js传来的uri进行解析,然后根据调用的类名别名从刚刚的map中查找是不是存在,存在的话拿到该类所有方法的methodMap,然后根据方法名从methodMap拿到方法,反射调用,并将参数传进去,参数就是前文说的满足条件的三个参数,即WebView,JSONObject,Callback。

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

    可以看到该方法中使用了 new Callback(webView, port)进行新建对象,该对象就是用来回调js中回调方法的java对应的类。这个类你需要将js传来的port传进来之外,还需要将WebView的引用传进来,因为要使用到WebView的loadUrl方法,为了防止内存泄露,这里使用弱引用。如果你需要回调js的callback,在对应的方法里调用一下callback.apply()方法将返回数据传入即可。

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

    通过Callback中的mWebViewRef.get().loadUrl("javascript:JSBridge.onFinish(port,jsonObj);"),就可将结果返回给js。

    5.safe-java-js-webview-bridge

    这个开源项目用到的原理也是JSBridge,都是js调用prompt函数,传输一些参数,onJsPrompt方法拦截到prompt动作,然后解析数据,最后调用相应的Native方法。

    HostJsScope类中定义了所有可以被js调用的方法,这些方法都必须是静态方法,并且所有的方法第一个参数必须是WebView。

    这个库中一个最关键的叫做JsCallJava,这个实现的就是js来调用Java方法的功能,这个类只用于InjectedWebChromeClient类

    public class InjectedChromeClient extends WebChromeClient {
    
      private JsCallJava mJsCallJava;
      private boolean mIsInjectedJS;
    
      public InjectedChromeClient(String injectedName, Class injectedCls) {
        this(new JsCallJava(injectedName, injectedCls));
      }
    
      public InjectedChromeClient(JsCallJava jsCallJava) {
        mJsCallJava = jsCallJava;
      }
    
      // 处理Alert事件
      @Override
      public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
        result.confirm();
        return true;
      }
    
      @Override
      public void onProgressChanged(WebView view, int newProgress) {
        //为什么要在这里注入JS
        //1 OnPageStarted中注入有可能全局注入不成功,导致页面脚本上所有接口任何时候都不可用
        //2 OnPageFinished中注入,虽然最后都会全局注入成功,但是完成时间有可能太晚,当页面在初始化调用接口函数时会等待时间过长
        //3 在进度变化时注入,刚好可以在上面两个问题中得到一个折中处理
        //为什么是进度大于25%才进行注入,因为从测试看来只有进度大于这个数字页面才真正得到框架刷新加载,保证100%注入成功
        if (newProgress <= 25) {
            mIsInjectedJS = false;
        } else if (!mIsInjectedJS) {
            view.loadUrl(mJsCallJava.getPreloadInterfaceJS());
            mIsInjectedJS = true;
            StopWatch.log(" inject js interface completely on progress " + newProgress);
        }
        super.onProgressChanged(view, newProgress);
      }
    
      @Override
      public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
          result.confirm(mJsCallJava.call(view, message));
          StopWatch.log("onJsPrompt: " + view.toString() +", " + url +", " + message +", " + defaultValue + ", " + result) ;
          return true;
      }
    } 
    

    这个InjectedWebChromeClient是设给WebView的,在onProgressChange
    方法中,向WebView注入了一段js代码,这段js代码如下:

    javascript: (function(b) {
    console.log("HostApp initialization begin");
    var a = {
        queue: [],
        callback: function() {
            var d = Array.prototype.slice.call(arguments, 0);
            var c = d.shift();
            var e = d.shift();
            this.queue[c].apply(this, d);
            if (!e) {
                delete this.queue[c]
            }
        }
    };
    a.alert = a.alert = a.alert = a.delayJsCallBack = a.getIMSI = a.getOsSdk = a.goBack = a.overloadMethod = a.overloadMethod 
        = a.passJson2Java = a.passLongType = a.retBackPassJson = a.retJavaObject = a.testLossTime = a.toast = a.toast = function() {
        var f = Array.prototype.slice.call(arguments, 0);
        if (f.length < 1) {
            throw "HostApp call error, message:miss method name"
        }
        var e = [];
        for (var h = 1; h < f.length; h++) {
            var c = f[h];
            var j = typeof c;
            e[e.length] = j;
            if (j == "function") {
                var d = a.queue.length;
                a.queue[d] = c;
                f[h] = d
            }
        }
        var g = JSON.parse(prompt(JSON.stringify({
            method: f.shift(),
            types: e,
            args: f
        })));
        if (g.code != 200) {
            throw "HostApp call error, code:" + g.code + ", message:" + g.result
        }
        return g.result
    };
    //有时候,我们希望在该方法执行前插入一些其他的行为用来检查当前状态或是监测
    //代码行为,这就要用到拦截(Interception)或者叫注入(Injection)技术了
    /**
     * Object.getOwnPropertyName 返回一个数组,内容是指定对象的所有属性
     *
     * 其后遍历这个数组,分别做以下处理:
     * 1. 备份原始属性;
     * 2. 检查属性是否为 function(即方法);
     * 3. 若是重新定义该方法,做你需要做的事情,之后 apply 原来的方法体。
     */
    Object.getOwnPropertyNames(a).forEach(function(d) {
        var c = a[d];
        if (typeof c === "function" && d !== "callback") {
            a[d] = function() {
                return c.apply(a, [d].concat(Array.prototype.slice.call(arguments, 0)))
            }
        }
    });
    b.HostApp = a;
    console.log("HostApp initialization end")
    })(window);
    

    这段代码是在JsCallJava类的构造函数方法中生成的,这个构造方法做的事情就是解析HostJsScope类中的方法,把每一个方法的签名都保持到private Map<String, Method> mMethodsMap中。

     public JsCallJava (String injectedName, Class injectedCls) {
        try {
            if (TextUtils.isEmpty(injectedName)) {
                throw new Exception("injected name can not be null");
            }
            mInjectedName = injectedName;
            mMethodsMap = new HashMap<String, Method>();
            //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
            Method[] methods = injectedCls.getDeclaredMethods();
            StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\"");
            sb.append(mInjectedName);
            sb.append(" initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");
            for (Method method : methods) {
                String sign;
                if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) {
                    continue;
                }
                mMethodsMap.put(sign, method);
                sb.append(String.format("a.%s=", method.getName()));
            }
    
            sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
            sb.append(mInjectedName);
            sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f})));if(g.code!=200){throw\"");
            sb.append(mInjectedName);
            sb.append(" call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&&d!==\"callback\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b.");
            sb.append(mInjectedName);
            sb.append("=a;console.log(\"");
            sb.append(mInjectedName);
            sb.append(" initialization end\")})(window);");
            mPreloadInterfaceJS = sb.toString();
        } catch(Exception e){
            Log.e(TAG, "init js error:" + e.getMessage());
        }
    }
    

    与前面介绍的JSBridge不同的是,js调用native代码时并不是拼接成一个uri形式,而是将要调用的native方法的名字、参数类型、方法参数等封装成一端JSON字符串,通过js的prompt方法传到onJsPrompt方法中,JsCallJava调用call(WebView view, String msg)解析json字符串,其中还会验证json中的方法参数类型和HostJsScope中同名方法参数类型是否一致等等。

    public String call(WebView webView, String jsonStr) {
        if (!TextUtils.isEmpty(jsonStr)) {
            try {
                JSONObject callJson = new JSONObject(jsonStr);
                String methodName = callJson.getString("method");
                JSONArray argsTypes = callJson.getJSONArray("types");
                JSONArray argsVals = callJson.getJSONArray("args");
                String sign = methodName;
                int len = argsTypes.length();
                Object[] values = new Object[len + 1];
                int numIndex = 0;
                String currType;
    
                values[0] = webView;
    
                for (int k = 0; k < len; k++) {
                    currType = argsTypes.optString(k);
                    if ("string".equals(currType)) {
                        sign += "_S";
                        values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k);
                    } else if ("number".equals(currType)) {
                        sign += "_N";
                        numIndex = numIndex * 10 + k + 1;
                    } else if ("boolean".equals(currType)) {
                        sign += "_B";
                        values[k + 1] = argsVals.getBoolean(k);
                    } else if ("object".equals(currType)) {
                        sign += "_O";
                        values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);
                    } else if ("function".equals(currType)) {
                        sign += "_F";
                        values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));
                    } else {
                        sign += "_P";
                    }
                }
    
                Method currMethod = mMethodsMap.get(sign);
    
                // 方法匹配失败
                if (currMethod == null) {
                    return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters");
                }
                // 数字类型细分匹配
                if (numIndex > 0) {
                    Class[] methodTypes = currMethod.getParameterTypes();
                    int currIndex;
                    Class currCls;
                    while (numIndex > 0) {
                        currIndex = numIndex - numIndex / 10 * 10;
                        currCls = methodTypes[currIndex];
                        if (currCls == int.class) {
                            values[currIndex] = argsVals.getInt(currIndex - 1);
                        } else if (currCls == long.class) {
                            //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number
                            values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));
                        } else {
                            values[currIndex] = argsVals.getDouble(currIndex - 1);
                        }
                        numIndex /= 10;
                    }
                }
    
                return getReturn(jsonStr, 200, currMethod.invoke(null, values));
            } catch (Exception e) {
                //优先返回详细的错误信息
                if (e.getCause() != null) {
                    return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage());
                }
                return getReturn(jsonStr, 500, "method execute error:" + e.getMessage());
            }
        } else {
            return getReturn(jsonStr, 500, "call data empty");
        }
    }
    

    如果方法正确执行,call方法就返回一个json字符串code=200,否则就传code=500,这个信息会通过prompt方法的返回值传给js,这样Html 5 代码就能知道有没有正确执行了。

    相关文章

      网友评论

        本文标题:Android Hybrid App通信

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