Hybrid 开发

主讲人:吴彬

要学习某个东西之前,我们首先要了解这个东西是什么?然后我们要了解这东西有什么用,有什么好处和弊端?最后我们要知道这东西怎么用? 简单点就是 ——是什么?有什么用?怎么用? 那么进入正题

一、什么是Hybrid 开发?

Hybrid App开发(混合模式移动应用开发)是指开发介于web-app、native-app这两者之间的app,这种app兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。

二、存在即合理,Hybrid开发的意义

首先,我们看下Hybrid app和web-app、native-app之间的一些区别

可见Hybrid app开发相比于其他开发还是好处多多的,那么它相比于Native开发的好处有哪些呢?

使用Native开发的方式人员要求高,只是一个简单的功能就需要iOS程序员和Android程序员各自完成;

使用Native开发的方式版本迭代周期慢,每次完成版本升级之后都需要上传到App Store并审核,升级,重新安装等,升级成本高;

使用hybrid开发的方式简单方便,同一套代码既可以在IOS平台使用,也可以在Android平台使用,提高了开发效率与代码的可维护性;

使用hybrid开发的方式升级简单方便,只需要服务器端升级一下就好了,对用户而言完全是透明了,免去了Native升级中的种种不便;

三、如何进行Hybrid 开发?

Hybrid 开发可以使用第三方框架(此处不细讲,有兴趣的朋友可以自行了解),也可以用Android自带的webView来加载H5 那么我们来讲讲android 自带的webView和Js之间的互调吧。

首先,别忘记加网络权限和引入webView.

Native调用JS

在4.4之前,调用的方式:

	//ui线程中运行,因为mWebView是UI控件
 	runOnUiThread(new Runnable() {  
        @Override  
        public void run() {  
	    //缺点:无法取得返回值
            mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");  
            Toast.makeText(Activity.this, "调用方法...", Toast.LENGTH_SHORT).show();  
        }  
	});    

4.4以后(包括4.4),使用以下方式:

	mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
	@Override
	public void onReceiveValue(String value) {
	//这里的value即为对应JS方法的返回值
	}
	});

传统的Native调用JS还有个缺点就是不适合传输大量数据(大量数据建议用接口方式获取)。

JS调用Native

Native中通过addJavascriptInterface添加暴露出来的JS桥对象,然后再该对象内部声明对应的API方法。

WebSettings webSettings = mWebView.getSettings();  
 //Android容器允许JS脚本
webSettings.setJavaScriptEnabled(true);
//Android容器设置侨连对象,api17前有可被hacker攻击的漏洞,api17(4.4)之后调用需要在调用方法加入加入@JavascriptInterface注解,
//如果代码无此申明,那么也就无法使得js生效,也就是说这样就可以避免恶意网页利用js对安卓客户端的窃取和攻击。
mWebView.addJavascriptInterface(this, "JSBridge");

首先看下这个方法

public void addJavascriptInterface(Object object, String name)

我是在Activity里 写的这段代码,那么这里第一个参数传入this,即Activity实例,第二个参数则是给第一个参数起的别名,方便JS里调用。

然后在Activity里暴露两个方法

		//在Android4.2以上(api17后),暴露的api要加上注解@JavascriptInterface,否则会找不到方法
        @JavascriptInterface
        public String getStudentName(){  
            return "张三";  
        }  

        @JavascriptInterface
        public String getMyName(final String name){  
            return "my name is:" + param;  
        }

然后我们在HTML里调用Native方法

	//调用方法一
	window.JSBridge.getStudentName(); //返回:'张三'
	//调用方法二
	window.JSBridge.getMyName('韩梅梅');//返回:'my name is 韩梅梅'

OK,以上就是我们传统的调用方式,但是显而易见,传统的调用存在许多的不足,所以出现了许多好用的第三方框架,那么接下来,我们讲解一个常用框架JSBridge

四、JSBridge

OK,让我们提出我们的疑问,什么是JSBridge?JSBridge有什么好处(相比于传统的Native和JS互调)?JSBridge是怎么设计出来的呢?JSBridge怎么使用呢?

1、什么是JSBridge?

JSBridge就是js和Native之前的桥梁,是用来定义Native和JS通信的,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用Native。

2、为什么使用JSBridge?

Android4.2以下,addJavascriptInterface方式有安全漏掉 iOS7以下,JS无法调用Native JSBridge有一套现有的成熟方案,可以完美兼容各种版本,对以前老版本技术的兼容。

3、JSBridge的原理与实现

我们知道,Native要调用JS是非常简单的,只要使用WebView.loadUrl(“JavaScript:function()”)即可。那么JS怎么调Native呢?很明显,上面讲的已经行不通了,毕竟有兼容性问题和安全问题。那么怎么办呢?我们得寻找一个突破口吧,大家仔细回一下,WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,我们是不是可以利用这个机制,自己做一些处理?答案是肯定的。

至于js这三个方法的区别,可以详见w3c JavaScript 消息框 。一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,然后让你输入,输入完成后返回输入框中的内容。因此,占用prompt是再完美不过了。

OK,找到突破口后,我们就开始实现通信,那么怎么实现呢?既然要通信,那么我们是不是得有通信协议呢?我们可以参考http 的样式http://host:port/path?param=value 来 规定一个属于我们自己的协议:

jsbridge://className:port/methodName?jsonObj

这样我们Native拿到这串字符串后,通过解析,就可以知道调用哪个类的哪个方法,此处jsonObj用来封装请求的参数。

细心的朋友一点发现了里面有个port,那么这个port是拿来干什么的呢? 其实js层调用native层方法后,native需要将执行结果返回给js层,不过你会觉得通过WebChromeClient对象的onJsPrompt方法将返回值返回给js不就好了吗,其实不然,如果这么做,那么这个过程就是同步的,如果native执行异步操作的话,返回值怎么返回呢?这时候port就发挥了它应有的作用,我们在js中调用native方法的时候,在js中注册一个callback,然后将该callback在指定的位置上缓存起来,然后native层执行完毕对应方法后通过WebView.loadUrl调用js中的方法,回调对应的callback。那么js怎么知道调用哪个callback呢?于是我们需要将callback的一个存储位置传递过去,那么就需要native层调用js中的方法的时候将存储位置回传给js,js再调用对应存储位置上的callback,进行回调。

上面是js向native的通信协议,那么另一方面,native向js的通信协议也需要制定,一个必不可少的元素就是返回值,这个返回值和js的参数做法一样,通过json对象进行传递,该json对象中有状态码code,提示信息msg,以及返回结果result,如果code为非0,则执行过程中发生了错误,错误信息在msg中,返回结果result为null,如果执行成功,返回的json对象在result中。下面是两个例子,一个成功调用,一个调用失败。

	{
    "code":500,
    "msg":"method is not exist",
    "result":null
	}


	{
    "code":0,
    "msg":"ok",
    "result":{
        "key1":"returnValue1",
        "key2":"returnValue2",
        "key3":{
            "nestedKey":"nestedValue"
            "nestedArray":["value1","value2"]
        }
    }
	}

Ok,这样我们在native里调用

mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");

即可,在调用JS暴露的方法时顺便把port带上,为什么要带port,可以参考上面一段解释。

好了,那么我们可以根据以上的内容先来设计JS了(看注释的重点)

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

我们在call()方法里随机生成领域各port后将callback存入callbacks数组中的port位置,根据之前我们定义的协议生成uri,调用window.prompt(uri,””); 我们还需要一个方法来处理js回调,所以有了onFinish方法,方法里面做的事:首先根据port从callbacks中取出callback执行,然后将callbacks的port位置元素删除。

好了,JS这边处理完了,那我们native这边是不是要暴露方法给JS调用呢? 没错,我们需要一个类似android 原生的@javascriptInterface引用,就是暴露给JS调用的方法,所以我们有了如下的方法

JSBridge.register("jsName",javaClass.class)

这个javaClass就是满足某种规范的类,该类中有满足规范的方法,我们规定这个类需要实现一个空接口,为什么呢?主要作用就混淆的时候不会发生错误,还有一个作用就是约束JSBridge.register方法第二个参数必须是该接口的实现类。那么我们定义这个接口

public interface IBridge{
}

类规定好了,类中的方法我们还需要规定,为了调用方便,我们规定类中的方法必须是static的,这样直接根据类而不必新建对象进行调用了(还要是public的),然后该方法不具有返回值,因为返回值我们在回调中返回,既然有回调,参数列表就肯定有一个callback,除了callback,当然还有前文提到的js传来的方法调用所需的参数,是一个json对象,在java层中我们定义成JSONObject对象;方法的执行结果需要通过callback传递回去,而java执行js方法需要一个WebView对象,于是,满足某种规范的方法原型就出来了。

public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){

}

OK,那我们看看这个register方法究竟做了些什么事吧

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

简单来说,我们这个方法做的事就是先判断这个类的别名(此处是jsName)是否存在,不存在的话就往里面添加该类暴露出来的方法(三个参数要满足上面的规范的方法)

好了,我们暴露出去方法了,那么这时候js调用后,从我们的突破口,没错,就是onJsPrompt方法里,我们要做的事就是调用我们的native方法,所以有了以下代码

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

我们callJava方法里做了什么事呢?其实就是解析url,查找对应的暴露方法调用

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

好了,最后,我们当然是来讲Callback啦,看重点注释就可以了,其实就是回调js的onFinish方法。

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

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

别忘了注册

JSBridge.register("bridge", BridgeImpl.class);

JS这边我只放重点的那行代码

<button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">

接着就是使用WebView将该index.html文件load进来测试了

mWebView.loadUrl("file:///android_asset/index.html");

点击按钮后的结果截图

好了,那我们来试试子线程回调,在在BridgeImpl中加入测试方法

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

JS中加入


	<button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">

点击后,3秒弹出

有些框架里面的突破口不在onJsPrompt方法里,在shouldOverrideUrlLoading方法里,不过原理应该差不多。

好了,完结,撒花,放鞭炮,收工!