安卓入门之WebView

过去的一年大大小小写了不少移动端项目(基本都是微信公众号),再加上之前跟Android的同学咨询过Hybird开发,因此对于WebView或多或少有一些了解。恰好最近的项目,需要跟移动端的同事协作处理一个分享页面,所以一并整理出来。

<!--more-->

参考:

1. 基础

webview主要就是用来在应用中加载和显示网页。一看webview的名字里面居然挂了个view后缀,没错,这也是一个组件,在布局里面直接调用即可

<WebView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/webview_1">
</WebView>    

跟网页中插入一个iframe的感觉挺像的,为了体验一般会将整个组件完全填充在屏幕上(顶部栏不算),除此之外,并不需要在额外设置其他样式(因为整个页面的样式都交给CSS控制了)。

骨架是有了,但我们貌似还没有指定加载网页的URL呢?没错,接下来的工作就是在加载网页了,但是在此之前,我们还可以对webview进行一系列设置

// 获取webview对象
webview = (WebView) findViewById(R.id.webview_1);

WebSettings webSettings = webview.getSettings();
// 禁止缓存
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
// 允许javascript
webSettings.setJavaScriptEnabled(true);

// 处理webview的各种事件和通知
webview.setWebViewClient(new WebViewClient(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url){
        view.loadUrl(url);
        // 这里主要告知提供使用webview而不是浏览器打开网页
        return true;
    }
});

// 允许弹窗等操作
webview.setWebChromeClient(new WebChromeClient());

// 加载网页
webview.loadUrl("http://www.shymean.com/webview.html");

观察上面这段代码,除了webview对象之外,还有三个类,他们分别是:

  • WebSettings,主要用来设置webview对象,包括缓存,JavaScript,缩放和布局等设置接口,这里遇见的第一个坑就是网页刷新页面不发生改变,被缓存坑了无数次的我马上想到了禁止缓存(是不是很机智[/斜眼])
  • WebViewClient,处理网络请求时的各种事件,这里需要注意的是为了防止应用使用系统浏览器打开网页,需要覆盖shouldOverrideUrlLoading方法
  • setWebChromeClient,在调试的时候遇见的一个坑,浏览器的弹出框在webview中不生效!!后来发现原来是没有设置WebChromeClient的缘故~

实际上这三个类提供了非常多的接口供我们使用(现在我感觉微信公众号就是一个非常庞大的webview),这里先不展开了,有需要再去查文档。

最后,打开模拟器看看效果,卡擦,什么鬼?原来还需要在manifest中配置网络权限,这茬千万比忘记了

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

2. Hybird

关于Hybird这里有一篇不错的文章:浅谈Hybrid技术的设计与实现。我觉得webview一个非常吸引人的地方就是整个界面甚至业务逻辑都可以交给web前端处理,但是又为JS提供调用原生的系统方法的途径。

讲道理,最近两三周一直在搞安卓,个人认为搭页面用CSS比写Android快N倍(还不算上用SCSS,livereload这些了),但是论体验肯定是APP要好得多。哈这里抛开布局不谈,还是来了解JS与原生之间是如何实现交互的。

2.1. JS调用原生方法

最近的工作需求是完成一个分享页面,需要通过原生应用将微信QQ等应用分享的权限下放到webview中,那么问题来了,原生的方法如何才能够被webview页面上的JS调用呢?

首先定义方法:

public class WebAppInterface {
    Context mContext;
    WebAppInterface(Context c){
        mContext = c;
    }

    @JavascriptInterface
    // 这里为了省事就只是调用个原生的提示框了
    public void showToast(String toast){
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

然后向webview中注入定义的方法,实际上就是向webview的宿主对象添加了一个全局变量

 // ...省略相关配置
 webview.addJavascriptInterface(new WebAppInterface(this), "Android"); // 这个名字是自定义的,当然得跟前端商量好
 webview.loadUrl("http://www.shymean.com/webview.html");

接着,我们就可以在这个webview.html页面中通过全局变量Android来访问对应的方法showToast

<script type="text/javascript">
    console.log(Android);
    Android.showToast("hello Android");
</script>

宾果~现在在模拟器中打开,就可以看见网页上触发了原生的提示框效果,这对于整个应用的设计统一是很有帮助的。

上面有个需要注意的地方:在Android API 17后的JS接口需要使用@JavascriptInterface注解,而webview中的JS代码只能调用声明时使用该注解的Java方法。

2.2. 原生调用JS方法

loadUrl

前面通过loadUrl为webview加载页面,实际上也可以通过它来调用JS函数。稍微修改一下布局,添加一个原生的按钮,然后注册点击事件处理方法

public void callJS(View view){
    webview.post(new Runnable() {
        @Override
        public void run() {
            webview.loadUrl("javascript:alert('Hello from Android');");
        }
    });        
}

这里使用的是webview.post传入了一个任务,然后通过loadUrl执行了一个伪协议,在《DOM编程艺术》中了解到,通过伪链接javascript:;的方法也可以执行JS代码,这就相当于为原生Android代码提供了调用JS代码的途径。

这里只是简单调用了alert方法,实际上可以调用任何已被注册的JS函数,相当于达到了我们通过原生调用JS代码的目的。

这里有一个小疑问:看见有的博客上介绍到这里必须通过post来执行loadUrl,然而实际上经过测试我发现直接在callJS函数中使用loadUrl也是可以执行的。查看post方法的源码发现:

The runnable will be run on the user interface thread.

实际上任务也是跑在UI线程的,可能是前面那些博客的Android版本问题,毕竟现在都2017年了,这个问题就不深究了。

另外还有介绍到使用loadUrl调用伪协议会刷新页面,实际上我也没有发现这个问题,可能是测试的不够全面,先挖个坑吧。

最后的一个问题是如何获取js函数的返回值呢?查这个问题的时候了解到了evaluateJavascript这个接口。

evaluateJavascript 在Android 4.4之后,我们还可以使用evaluateJavascript来调用js方法,最主要的,该方法可以很方便的获取函数返回值

public void callJS(View view){
    webview.evaluateJavascript("javascript:responseFromJS();", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            Log.i(TAG, value);
        }
    });
}

对应的Javascript代码

function responseFromJS(){
    alert("recive");
    return "value from js";
}

这个返回值ValueCallback也大有讲究,毕竟js跟java是两门完全不一样的语言,在js函数中返回的闭包函数这里会被处理成null哦~不过一般原生和js之间的通信应该都是传基础数据而已吧,如果需要传递函数,一个折衷的办法是两端规定方法的索引值(或者其他的能表明某个方法的字段),在JavaScript返回这个索引值,然后在安卓这边通过判断这个值调用对应的原生方法。

2.3. JsBridge

原生和JS的代码交互产生了一个JsBridge的概念,归根结底就是原生和JS的代码交互问题:

  • 原生调用JavaScript代码
  • JavaScript调用原生代码,实际上,通常更多地应该是JS借助原生接口实现HTML页面的更多功能
  • 参数和返回值处理

这个暂时就没有深入了,首先应该掌握前面俩小结介绍的东西。不同的业务下JsBridge应该也不尽相同,而在Hybird开发中,应该要写大量的JsBridge吧。

这么回想起来,微信公众号中的JSSDK也算作是一个JsBridge了吗。PS:为啥要叫做"JS"Birdge呢?

3. 调试

开发WebView页面一个非常蛋疼的问题就是:这特么该怎么调试啊?之前做微信公众号项目的时候,好歹还有微信开发者工具(虽然被我吐槽了无数次),现在怎么办?

起初只能有最原始的办法:alert大法。不过这个调试起来真的太坑爹了,别的不说,alert的遗留问题:查看对象啥的都十分麻烦。

目前的的调试方法是模拟器结合Chrome进行调试,这个还是比较人道的:

  • 打开模拟器,进入一个webview页面(或者打开浏览器随便进个页面)
  • 打开chrome浏览器(我现在使用的版本是58.0.3029.110),地址栏输入chrome://inspect/#devices
  • 正常情况下可以在Remote Target这里发现我们打开的页面,然后点击下面的inspect按钮,会弹出一个Developer Tools窗口,这个过程可能需要加载一段时间
  • 现在就可以像调试PC端页面一样的来调试webview页面了,查看数据啥的也简单多了。但是需要注意的是有的操作(比如调用原生Toast)这些操作在开发者工具上面是看不见效果的
  • 哈哈,上面说的都是模拟器下的调试,使用真机的话,需要使用USB连接电脑和手机,然后再手机勾选开发者选项啥的,后面就跟这些操作差不多了(PS:真机调试兼容是多么苦逼的事情啊~~)

4. 小结

了解了webview和基本的Android之后,用前端的东西就感觉可以直接开发出一个“伪”APP了。不过这里只介绍了最基本的使用方法,具体的坑还得在实践中一步一步去填。