Android开发之WebView知识和常见问题


Android开发之WebView知识和常见问题

一、前言

最近在学习HTML5相关的知识,发现前端技术的功能越来越强大了,很多功能如果我们通过原生代码的形式进行实现的话相对于H5会花费数倍的时间,在最求快速迭代的时候是不可取的,再一个就是现在App的种类也越来越多了。

以前一提起Android开发,我们就会想到Java,一提起IOS我们就会想到Object-c、swift,可是技术的进步速度之快让人难以想象,现在市面上已经有非常多的技术可以用来进行移动开发,这里就列举几种我所知道的技术,对于前端开发工程师而言,我们可以使用JqueryMobile来开发一个WebApp、利用AngularJs加cordova加PhoneGap开发一个可跨平台的纯WebApp,或者结合Java原生开发一个Hybird混合App。

使用H5开发App有一个巨大的优势就是它开发的产品可以跨平台,并且开发成本低,而且我们进行开发的时候有很多的前端开源框架可以让我们很容易的写出许多漂亮的页面,对于一个WebApp最重要的非WebView莫属了,一个WebView的性能好坏,因此在当前下,同一款App往往在Ios系统上的性能强于Android就是这个原因,有的程序为了App减少平台带来的差异还在App中加入了自己的WebView。那么我们就来看看WebView这个核心控件。

二、WebView的介绍

A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.

上文是Google对于WebView的介绍,一句话它就是一个显示网页的控件,并且可以简单的显示一些在线的内容,并且基于WebKit内核。

在Android4.4(API Level 19)引入了一个新版本的基于Chromium的Webview,这让我们的的WebView能支持HTML5和CSS3以及JavaScript。

注意:用于Webview的升级,对于我们的程序带来了一些影响,如果我们的targetSdkVersion设置的是18或则更低,single and narrow column和default zoom levels不在支持。并且在Android4.4中我们可以通过设置setWebContentDebuggingEnabled()方法让我们的程序可以进行远程桌面调试。

三、WebView的使用及常见坑

  • 我们就拿如何在一个Activity中显示一个WebView来说吧,我们可以通过创建一个WebView对象:
    这里写图片描述
    或则在布局文件中定义,再在主界面中findViewById:
    这里写图片描述
    在或则我们还可以创建一个Fragment类继承WebViewFragment:
    这里写图片描述
    当然这对于我们来说也只是创建了一个WebView,接下来我们就看看如何使用它,让它显示我们丰富的内容。
  • Google为我们提供了几种让WebView记载数据的方法,我们就简单的进行介绍下:

    这里写图片描述

1.loadData()方法:

当我们不需要加载一整个的HTML页面而是加载一小段的类容时,此时我们就可以使用这个方法,他需要我们传入三个参数,第一个就是我们需要Webview展示的内容,第二个是我们告诉WebView我们展示内容的类型,一般,第三个是字节码(这货就是一个坑,我们下面会介绍)。

说完介绍我们来看一下如何使用它,我们看一个简单的例子:

这里写图片描述

我们让WebView加载一个简单的富文本标签,看看运行过后的效果:

这里写图片描述

What?怎么是乱码,我们明明指定了编码格式为UTF-8啊,可是现实是残酷的,那下面我们来看看解决办
法:
这里写图片描述

这里写图片描述

这下我们终于看到了正确的内容,Google还提出了,在这种加载的方法下,我们的Data数据里不能出现’#’, ‘%’, ‘\’ , ‘?’ 这四个字符,如果出现了我们要用%23, %25, %27, %3f,这些字符来替换,我在测试的过程中没有遇到错误,当以后遇到类似情况时可以用于排查问题,下面附上网上以为为将特定字符转义过程中遇到的现象:

A)   %,会报找不到页面错误,页面全是乱码。乱码样式见符件。
   B)   #,会让你的goBack失效,但canGoBAck是可以使用的。于是就会产生返回按钮生效,但不能返回的情况。
   C)   \ 和? 我在转换时,会报错,因为它会把\当作转义符来使用,如果用两级转义,也不生效。

我们在使用loadData时,就意味着需要把所有的非法字符全部转换掉,这样就会给运行速度带来很大的影响,因为在使用时,在页面stytle中会使用很多%号。页面的数据越多,运行的速度就会越慢。

2.laodDataWithBaseUrl():

其实这个方法的用处和我们上面的方法差不多,但是多出了两个参数,BaseUrl和historyurl,那它们的区别在什么地方了,我们设想一下这样的场景,在一些第三方的资源存储网站中,当我们需要用它来托管我们上传的例如图片的资源时,上传成功后它会给我们返回一个key而不是一个完整的路径,如果我们此时只有请求数据后手动拼接吗?loadDataWithBaseUrl()为我们提供了解决方案,同样的看代码:

这里写图片描述

我们从网上找了一个图片,并将它的地址进行拆分模拟上诉的情况,然后在显示的内容中定义一个IMG标签显示我们的图片,把基地址传递给baseUrl,效果如下:

这里写图片描述

可以看到我们的图片正常显示出来了,那我们就来介绍下historyUrl,这个属性要想测试还得费不小的劲,我们假设一个这样的需求,当我们先通过loadDataWithBaseUrl()时,页面当中有一个A标签会跳转到其它的页面,那么我们返回页面时应该回到前一个我们加载的页面,可是真的如我们所想吗?我们来看一下代码的实现和效果:

这里写图片描述

注意:如果我们直接这样写的话,我们是不可能看到现象的,我们还需要做额外的两部操作,因为,当我们的在网页中点击一个链接的时候,WebView会去启动一个外部的程序来打开我们的页面,这是我们不愿意看到的,所以我们需要处理,还有就是当我们按返回按钮的时候,我们希望WbView当有可以后退的历史页时是返回上一页而不是直接退出程序,解决代码如下:

这里写图片描述

这里写图片描述

当实现了上面的步骤后,我以为成功了,可是事实又给了我一棒,当我返回的时候看到的不是我加载的页面,而是一个大白屏,这是什么鬼啊,查文档才发现第二种加载数据的方式不会缓存我们的历史记录,所以为我们提供了historyUrl的参数就是让我们返回时不至于看到大白屏,如果我们不指定这个参数的话,它会默认是”about:blank”,这就是为啥看到大白屏。

这里写图片描述

当我们为其添加一个参数过后,我们返回时就可以看到一个我们指定的页面了,所以,你可以将之前的加载页面进行存储,具体怎么操作你可以自己发挥。

3.LoadUrl():

这个方法太简单了,我们就不介绍了,参数传递一个我们想加载的合法url就行了,本地的也行,注意路径,比如assets下的文件路径就是:

file:///android_asset/  开头

4.loadUrl (String url, Map<String, String> additionalHttpHeaders)

这个方法和前一个方法类似,只是而外给我们提供了一种可以设置请求头的方式,当我们设置的请求头与默认的请求头冲突的时候,我们会覆盖掉默认的值。

说完了记载内容,我们再来看一看一些使用WebView时的常见设置:

1)mWebView.getSettings().setJavaScriptEnabled(false);   
设置了这个属性后我们才能在WebView里与我们的Js代码进行交互,对于WebApp是非常重要的,默认是false,因此我们需要设置为true
2)mWebView.getSettings().setSupportZoom(false);
     这个是设置我们的页面是否支持缩放,默认也是允许的。
3)mWebView.getSettings().setBuiltInZoomControls(false);
设置是否显示缩放控件,默认是false。
4)mWebView.getSettings().setDefaultFontSize(18);
设置默认的显示字体,默认是16,有效值是1-72之间。

在我们写布局文件的时候,有时我们为了让我们的WebView与屏幕的四周有一定的边距,我们通常会设置一个padding值,但是我们会发现这样其实并不能达到我们想要的效果,反而移动了我们滚动条的位置,此时我们一般会在Wbview的外层包裹一层布局来实现这样的效果,或则修改本地的CSS代码。
为了美观,我们还会设置我们滚动条的样式,通过设置:android:scrollbarStyle 为insideOverlay 或则outsideOverlay,或则在代码中设置:
这里写图片描述
这里写图片描述
有时在我们的程序中,我们需要为WebView设置一个我们自定义的背景而不现实默认的大白屏或则大黑屏,我们肯定会想到去设置WebView的背景:
这里写图片描述
可是事实是没有任何变化,WebView的背景还是原来的样子,那我们查询网络,发现有人说还需添加一个把硬件加速关闭的代码:
这里写图片描述
这样就好了吗?看来还是我们想多了,依旧是大白屏,难道没有办法了吗?最终我们通过在代码中写入如下代码:
这里写图片描述

问题最终得以解决,在我的手机上只需要第一行代码就可以达到效果了,加第二行代码是怕遇到一些奇葩的手机。

我们再来看之前遇到过的一个问题,在一个很简单的Activity中,这个Activity主要用来显示从网络请求后显示一些详情信息,可能返回的是富文本或者一个Html页面,具体的内容大小是不确定的,这个页面还有两个标题栏,一个主标题和一个副标题,他们的显示会根据实际的情况进行显示或则隐藏。

那我们看下出现的问题,在大多数的手机上都能正常的显示,而在一款性能较差的三星手机上就出现了问题,Webview的区域超过了布局文件中的区域,将标题栏的部分也进行了覆盖,导致一种标题栏显示不出来的现象,进过分析和定位,我们确定了问题的所在,主要有两方面的原因:

  • 1.整个页面的大小不确定,需要在代码中动态的控制控件的显示和隐藏,导致一些性能差的手机在画面的渲染上出现问题
  • 2.Webview显示的内容是网络请求的数据,有延迟也会对界面的绘制带来问题

    那我们就来看一下解决办法:
    这里写图片描述
    我们通过开启JavaScript接口进行调试,动态的设置我们的WebView的区域大小来解决View绘制时的问题,在这过程中我们还以先让Webview不可见,等数据加载成功后再让其显示。

接下来看看Webview中两种“客服端”

1.WebViewClient

它主要为我们提供了各种通知事件和请求事件,比如网页开始加载,网页加载结束,当然还有我们对链接的处理。

这里需要注意的地方就是如果我们不覆写shouldOverrideUrlLoading这个方法的话,我们点击一个链接的时候就会默认开启一个外部的浏览器加载网页,所以值得注意,还有就是当我们需要加载一个JavaScript命令的时候,我们需要在该客服端的onPageFinished方法中去执行。

2.WebChromeClient

它主要用于一些Web页面中的弹出事件传递给我们Native部分,让我们可以使用自定义的形式进行显示
这里写图片描述
上图中就是一个处理Alert事件的回调,让我们用一个Toast显示,值得注意的一点是,当我们忘记设置result.confirm()的时候,我们发现Toast后我们的Web页面无法滑动了,而且还会出现该回调函数只会执行一次等异常错误的出现,所以得多加留意。

WebView与JS交互

我们都知道我们的Native是可以和web页面中的JS进行交互的,而且方法都差不多,那我们就来看看他们的差别和使用场景吧

首先我们必须设置
这里写图片描述
设置后,我们会得到如下的提示,说会有危险,我们后面会介绍一种办法
这里写图片描述

  1. Webview里面的界面调用Native Java代码
    首先我们需要定义一个类,在这个类中定义一些方法供我们的Js调用:
    这里写图片描述
    值得注意的是,在Android4.2开始,我们必须田间@JavascriptInterface注解,我们的代码才能够被Js所调用。
    然后我们就需要把这个接口类传递给Webview,然Js可以调用:
    这里写图片描述
    这个方法的第二个参数是任意一个String类型的值,这个字会关系到我们后面调用接口中的方法。
    下面就是我在Html页面中定义了一个Button,让点击的时候调用Native的Toast:
    这里写图片描述
    然后就是按钮,我们绑定了它的点击事件:
    这里写图片描述

2.Native调用Webview中的Js
还记得我们前面讲过的WebViewClient吗?我们这里就需要先实现它,然后在它的onPagerFinish()回调函数中进行处理,这样是为了保证我们代码的正常运行,因为在以往的时候,当我们没有在这个回调当中执行操作,而是直接在LoadUrl()后就对页面进行操作,结果发现我们的代码没有起作用,最后发现是页面没有加载完成,而我们就去执行代码,所以造成了这种没必要的操作。
为了测试Native调用Js,我们现在Html页面中定义一个方法和一个Span标签:
这里写图片描述
在方法中我们让调用show()方法后,将Span标签里面的内容改为一个字符串,并弹出一个alert。
在Native中我们通过如下的LoadUrl()的方法调用这个函数:
这里写图片描述
就是这么简单,我们就实现了Native到Js的调用,但是我们用一个需求,我们向得到一个回调怎么办了?Google在Android4.4为我们新增加了一个新方法,这个方法比loadUrl方便,而且比loadUrl效率更高,因为每次load都会将页面刷新一次。

由于新增加的方法是4.4才引入的,所以我们使用的时候需要添加版本的判断:
这里写图片描述
如果是4.4之前的老版本,我们才用loadUrl的方式:
这里写图片描述

3.我们在开始的时候贴出了一个我们使用setJavaScriptEnable()的警告,那我们怎么来处理这个问题了,对于大多数的应用其实是不需要的,如果需要我们可以使用一种安全的方式,例如WebChromeClient中有一个onJsPrompt的回调方法,我们可以将我们需要传递的js命令转换为一种协议的模式,通过这个回调函数传递到我们的Native中,这样就避免了我们直接与代码交互,具体的过程在这里就不介绍了,下面提出一个该框架的github地址,大家需要的时候可以去下载:

https://github.com/pedant/safe-java-js-webview-bridge

开发WebApp

上面我们介绍了一些关于WebView这个控件的一些基本的使用和一些设置,这是我们卡发一个WebApp最重要的部分,因为我们的WebApp需要运行在它之上,接下来我们就正式开始介绍WebApp。

首先我们看一下WebApp的分类:

1.混合App,页面在服务端,只需要很少的Native交互,这种形式的App很大的优势就是当Web部分有更改的时候,不需要进行Apk的版本升级,直接在服务端就可以完成该工作,与我们Native部分交互很少,只需要为其提供一个页面进行显示,或则一些状态信息,不如账户信息。但是这种App有个缺点就是,这个模块只有在有网络的情况下才可以使用,而且非常的耗费流量。

2.混合App,页面在本地,这种类型的App从名字上看和上一种非常的想象,却别就是我们的页面和Js代码都在本地而不在服务端,这样做的好处就是我们在没有网络的情况下我们也可以进行访问,并且相率相对于前一种有所提高,我们一般会把这样的Web部分作为一个单独的模块,这样大大的降低了我们开发的速度和解决一些适配上的问题。

3.纯Web,这种模式也是现在很多公司比较青睐的,它的开发周期短,并且可以跨平台,实现了所谓的一次开发导出运行的效果,并且现在有非常多的前端框架可以选择,直接使用命令行就可以搭建起一个简单的框架,减少了很多的重复劳动。

接下来,我们就看一下我所知道的一些Web开发方法:

1.我们可以使用原始的HTML标签加上CSS样式来写我们的界面,这样虽然可以达到效果,但是工作量太大了,写一个简单的界面还好,要写一个复杂的界面的话会非常的麻烦。不推荐。
2.使用前端框架进行开发,例如我最近看的JqueryMobile,它为我们提供了许多的样式和组件,然我们只是写几个简单的属性就可以写出漂亮的界面来,而且还加入了Jquery。下面就附上一段页面的代码:
这里写图片描述
我们使用meta标签设置了页面的宽度为设备的宽度,并且不可以缩放,让他适合我们的手机屏幕,并且引入了三个开发JqueryMobile的资源,这下我们的前期工作就做完了,是不是觉得很方便,我们在看下具体的布局:
这里写图片描述
在JqueryMobile中的布局大致分为三个部分,Title、content、footer,我们可以通过定义三个

块将他们分为三个部分,然后在其中加入jqueryMobile给我们提供的属性,data-role=”header”、data-role=”content”、data-role=”footer”,值得注意的是,在header中的元素是会自动变为行内元素的,而且属性多以data开头等。

下面就来看一下上面的代码在手机中的展示效果:

这里写图片描述

看起来样子还不错,上面的红色部分其实是原生的两个TextView,很难看出差别了,如果你已经将开篇的三个资源都下载下来放入本地的assets文件下的话,我们即使在无网的情况下也能访问该页面,感觉很厉害的样子。

3.使用命令行的方式一键生成

这种方式相对于上一种方式前期可能要麻烦了一些,需要我们进行下载npm,然后还要配置一些环境变量才能使用,有时点背我们下载还总是失败,此时我们可以下载cnpm进行尝试,具体的下载过程和环境变量在这里我们就不在赘述了,网上的资源很多的

这里就提一下我们需要用到cordova和ionic,cordova是用于我们开发纯WebApp的时候帮助我们跨平台的调用系统原生的功能的。
看一看我们的大致步骤如下:

1.ionic start 在dos命令下输入如下指令创建一个我们指定的项目,其中 project-name 是我们指定的项目名,optional-template 有三个可选参数:sidemenu tabs blank 从名字上我们就能猜出大概的意思,第一个是创建一个基于侧滑菜单的模版,第二个是常见的底部tab模版,第三个就是空白页模版。

2.创建好工程后我们就cd 进入我们的工程目录,为工程添加运行的平台ionic platform add android

3.此时我们可以通过命令行打包我们的程序 ionic build android 也可以用我们的 Android Studio打开platform –》android 文件夹用ide进行编译打包
接下来我们就来看下工程的目录结构:

这里写图片描述

platforms 存放我们编译后的对应平台的工程
plugins 存放一些我们项目中用到的插件,例如上传图片啊,地图等
www 这个是我们开发的主要目录,我们看下它下面的结构:

这里写图片描述

css 存储项目中用到的css样式文件
Img 存储项目中的图片资源
Js 存储项目中的js代码
lib 存储一些库文件
templates 存储项目中的页面,模版

看完了目录结构,我们来看一下整个WebApp的大致架构, 我们可以发现www目录下有一个index.html文件,这个是我们程序的主页面,而且在应用中也是唯一存在的,为什么这么说,我们后面会介绍,我们来看一下它里面的内容:

这里写图片描述

这是我们看到的主页面,它定义了viewport和宽度,以及缩放比例,让网页适配我们的手机屏幕,接下来导入我们的ionic的资源和一些Js插件,然后我们看到26行的位置出现了一个ng-app的属性,这个属性是Angularjs中定义一个模块的指令,有点类似于我们Android开发中的Aplication,然后在30行的位置定义了一个的标签,只会让我们每一个主页面下的页面都有一个默认的标题栏,并且还有一个返回按钮。

39行的位置定义了一个的标签,这个就是为什么说我们的WebApp中只有一个Index.html的缘故,因为,以后的页面其实只是一个page,都显示在这个标签之内。

那么我们在随便找一个主页面中的子页面的代码来看看:

这里写图片描述

可以看到,我们的一个page页是通过ion-view标签包裹的,这个页面中通过设置属性hide-nav-bar=”true”将主页面中的Title给隐藏掉,我们可以自己定义一个样式通过
ion-header标签或则用div加ion提供的css class样式都可以实现,这种页面同样是 三层结构,分为头、内容、底部。

这只是一个页面,那如果我们页面不止一个的时候,我们需要页面之间的跳转怎么办?这是就需要用到一个组件 ui-Router,它的功能特别的强大,可以控制我们的页面跳转,配置页面是否缓存数据,配置页面跳转过程中需要传递的参数,它就是js目录下的app.js。我们接下来就看一看它里面的内容:

这里写图片描述

我们看到第8行的位置出现了一个starter,是不是很眼熟,其实我们之前见过,它就是index.html中第26行<body ng-app=”starter”>中定义的模块名,这就将我们的整个项目的Application建立起来了,这里就是对模块进行一些全局的配置,starter后面被[]包裹起来的部分就是starter模块依赖的模块或则控制器,或服务;我们在这里将其注入,以后我们就可以在starter的域中使用这些模块。这种依赖注入的方式有一个好处,只有当我们需要用到的时候它才回去加载,不会出现重复加载的情况。

接着往下:看看我们前面所说的页面之间跳转的部分:

我们的页面跳转需要依赖$stateProvider, $urlRouterProvider这两个模块,以$开头的都是Angularjs为我们提供的模块,因此以后我们自定义模块的时候不要以$开头。

这里写图片描述

这里定义了两个路由路径,在第二个中我们定义了不缓存和这个页面可以接收跳转到这个页面是传递的一个对象,其中有两个字符参数。templateUrl标明我们路由对应的页面,以后在controller中我们需要跳转时,我们可以通过注入$state实现:

这里写图片描述

这里写图片描述

这句就是配置我们的默认路由路径。

说了这么老半天,好像我们还是在页面这一层,那么我们是如何让我们的页面展示我们指定的数据并且处理逻辑了?接下来我们就说一下之前提到的新的框架AngularJS,不得不说这个框架的功能特别的强大。
它最大的特点就是数据的双向绑定,就是如果我们model层的数据发生变化后,我们不需要对页面去做额外的处理,框架就能通知我们的页面,让其随数据的改变而改变。这样就让我们将更多的精力放在我们的业务层,而且它也有类似MVC的结构,将model层和Controller层分开,是项目结构清晰,易于扩展。

这里写图片描述

上图是我们创建的模版中的一个模块,这个模块展示了一个列表的会话信息,那么我们就来看一看它的结构是如何实现的。

这里写图片描述

可以看到页面代码非常的简单,我们在<ion-content>中定义了一个<ion-list>一个列表,然后在<ion-item>标签中定义列表项,一个img用于展示头像等等,我们在页面中只看到了一个<ion-item>,可是页面中有五个,这就是AngularJs给我们提供的指令ng-reapeat的效果了,它是一个迭代器,chats是一个我们在Controller中定义的一个以
$scope.chats定义的数组。$scope是一个作用于对象,作用范围是当前的Controller和 它的子作用域。

遍历数据后我们就可以得到一个chat会话对象,我们在img标签中使用ng-src指令绑定我们的图片地址 {{}} 是Angularjs中的运算表达式,会计算当中的数据的值。并显示。我们也可以在标签属性中使用ng-bind来绑定数据。例如绑定名字我们也可以写成 <h2 ng-bind=”chat.name”></h2>

这里写图片描述

上图就是页面的控制器,名字叫ChatsCtrl,我们注入了一个AngularJs的$scope模块和一个Chats Server模块,将Chats模块中all()方法的返回值赋值给chats变量,就是我们程序中的进行遍历的chats。并定义了一个方法可以删除对应的会话。

这里写图片描述

上图就是我们Controller中依赖的Server模块,其中定义了一个jsonArry的东西,然后通过all()方法返回这个对象。我们Controller中就可以得到数据。

总结一下:

上面的过程其实很简单,可能我表述的不是很明白,我们一般在Controller中对数据进行操作,然后在Server中存储数据,和一些可以抽取出来的共通方法,例如访问网络,配置常量信息等数据。这样我们值要在Controller中改变了Chats的值后,页面中对应的选项就会对应的发生变化。这就是所说的双向绑定的概念,当然AngularJs的功能远不止如此,还有许多的强大功能,例如自定义指令标签等。

上面就是全部内容了,写这篇笔记时查询了很多网上的博客和文档,目的就是为了将WebView中的知识进行一个整理,以后需要的时候可以去查询,文中有错误的地方还望大家包含,多多真正,谢谢!



上帝制造人类的时候就把我们制造成不完美的人,我们一辈子努力的过程就是使自己变得更加完美的过程,我们的一切美德都来自于克服自身缺点的奋斗。