Android随笔

Android JS与Native交互实践

2018-11-22  本文已影响0人  谭海洋

前言

在移动开发中,开发的需求和节奏都越来越快,而Native App在这种节奏中略显笨拙,开发周期长、用户升级慢、应用市场审核时间长都深受开发者弊病。而这时候很多开发者都提出了Hybrid App的概念,这种开发模式有着迭代灵活、多端统一、开发周期短、快速上线等优势。但是Hybrid App也有其不足的地方,在性能很难到达Native App的水平,在访问设备上的硬件时也不是那么得心应手。对于这些问题,现在已经有较多的解决方案,比较重的框架有Facebook的React Native,轻量级别也有ionic。如果是已经成熟的产品,Web页面较多迁移比较困难,也可以使用VasSonic来提升WebView体验,然后通过JS调用Native。目前公司项目中由于历史原因采用后者的方式来实现,但是在使用过程中由于没有统一的管理,存在了通讯方式多样、调用混乱和安全性差等几个问题。下文主要讲述如何通过重新设计JS调用框架来解决以上问题。

Android WebView JS交互

首先介绍一下WebView中JS和Native相互调用的方式、相互之间的差异。

Android调用JS

WebView调用JS有以下两种方式:

在API 19之前是只能通过WebView.loadUrl()进行调用JavaScript。在API 19的时候新提供了WebView.evaluateJavascript(),它的运行效率会比loadUrl()高,还可以传入一个回调对象,方便获取Web端的回传信息。


webView.evaluateJavascript("fromAndroid()", new ValueCallback<String>() {

@Override

    public void onReceiveValue(String value) {

    //do something

    }

});

JS调用Android代码

JS调用Native代码有以下三种方式:

WebView.addJavascriptInterface()是官方推荐的做法,在默认情况下WebView是关闭了JavaScript的调用,需要调用WebSetting.setJavaScriptEnabled(true)来进行开启。这个方法需要一个Object类型的JavaScript Interface,然后通过@JavascriptInterface来标注提供的JS调用的方法,下面是一个Google官方提供的例子:


public class AppJavaScriptProxy {

    private Activity activity = null;

    public AppJavaScriptProxy(Activity activity) {

        this.activity = activity;

    }

    @JavascriptInterface

    public void showMessage(String message) {

        Toast toast = Toast.makeText(this.activity.getApplicationContext(),

                message,

                Toast.LENGTH_SHORT);

        toast.show();

    }

}


webView.addJavascriptInterface(new AppJavaScriptProxy(this),“androidAppProxy”);


// JS代码调用

if(typeof androidAppProxy !== "undefined"){

    androidAppProxy.showMessage("Message from JavaScript");

} else {

    alert("Running outside Android app");

}

这样就可以实现JS调用Android代码,使用者只需要关注被JS调用方法的实现,对调用的过程是不可知的。使用的时候有几个要注意的地方:

  1. 提供用于JS调用的方法必须为public类型

  2. 在API 17及以上,提供用于JS调用的方法必须要添加注解@JavascriptInterface

  3. 这个方法不是在主线程中被调用的

WebViewClient.shouldOverrideUrlLoading()是通过拦截Url的方式来实现与JS的交互。shouldOverrideUrlLoading()返回true时,代表拦截这次请求,让我们自己处理。shouldOverrideUrlLoading()返回false时,代表不拦截这次请求,让WebView去处理这次请求。

WebChromeClient.onJsAlert()、onJsConfirm()、onJsPrompt()三种方式和WebViewClient.shouldOverrideUrlLoading()类似,都是通过拦截请求的方式达到交互功能。

总结:这三种方式实际上可以归纳成两种:JavascriptInterface和拦截请求,两者之间各有好坏。

JS调用框架设计

为了解决前言中提到的通讯方式多样、调用混乱和安全性差等几个问题,需要重新设计JS调用框架,将整个流程从WebView中剥离出来,达到低耦合的目的。综合考虑后,决定沿用项目中之前的解决方式,通过拦截WebView请求的方式来实现。拦截性的方式在设计框架之前还需要考虑到通讯协议的问题。

协议设计

png1.png

如上图所示,通过设计通讯协议达到多端统一通讯。协议上面可以参考现有的通讯协议,或者根据项目需求和前端设计一套通用协议。这里推荐一种简单的现有的协议:统一资源标志符。

png2.png

jsbridge://method1?a=123&b=345&jsCall=jsMethod1"

该种标识允许用户对网络中(一般指万维网)的资源通过特定的协议进行交互操作,在这里不用完全使用,只使用了其中的三个字段。scheme定义为jsbridge,用于区分别的网络请求。authority用来定义JS需要访问的方法。后面的query用来传参数,如果需要客户端回调信息给前端,就可以加个参数jsCall=jsMethod1,然后客户端处理完后就可以通过WebView进行回调。


WebView.loadUrl("javascript:jsMethod1(result=1)")

这样就定义了一种简单的交互方式,能让JS和Native拥有基础的交互能力。如果需要传文件,可以通过将文件流转成Base64然后在通讯,当然如果文件太大,这种方式会有内存方面的风险。这里还有另外一种方式,拦截WebView的资源请求,将文件以流的形式进行通讯:


webView.setWebViewClient(new WebViewClient(){

@Override

    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

        return new WebResourceResponse("image/jpeg", "UTF-8", new FileInputStream(new File("xxxx");

    }

}

框架设计

在设计中,主要考虑了以下几点:

  1. 安全性: 防止第三方网站获取用户私密信息和通讯被第三方截取信息。(域名白名单、数据加密)

  2. 易用性: 设计框架都需要考虑易用性,方便实用。如Android的JavascriptInterface方式,使用者只用关注被调用方法的实现。(参考Android的JavascriptInterface方式)

  3. 可移植性: 现在Android系统日新月异,每个版本都有较大改动和优化,如果出现更好的方案或者特性时,要方便迁移整个JsBridge方案。(设计中要职责分明)

  4. 扩展性: 方便业务逻辑扩展。(添加中间件)

通过分析整个通讯的流程,结合项目中的需要,大体抽象出通讯流程中的五个角色:

  1. JsBridge: 整个Js框架的管理,提供对外的接口,连接Processor和JsProvider。

  2. JsCall: 抽象一次请求,包含一次请求的内容和环境,拥有回调信息给前端的接口。

  3. IProcessor: 协议的抽象体。由于项目原因需要多套协议兼容,所以抽象出协议,负责请求的分类和解析。

  4. JsProvider: Js方法的提供者,本身是Object类型,方便现有代码迁移,而且和JavascriptInterface方式一致,也方便以后迁移。

  5. @JsMethod: Js方法的一个注解,类似@JavascriptInterface。

png3.png

这样的模式和系统提供的JavascriptInterface方式基本一致,但是我们可以做的事情比JavascriptInterface方式更多,而且整个系统解耦清晰,但是这个结构实际上还缺乏较多的东西,无法达到设计的目标,整个流程中缺乏扩展性,没有拦截和二次处理机制。

可以在执行JsMethod之前添加一个拦截器,增强扩展性。

png4.png

安全性方面也可以通过添加拦截器的方式来实现,将JS请求拦截在执行JsMethod之前,而每个JsMethod的安全级别可以通过扩展注解参数来标注。例如下面代码,添加permission字段来标示方法的安全级别。


@JsMethod(permission = "high")

public void requestInfo(IJsCall jsCall) {

// do something

}

框架骨架搭建好了后,还需要一些优化性的设计:

  1. 日志系统:添加日志开关,打印关键性的日志。

  2. 线程转换:由于WebViewClient.shouldOverrideUrlLoading是在主线程里面执行,可以参考Android做法,将JS方法都放到其它线程去做,不影响页面流畅度。

  3. 异常机制:将框架中发生的异常统一管理后,抛出给框架调用者。

  4. ... (结合业务设计)

实现效果

最后,框架大体设计完毕,实现都是比较简单的。现在来看看使用的时候,首先是JS发起一个请求:


  var iframe = document.createElement('iframe');

  iframe.setAttribute('src', 'jsbridge://method1?a=123&b=345');

  document.body.appendChild(iframe);

  iframe.parentNode.removeChild(iframe);

  iframe = null;

客户端只需要简单的对WebView的请求做拦截。


@Override

    public boolean shouldOverrideUrlLoading(WebView webView, String url) {

    boolean handle = super.shouldOverrideUrlLoading(webView, url);

    if (!handle) {

handle = JSBridge.parse(activity, webView, url);

}

return handle;

}

创建一个解析当前协议的对象,这个是以后都可以复用的:


public class JsProcessor implements IProcessor {

    public static final int TYPE_PROCESSOR = 1;

    /**

    * 协议编号

    * @return

    */

    @Override

    public int getType() {

        return TYPE_PROCESSOR;

    }

    /**

    * 判断请求是不是属于这个协议

    * @param url

    * @return

    */

    @Override

    public boolean isProtocol(String url) {

        return !TextUtils.isEmpty(url) && url.startsWith("jsbridge");

    }

    /**

    * 解析协议

    * @param context

    * @param webView

    * @param url

    * @param webViewContext WebView的环境

    * @return

    */

    @Override

    public IJsCall parse(Context context, WebView webView, final String url, Object webViewContext) {

        return new IJsCall<RequestBean, ResponseBean>() {

            private String mMethodName;

            @Override

            public void callback(ResponseBean data, WebView webView) {

                JSBridge.callback(data, webView);

            }

            @Override

            public String url() {

                return null;

            }

            @Override

            public RequestBean parseData() {

                if (TextUtils.isEmpty(url)) {

                    return null;

                }

                Uri uri = Uri.parse(url);

                String methodName = uri.getPath();

                methodName = methodName.replace("/", "");

                mMethodName = methodName;

                return new RequsetBean(url);

            }

            @Override

            public String method() {

                return mMethodName;

            }

        };

    }

}

创建一个提供JS方法的对象,在对外提供的方法上加入注解@JsMethod,并标注调用该方法的协议编号、方法名称和权限级别,方法中所需要的信息都通过IJsCall获取,处理完成后,通过IJsCall回调信息给JS。


public class JsProvider {



    @JsMethod(processorType = JsProcessor.TYPE_PROCESSOR, name = "method1", permission = "high")

    public void method1(IJsCall jsCall) {

        // do anything

        // ...

        // ...

        // ...

        jsCall.callback("xxxx");

    }

}

png5.png

以上就完成了一次JS和Native的通讯。整个通讯的细节不对外开放,使用者只用关注方法的开发,方法的信息通过注解来承载,解析注解时可以通过编译时生成代码来提高效率。白名单和数据加密直接通过拦截器来实现。整个系统完美的解决了之前项目中问题,而且也方便以后的业务发展。

总结

Hybrid App是以后的趋势,JS和Native之间业务逻辑也会越来越重,所以项目中这块的设计也非常重要,需要不断的根据业务来调整,保证其稳定性的同时,又有很强的扩展能力。

参考链接

https://www.jianshu.com/p/93cea79a2443

https://zh.wikipedia.org/zh-hans/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E6%A0%87%E5%BF%97%E7%AC%A6

上一篇下一篇

猜你喜欢

热点阅读