android webview中js异步调用native接口实现
发布在前端杂谈2014年11月22日view:8209
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

导言

之前偶尔的机会接触了某个的大牛介绍了一下在pad客户端里嵌入的活动页面相关的优化工作,猛然醒悟,原来在移动应用开发中,webview是界面开发的神器,只要搞得定webview就等于在PM和美工心目中贴上了东方不败的铭牌。而且针对移动端的优化是有很多值得做的东西的。

没过多久就被我碰到了一个机会,要设计一个包含webview的安卓app。一开始按部就班地搭环境,码代码,没过多久就遇到了问题:面向低版本编译的安装包在高版本上js接口执行不了!

然后就开始了疯狂的一边找解决方案一边想替代方案。最终竟然真的让我琢磨出了一个极其巧妙的工作模式,虽然可能不是首创的,但绝对是自己想出来的,趁着一点空闲在这献丑贴一下。

浅谈web app混合编程

有过混合编程经验的同学都知道,选择一个好的通讯机制很重要。一般涉及到多种语言协同工作时,常用的结合方式可以分为三种:

  • 底层结合,通过底层api的支持,将一个语言的功能编程另一个语言的接口,底层提供语言之间的数据转换,作者接触过的有JNI, nodejs, flash的ExternalInterface, android的addJavaScriptInterface, 还有ngx_lua等。优点是性能可以优化的较好,缺点是通用性差,对语言环境本身要求较高。
  • 外部结合,通过socket/pipe等通讯机制,实现对接口的远程调用。这种方法的优点是对语言要求低,系统耦合度低,缺点是数据序列化、结果回传等性能低,且基于输入输出使得可靠性下降
  • 语言级嵌入,用一个语言实现另一个语言,或者将一个语言编译成另一个语言的编译结果,如rhino, swift等都是这样,其中一种语言实现了另一种语言的vm,或者是另外一种语言的api超集

相对于ios的UIWebView, 安卓提供了addJavaScriptInterface, 支持通过“底层结合”的方式提供给webview调用支持。但是这种方式存在以下问题:

  • 阻塞界面。我们知道浏览器的渲染以及js都是在一个线程中,所以如果js调用的native代码执行时间过长,就会导致整个webview卡顿。那么包含网络请求的过程就需要创建额外的线程来跑,并采用异步回调的方式回传结果,使得本来很简洁优美的代码变得复杂无比
  • 兼容性问题。安卓自从4.2.2开始引入了JavascriptInterface,使得面向低版本编译的程序无法在高版本手机上执行
  • 安全性问题。已报告了在低版本手机上通过反射方式获得执行任何代码的问题(通过getClass().getClassLoader()等手段)

那么除了这条路以外还有好的解决方案么?对,就是和ios的shouldStartLoadWithRequest类似的API: shouldInterceptRequest

shouldInterceptRequest 实现混合编程

原理很简单,当浏览器页面发起一个请求时,可以通过重载webview的webviewclient中的函数监听网络请求,当网络请求满足一个规则时(如以/native/开头),则不再进行网络请求,而是调用native代码并返回结果。这种模式有几种好处:

  • 页面初始化速度快。js代码无需等待接口绑定完成就可以直接运行,而且关键的初始化数据可以通过写入到html中或者作为js请求的响应方式灌入到页面中,从而最大化减少首屏时间
  • 兼容性好。除了兼容不同版本安卓外,也更容易迁移到ios中。
  • 不阻塞页面。我们可以通过jsonp或者异步xhr的方式发起请求,native代码的执行不影响webview响应用户输入,且完美支持jQuery等ajax封装库。所以网络io等耗时的操作完全可以直接放在native函数调用中而无需新建线程。
  • 支持异步代码执行。因为响应本身就是异步的,所以我们完全可以将响应推迟到shouldInterceptRequest函数执行完成后,而无需改动现有的编程结构。

下面我将会提供一个简单的实现,里面通过匹配url地址来得到调用参数,返回数据。

hook web请求

public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    Uri uri = Uri.parse(url);
    if (!uri.getPath().startsWith("/native/"))
        return null;
    PipedOutputStream out = new PipedOutputStream();
    try {
        PipedInputStream in = new PipedInputStream(out);
        doSomething(uri, out);
        return new WebResourceResponse("application/json", "utf-8", in);
    } catch (IOException e) {
    }
    return null;
}

我们在调用doSomething时传入了两个参数:一个Uri和一个OutputStream,前者可以从中读取请求数据,后者可以写入响应结果。下面来演示两种返回数据方式:同步回调和异步回调

同步的调用

void doSomething(Uri uri, OutputStream out) throws IOException {
    PrintWriter writer = new PrintWriter(out);
    JSONObject result = new JSONObject();
    result.put("result", "You have visited: " + uri);
    writer.write(result.toString());
    writer.close();
}

异步的调用

void doSomethingAsync(Uri uri, OutputStream out) throws IOException {
    final PrintWriter writer = new PrintWriter(out);
    Http.get("http://www.example.com", new Http.StringHandler() {
        @Override
        public void resolved(Request req, Response res, String result) {
            writer.write(result);
            writer.close();
        }

        @Override
        public void rejected(Request req, IOException e) {
            writer.write(e.getMessage());
            writer.close();
        }
    });

}

在这里我们用了相似的方式来处理响应数据:将OutputStream封装为PrintWriter,并调用write方法写入数据,调用close方法结束写入。不过前者的数据写入发生在WebResourceResponse返回前,后者的写入发生在之后。对浏览器来说,都是在若干时间后收到了响应,所以处理方式是一样的。

后记

(虽然我有写文章必须有前有后的强迫症,但今天太晚了就这样吧)

评论
发表评论
暂无评论
WRITTEN BY
Kyrios
介绍什么的超麻烦的
TA的新浪微博
PUBLISHED IN
前端杂谈

一些杂七杂八的东西,仅供备忘

我的收藏