从本篇博客可以学到什么?
1. 用WebView构建页面
2. 优化WebView的加载
3. 成型的WebView优化加载方案,crosswalk
4. hybrid app混合开发,常用框架
WebView提供了自定义的方式 让我们在Android App中显示web内容时,响应用户的行为,比如在web页面点击,跳转url等
WebChromeClient
WebViewClient 比如可以通过shouldOverrideUrlLoading( )来中断URL的载入(为什么要中断?有的机型会默认跳转浏览器去加载URL,而不是在我们自定义的WebView中展示网页)
WebSettings,比如常用的通过setJavaScriptEnabled( )设置 JavaSrcipt脚本可用
调用WebView对象的addJavascriptInterface(object ,String )接口,这个接口允许js代码调用Android的代码,
了解上述四大特性后,通过一个小demo来熟悉
WebView和普通的View一样,可以通过代码静态加载
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/webview" android:layout_width="fill_parent" android:layout_height="fill_parent" />
也可以通过new WebView()创建一个对象的形式加载。
WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("博客地址");
比如通过WebView打开我的博客:
预览图
<manifest ... >
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
很多人可能遇到loadUrl载入地址的时候,App打开外部浏览器来显示页面的bug,接下来会告诉你怎么做!
关于什么是JavaScript 请看 WC3的官方介绍 JavaScript
WebView默认不支持JavaScript,我们可以通过WebSettings的setJavaSriptEnabled()来设置它可用
WebSettings 如何获得?答:mWebView.getSettings();
例如:
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
通过WebView开发Android App的时候,你可以创建interface来服务于JavaScript脚本代码调用Android代码。
例如,通过JavaScript代码去调用Android的代码,向用户展示一个Dialog,我知道你是大牛,了解JavaScript的alert()函数也可以完成类似的功能。但这里只是作为一个引入的例子,通过它我们来了解JavaScript和Android 是如何交互的
首先自定义一个Object对象,任意名称即可
public class WebAppInterface {
Context mContext;
/** Instantiate the interface and set the context */
WebAppInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
注意@JavascriptInterface注解,当版本高于17的时候,对应Android4.2以上的系统级别,一定要使用JavascriptInterface注解,否则JS代码不识别Android的本地代码,
创建完毕对象,我们就要把这玩意传递给WebView了,如何传呢?
答案是通过WebView的addJavascriptInterface()方法
WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");
呃,光这么做,可能还不够,我们还缺少测试的javascript代码
在Android Studio环境下:
src-main->新建assets文件夹->新建test.html 文件
代码如下:
<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />
<script type="text/javascript"> function showAndroidToast(toast) { Android.showToast(toast); } </script>
等等,我们该如何引用assets目录下的文件呢?(忽略e的命名)
答:file://android_asset/xx.html
mWebView.loadUrl("file:///android_asset/e.html");
这一步有的同学可能出错
出错原因无非是两个:
确定assets目录名称和摆放位置是否正确
确定是否正确引入到webView中
这么快就OK了吗?是不是已经迫不及待来看它的效果啦:
嗯,满足你:
通常浏览器不可能只展示一个页面,比如页面前进,页面后退的功能也是要有的,android也提供了WebViewClient来满足商户需求
WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.setWebViewClient(new WebViewClient());
WebViewClient是系统默认的组件
当日我们也可以自定义WebViewClient
比如:
private class MyWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (Uri.parse(url).getHost().equals("www.example.com")) {
// This is my web site, so do not override; let my WebView load the page
return false;
}
// Otherwise, the link is not for a page on my site, so launch another Activity that handles URLs
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}
}
shouldOverrideUrlLoading()就是用来解决 有的系统版本 会默认打开外部浏览器 载入页面。
接着创建webView实例引用WebViewClient实例
WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.setWebViewClient(new MyWebViewClient());
最后就是在activity控制层 来处理WebView的跳转逻辑
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Check if the key event was the Back button and if there's history
if ((keyCode == KeyEvent.KEYCODE_BACK) && myWebView.canGoBack()) {
myWebView.goBack();
return true;
}
// If it wasn't the Back key or there's no web page history, bubble up to the default
// system behavior (probably exit the activity)
return super.onKeyDown(keyCode, event);
}
Activity.java 类
public class MainActivity extends AppCompatActivity {
private MyWebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mWebView = (MyWebView) findViewById(R.id.my_webview);
// mWebView.loadUrl("http://www.taobao.com");
mWebView.loadUrl("file:///android_asset/e.html");
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new WebAppInterface(getApplicationContext()),"Hello");
mWebView.setWebViewClient(new MyWebClient());
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode==KeyEvent.KEYCODE_BACK&&mWebView.canGoBack()){
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
}
MyWebClient类,指定页面加载的逻辑
public class MyWebClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.i("onPageFinished","onPageFinished");
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.i("onPageStarted","onPageStarted");
}
@Override
public void onLoadResource(WebView view, String url) {
Log.i("onLoadResource","onLoadResource");
super.onLoadResource(view, url);
}
}
WebAppInterface类,用来和Javascript交互
public class WebAppInterface {
Context mContext;
/** Instantiate the interface and set the context */
WebAppInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
e.html 文件,模拟服务器的jsp数据
<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />
<script type="text/javascript"> function showAndroidToast(toast) { Hello.showToast(toast); } </script>
为了能够更好的使用WebView展示出流畅的的页面,可以从以下几点做优化:
开启WebView的缓存功能可以减少对服务器资源的请求,一般使用默认缓存策略就可以了。
//设置 缓存模式
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
// 开启 DOM storage API 功能
webView.getSettings().setDomStorageEnabled(true);
资源等文件(不需要更新)本地存储,在需要的时候直接从本地获取。哪些资源需要我们去存储在本地呢,当然是一些不会被更新的资源,例如图片文件,js文件,css文件,替换的方法也很简单,重写WebView的方法即可。
{
try {
if (url.endsWith("icon.png")) {
InputStream is = appRm.getInputStream(R.drawable.icon);
WebResourceResponse response = new WebResourceResponse("image/png",
"utf-8", is);
return response;
} elseif (url.endsWith("jquery.min.js")) {
InputStream is = appRm.getInputStream(R.raw.jquery_min_js);
WebResourceResponse response = new WebResourceResponse("text/javascript",
"utf-8", is);
return response;
}
} catch (IOException e) {
e.printStackTrace();
}
returnsuper.shouldInterceptRequest(view, url);
}
appRm为app资源管理器,读取drawable,assets,raw下的资源,都是android系统的一些很简单的函数调用。
getInputStream的参数代表资源具体位置
WebResourceResponse后的资源类型需要写正确
有些时候我们会为我们的网站加入一些统计代码,这些也可以精简掉(自己使用的CNZZ的大概占的有10k左右),可以使用Charles对客户端进行抓包查看。
准确的说,是减少同步操作的操作时间,尽量使用异步操作替代同步操作。如果服务端存在读取数据库和计算耗时的操作,尽量使用异步(ajax)进行操作,把原本的时间花在异步操作上。
举个例子,A页面到B页面,A页面实现登录功能,B页面展示主功能页面,如果让B页面去进行用户登录信息验证的话,B页面加载时间会加长(数据库查询等操作),同时客户端可能需要提供一个等待框(或进度条等)给用户,那看看在A页面使用异步操作的优势吧:
可以提供统一的js等待框,多平台保持一致性,减少客户端代码工作量。
加载页面的时间变短。B页面由于减少了耗时的操作,加载时间变短,用户等待时间也变短。
可以方便加入一些验证后的控制逻辑,不需要进行页面跳转。A页面可以根据异步操作进行结果判断,做出相应的处理。
怎么让用户看不到WebView加载前的白色页面呢?首次加载后页面的跳转可以用上面的步骤进行优化,可以提供给用户一个很好的体验,那加载的第一页呢?我们需要WebView预加载页面,这个该怎么做到的呢?下面提供两种方法:
ViewPager,将欢迎页面与WebView页面一起放进ViewPager中,设置预加载页面个数,使WebView所在页面可以预加载,在加载完毕的时候切换到WebView所在页面。
FrameLayout,将欢迎页面与WebView页面的布局合在一起,显示在一个页面内,起始隐藏WebView布局,待WebView加载完毕,隐藏欢迎布局,显示WebView布局。
使用FrameLayout简单一些,两种方法都是需要对WebChromeClient的onProgressChanged进行监听,加载完毕进行页面切换,如下:
webView.setWebChromeClient(new WebChromeClient() {
@OverridepublicvoidonProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
if (newProgress >= 100) {
// 切换页面
}
}
});
默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。
故在WebView初始化时设置如下代码:
public void int () {
if(Build.VERSION.SDK_INT >= 19) {
webView.getSettings().setLoadsImagesAutomatically(true);
} else {
webView.getSettings().setLoadsImagesAutomatically(false);
}
}
同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:
@Override
public void onPageFinished(WebView view, String url) {
if(!webView.getSettings().getLoadsImagesAutomatically()) {
webView.getSettings().setLoadsImagesAutomatically(true);
}
}
从上面的代码,可以看出我们对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。
当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示一个卖萌的出错界面。但我们怎么能让用户发现原来我使用的是网页应用呢,我们期望的是用户在网页上得到是如原生般应用的体验,那就先要从干掉这个默认出错页面开始。当WebView加载出错时,我们会在WebViewClient实例中的onReceivedError()方法接收到错误,我们就在这里做些手脚:
@Override
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mErrorFrame.setVisibility(View.VISIBLE);
}
从上面可以看出,我们先使用loadDataWithBaseURL清除掉默认错误页内容,再让我们自定义的View得到显示(mErrorFrame为蒙在WebView之上的一个LinearLayout布局,默认为View.GONE)。
同样我们在做上拉加载下一页这样的功能时,也需要知道当前页面滚动条所处的状态,如果快到底部,则要发起网络请求数据更新网页。同样继承WebView类,在子类覆盖onScrollChanged方法,具体如下:
@Override
protected void onScrollChanged(int newX, int newY, int oldX, int oldY) {
super.onScrollChanged(newX, newY, oldX, oldY);
if (newY != oldY) {
float contentHeight = getContentHeight() * getScale();
// 当前内容高度下从未触发过, 浏览器存在滚动条且滑动到将抵底部位置
if (mCurrContentHeight != contentHeight && newY > 0 && contentHeight <= newY + getHeight() + mThreshold) {
// TODO Something...
mCurrContentHeight = contentHeight;
}
}
}
上面mCurrContentHeight用于记录上次触发时的网页高度,用来防止在网页总高度未发生变化而目标区域发生连续滚动时会多次触发TODO,mThreshold是一个阈值,当页面底部距离滚动条底部的高度差<=这个值时会触发TODO。
当我们在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用 file:///android_asset/xxx.png 的链接来引用包里面assets下的资源了。示例如下:
private void loadWithAccessLocal(final String htmlUrl) {
new Thread(new Runnable() {
public void run() {
try {
final String htmlStr = NetService.fetchHtml(htmlUrl);
if (htmlStr != null) {
TaskExecutor.runTaskOnUiThread(new Runnable() {
@Override
public void run() {
loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", "");
}
});
return;
}
} catch (Exception e) {
Log.e("Exception:" + e.getMessage());
}
TaskExecutor.runTaskOnUiThread(new Runnable() {
@Override
public void run() {
onPageLoadedError(-1, "fetch html failed");
}
});
}
}).start();
}
上面有几点需要注意:
如果你的多个WebView是放在ViewPager里一个个加载出来的,那么就会遇到这样的问题。ViewPager首屏WebView的创建是在前台,点击时没有问题;而其他非首屏的WebView是在后台创建,滑动到它后点击页面会出现如下错误日志:
20955-20968/xx.xxx.xxx E/webcoreglue﹕ Should not happen: no rect-based-test nodes found
解决办法:继承WebView类,在子类覆盖onTouchEvent方法,填入如下代码:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY());
}
return super.onTouchEvent(ev);
}
该方法的最先提出在。WebView in ViewPager not receive user inputs
4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启,代码如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
使用开源项目Safe Java-JS WebView Bridge可以很好替代addJavaScriptInterface方法,同时增加了异步回调等支持,并且不存在了安全风险。
在开发过程中你可能会遇到这样一种情况。端里面使用ViewPager嵌套了多个WebView页面,同时某一个WebView中的页面元素需要响应TouchMove事件
这时你就会发现上层(ViewPager)阻断了下层(WebView)接收TouchMove事件,即使你的WebView在TouchDown时返回true也无效,因为上层直接使用了onInterceptTouchEvent对后续的TouchMove进行了拦截。针对这个问题的解决,简单做法是在重写WebView onTouchEvent方法,如下
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean ret = super.onTouchEvent(ev);
if (mPreventParentTouch) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
ret = true;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
requestDisallowInterceptTouchEvent(false);
mPreventParentTouch = false;
break;
}
}
return ret;
}
public void preventParentTouchEvent () {
mPreventParentTouch = true;
}
代码控制的关键在于mPreventParentTouch这个变量,mPreventParentTouch默认为false,当用户touchdown页面元素时通知该WebView将mPreventParentTouch设置为true。示意代码如下:
<script type="text/javascript"> document.getElementById("targetEle").addEventListener("touchstart", function(ev) { HostApp.preventParentTouchEvent(); // 通知WebView阻止祖先对其Touch事件的拦截 } ); document.getElementById("targetEle").addEventListener("touchmove", function(ev) { // todo something on this page } ); </script>
刚提到了上面是一种简单的做法,并不能很好的解决手指滑动过快带来的误操作问题,即当用户快速地滑动时,还是有一定机率会出现ViewPager拦截TouchMove事件而发生了Tab切换而非页面元素做出了响应。要完美解决此问题,就要用到稍微复杂一点的方法(仅是整体消息传递流程复杂一点)。
首先假设在ViewPager之上还有一个父元素叫做ParentViewOnViewPager,当我们接收到页面preventParentTouchEvent通知时就先于ViewPager而进行拦截。如下:
public class ParentViewOnViewPager extends FrameLayout {
private MineWebView mDispatchWebView;
public void preventParentTouchEvent (WebView view) {
mDispatchWebView = (MineWebView)view;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE && mDispatchWebView != null) {
mDispatchWebView.ignoreTouchCancel(true);
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mDispatchWebView != null){
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
mDispatchWebView.onTouchEvent(ev);
break;
default:
mDispatchWebView.ignoreTouchCancel(false);
mDispatchWebView.onTouchEvent(ev);
mDispatchWebView = null;
break;
}
return true;
}
return super.onTouchEvent(ev);
}
}
即当ParentViewOnViewPager接收到通知时,发起TouchEvent拦截,将拦截到的Touch事件转嫁到装载页面的mDispatchWebView进行事件派发。这样就直接跳过了ViewPager这一层。这里需要注意的是当ParentViewOnViewPager发起拦截时,WebView会接收到一个TouchCancel事件,WebView应该忽略这个事件,以避免页面接收到这个事件而打断整个处理流程。如下代码所示:
public class MineWebView extends WebView {
boolean mIgnoreTouchCancel;
public void ignoreTouchCancel (boolean val) {
mIgnoreTouchCancel = val;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return ev.getAction() == MotionEvent.ACTION_CANCEL && mIgnoreTouchCancel || super.onTouchEvent(ev);
}
}
就是想办法让浏览器延迟加载JS脚本,但是Android的WebView控件没有这样的参数。无法单独阻塞JS脚本,另外有个setBlockNetworkLoads,用了之后也无法实现类似图片的异步载入的功能,页面成了光板,连CSS也阻塞了。
这个问题困扰了很久,直到在做HTML照片墙时,由于setBlockNetworkImage在OnPageFinished之后才会释放,导致在JS脚本载入图片过程中无法获取图片的高宽信息,最后巧妙地通过$(document).ready(function()
{setTimeout(func,10)});,成功将函数在onPageFinished之后运行。那么延伸来想,是否可以将JS脚本也用同样的方式延迟载入呢?答案是肯定的,在http://wonko.com/post/painless_javascript_lazy_loading_with_lazyload可以找到JS脚本延迟加载的第三方组件。
我改造了之前速度奇慢的界面,我所使用的核心JS代码如下:
<script src="/css/j/lazyload-min.js" type="text/javascript"></script> <script type="text/javascript" charset="utf-8"> loadComplete(){ //instead of document.read() } function loadscript() { LazyLoad.loadOnce([ '/css/j/jquery-1.6.2.min.js', '/css/j/flow/jquery.flow.1.1.min.js', '/css/j/min.js?v=2011100852' ], loadComplete); } setTimeout(loadscript,10); </script>
就是混搭setTimeout和layzload,让JS脚本可以真正在onPageFinish之后执行。
经过以上几步的优化,一个流畅的webapp生成了。
crosswalk官网
Crosswalk是一款为HTML应用提供运行时环境的开源项目,同时它也扩展了一些Web平台的新特性。
Web平台已经拥有很多优势,例如从简单的云服务集成到灵活的用户界面元素。当前随其对移动性能和设备API持续增长的关注,Web平台也越来越具有吸引力。
然而对于很多开发者而言,由于基础功能仍然缺失,导致时至今日采用Web平台依旧困难重重。
使用Crosswalk项目,可以改变这种情况
通过使用Crosswalk项目,应用开发人员可以:
- 使用所有现代浏览器可提供的特性:HTML5,CSS3,JavaScript。
- 访问主流和新兴的Web标准。
- 使用主流浏览器无法获取的实验性API。
- 通过部署自己的运行时环境来控制应用的升级周期。
- 通过为应用添加自定义扩展,来使用并未通过Crosswalk或公共Web标准暴露的系统平台功能
crosswalk扩展阅读
放弃WebView,使用Crosswalk做富文本编辑器
一篇文章读懂开源web引擎Crosswalk
百度百科关于hybrid app的介绍
Hybrid App:Hybrid App is a mobile application that is coded in both browser-supported language and computer language. They are available through application distribution platforms such as the Apple App Store, Google Play etc. Usually, they are downloaded from the platform to a target device, such as iPhone, Android phone or Windows Phone. The subscribers need to install to run them.
我们来拆解一下里面的含义:
1、mobile application:Hybrid App就是一个移动应用
2、both browser-supported language and computer language:同时使用网页语言与程序语言编写
3、available through application distribution platforms:通过应用商店进行分发
4、a target device:区分目标平台
5、install to run:用户需要安装使用综合一下就是:“Hybrid App同时使用网页语言与程序语言开发,通过应用商店区分移动操作系统分发,用户需要安装使用的移动应用”。总体特性更接近Native App但是和Web App区别较大。只是因为同时使用了网页语言编码,所以开发成本和难度比Native App要小很多。因此说,Hybrid App兼具了Native App的所有优势,也兼具了Web App使用HTML5跨平台开发低成本的优势
hybrid扩展阅读:
浅谈Hybrid技术的设计与实现
点评:前携程员工,经历过最初的hybrid开发,后加入百度,学习到完整的hybrid app开发流程
Android developer Web Apps课程 只翻译了一部分
还有Migrating to WebView in Android 4.4 等重要章节没有翻译,日后补充上来!
关于JavaScript 来自WC3的官方介绍
demo链接
WebView开发问题汇总
WebView究极优化
Web加载速度究极优化方案
Google Android developer Web Apps 课程