老早之前就想总结下Webview相关的知识点了,因为互联网大潮中,很多APP都会使用到Webview,像那些不计其数的电商APP,无一例外的使用Webview;或者一些非电商APP中的像广告页面,注册协议页面都会用到;最后因为一些事情拖到现在才做,感觉事情真不能拖,越往后推越做不了,罪过罪过。
怎么总结Webview呢
1.简单介绍
2.WebView/WebViewClient/WebChromeClient api介绍
3.简单使用
4.JS调用Android本地
5.Android调用JS方法
6.缓存处理及性能优化
7.webview使用注意点
webview系列文章
Android之WebView/WebViewClient/WebChromeClient简介 API详述 【一】
Android之WebView/WebViewClient/WebChromeClient 使用样例 【二】
Android之WebView Android调用JS方法 JS调用Android方法 【三】
Android之WebView 使用注意点 JS注入漏洞问题 内存优化【五】
6.缓存处理及性能优化
*.性能优化
现在不管什么样的APP,在里面嵌入H5页面已经是非常普通的了,感觉你的APP没有H5就OUT了这种,大家都这么热衷于H5的使用,肯定是有它的合理之处,有句话怎么说来着,存在即合理;能这么广泛的使用,很重要的一点得益于现在通信技术的发展,你很难想象在4G技术出来以前在手机APP里使用H5,那加载速度让你生不如死;当然现在手机硬件的提升也很重要。Webview的开发能让开发商用最低的成本实现Android,Ios,Web之间的复用,并且能很好的对功能及时作出更新,从长远眼光来看,这必然是发展的趋势。
对我们开发者来说,面对的问题肯定也是越来越多,很重要的一点就是在使用WebView的时候,它的性能问题。
1.当我们在使用WebView去加载页面的时候,很明显的能感受到它的加载速度比Native慢,也就是加载速度问题
2.加载网页的时候,里面引用的图片,js,css等全是通过网络下载的,也就是流量的消耗问题
影响Webview加载性能的原因有很多
1.webview的初始化
Android在4.4以前使用Webkit作为webview内核,之后做了优化以chromium替代,但是我们在使用的时候第一次打开Webview的时候,还是会感觉很慢,而且这种慢还是没有开始请求数据,仅仅是内核的启动初始化,这一块我们开发者要去修改这块,提高内核启动效率不太现实,尽管国内厂家也做了自己的优化,像腾讯的X5内核,相比于原生的有很大的提升,而且提供的功能更多,已经开放了。
但是我们如果就用原生内核怎么避免在使用webview加载页面出现过久初始化的情况呢。
第一种:建立一个全局的Webview,当App启动的时候,就在Application里初始化一个Webview,对,就是直接new;当需要用的时候就直接取这个单例形式的webview去加载网页,这样就把webview初始化的等待时间变得让用户感知不了;不过每次使用的时候需要清空上次使用时的页面内容。
使用这种方法会带来一些内存问题,这个放在后面讲述
第二种:上面这种方法是初始化webview-》加载网页-》请求数据,是串行的,阻塞的;如果我们不初始化全局webview,而是在使用的时候初始化webview,然后同时去请求网络数据到本地,并行处理,这样总体的加载时间就被缩短了。
2.页面加载
影响页面加载的原因也有很多,每次页面加载,都包含了很多的网络请求,有很多的资源需要下载,包括web页面本身的url请求,还有页面引用的图片,js,css,而且这些请求都是串行的;请求结束后还要进行渲染,这其中又涉及到JS的解析;如果每次都这么干,加载速度慢就不说了,光流量就是一个大的消耗,虽然现在运营商都推出了很多不限量套餐,我自己用的就是无限流量套餐,但是大部分用户还不是的。
不想每次都请求重复数据,那就涉及到缓存了。
*.Webview自带的缓存机制主要有以下几种:
1.浏览器缓存机制
2.APPlication Cache
3.Dom Storage
4.Web Sql Database
5.Indexed Database
6.File System
第一种:浏览器缓存机制
这个协议主要是前端同学设置,android客户端开发无须关心,原理是通过Http协议头部的Cache-Control/Expires,
Last-Modifed/Etag这几个字段去控制文件缓存,例如
Cache-Control:max-age=65265,这就表示要缓存65265s,如果在这个时间内再去请求这个文件,webview就不会发出请求,直接使用本地缓存的文件
Expires:Tue,12 Jun 2018 16:39:54 GMT,这表示文件到期时间是2018年6月12号16点39分54秒,在这个时间前都是使用本地缓存,不再去请求了
Last-Modified:Tue,12 Jun 2018 16:39:54 GMT,这表示文件最后的修改时间,在下次请求的时候,如果这个时间已经超过了上面两个字段的时间,就会作为If-Modified-Since字段的值,发送到服务器,服务器会跟Last-Modified比对,然后判断是否需要重新返回给客户端
ETag:"10df8c5f-110",这个属于文件的一个标志, 当请求的时候会赋值到If-None-Match字段,服务器会与最新的标志比较;如果与 Last-Modified同时出现,只要一个生效,就认为没有更新
这种缓存机制只要是正规的浏览器基本都支持,不过手机缓存的存储空间有限(在data/data/包名 目录下),随时可能被清除。
第二种:APPlication Cache
这种缓存机制可以说是对浏览器缓存机制的补充,原理相似,都是以文件为单位,有文件更新机制;这个机制需要前端设置,客户端也需要进行一些设置才能起作用。是一个专门为Web App离线使用的缓存机制,不过对于我们客户端开发官方已经不推荐使用了,
在APP上设置如下
//Android 私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录。 mSetting.setAppCachePath(MyApplication.getInstance().getCacheDir().getAbsolutePath()); //设置是否启用缓存,不过需要设置好缓存路径,默认false mSetting.setAppCacheEnabled(true);
在编写Html代码的时候需要指定manifest属性 ,这样页面就能使用app cache
一个完整的appcache文件包含3个section
cache manifest 下面的文件就是要缓存的文件,
network 下面的文件就是要加载的文件
fallback 下面的文件就是页面加载失败的时候显示的页面
第三种 Dom Storage
在APP上进行设置
//设置是否启用DOM存储 mSetting.setDomStorageEnabled(true);
这种机制分为两种,sessionStorage和localStorage
前者是临时性的,存储页面相关的数据,页面关闭后不能使用
后者是持久性的,就是在页面关闭后数据也能使用
这种机制就是替代cookies存储一些无须与服务器交流的数据,有点类似于SharedPreference,以key-value方法存储数据
第四种 Web Sql Database
在APP上进行设置
mSetting.setDatabaseEnabled(true); String dbPath = MyApplication.getInstance().getDir("db", Context.MODE_PRIVATE).getPath(); mSetting.setDatabasePath(dbPath);
这种方式官方已经不推荐使用了,后续版本不再维护
原理是基于SQL的数据库存储一些结构性的数据,可以方便对数据进行增删改查
第五种 Indexed Database
这种就是取代上面那种机制,是一种NoSql数据库,使用key-value的存储方式,相比于dom功能更强大,可以通过数据库的事务机制进行数据操作;存储空间更大,默认推荐250M,比dom的5M大的多了。比较适合复杂,大量的结构化数据存储
在app上只用设置支持js就自动打开了这种缓存机制。
mSetting.setJavaScriptEnabled(true);
第六种 File System
这种机制是H5新加入的存储机制,目前Android webview暂时不支持
综上所述
当我们存储静态资源文件,比如js,使用浏览器缓存、 APP Cache缓存
存储临时简单数据 使用dom storage
存储复杂大量数据 使用indexedDB
说完了缓存机制的使用,webview还有缓存模式的设置
//设置缓存模式 mSetting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
/** * Normal cache usage mode. Use with {@link #setCacheMode}. * * @deprecated This value is obsolete, as from API level * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and onwards it has the * same effect as {@link #LOAD_DEFAULT}. */ @Deprecated public static final int LOAD_NORMAL = 0; /** * Use cached resources when they are available, even if they have expired. * Otherwise load resources from the network. * Use with {@link #setCacheMode}. */ public static final int LOAD_CACHE_ELSE_NETWORK = 1; /** * Don't use the cache, load from the network. * Use with {@link #setCacheMode}. */ public static final int LOAD_NO_CACHE = 2; /** * Don't use the network, load from the cache. * Use with {@link #setCacheMode}. */ public static final int LOAD_CACHE_ONLY = 3;
这四种模式在前面文章有介绍。
*.预加载
上面这几种都是自带的缓存,但是都有一个前提是总要下载一次才能缓存,其实我们加载的页面中通常有很多东西是静态的,也就是长时间不会改变的,这些分两部分
1.整个H5页面在一段时间内都不会更改,比如一些广告页,公告页等
2.对于H5页面的一些js,css,图片等一部分文件一段时间内不会变化
对于第一种情况,我们可以在用户空闲时间直接把页面下载到本地保存,加载的时候取本地资源,这样速度也会更快
对于第二种情况,将一些静态文件下载下来,然后当页面进行加载的时候,拦截网络请求,判断本地有没有相同的资源,如果有,就加载本地的,就无需发请求到服务器,实现方式如下
/** * WebView 可以拦截某一次的 request 来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。 * API21加入 * @param view WebView * @param request 当前产生 request 请求 * @return WebResourceResponse */ @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { WebResourceResponse response; String url = request.getUrl().toString(); // 判断拦截资源的条件 if (url.endsWith(".png")) { response = getWebResourceResponse(url, "image/png", "png"); } else if (url.endsWith(".gif")) { response = getWebResourceResponse(url, "image/gif", "gif"); } else if (url.endsWith(".jpg")) { response = getWebResourceResponse(url, "image/jepg", "jpg"); } else if (url.endsWith(".jepg")) { response = getWebResourceResponse(url, "image/jepg", "jepg"); } else if (url.endsWith(".js") ) { response = getWebResourceResponse("text/javascript", "UTF-8", "js"); } else if (url.endsWith(".css") ) { response = getWebResourceResponse("text/css", "UTF-8", "css"); } else if (url.endsWith(".html") ) { response = getWebResourceResponse("text/html", "UTF-8", "html"); }else{ return super.shouldInterceptRequest(view, request); } if(response == null) return super.shouldInterceptRequest(view, request); return response; } private WebResourceResponse getWebResourceResponse(String url, String mime, String type) { WebResourceResponse response = null; /** * 图片资源的地址为:http://www.mango.com/imgage/logo.gif * 比如,有一个请求是要像服务器下载一张图片logo.gif,这个图片正好本地已经提前下载过了, * 那我们就读取本地资源, */ if (!TextUtils.isEmpty(url)) { FileInputStream fis = null; try { String path = Environment.getExternalStorageDirectory() + File.separator+resource +File.separator+type+File.separator; String name = url.substring(url.lastIndexOf("/")+1); File file = new File(path+name); if(!file.exists()){ return response; } fis = new FileInputStream(path+name); // 这里资源可以在assets目录下取 // is = MyApplication.getInstance().getAssets().open("images/abc.png"); /** * 参数1:http请求里该图片的Content-Type,此处图片为image/gif * 参数2:编码类型 * 参数3:存放着替换资源的输入流(上面创建的那个) */ response = new WebResourceResponse(mime, "utf-8", fis); } catch (IOException e) { e.printStackTrace(); }finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); fis = null; } } } return response; }
这个方法是webviewclient里的方法,我们自己写一个类去继承它,然后重写这个方法就行了,然后使用webview的set方法
mWebViewClient = new MyWebViewClient(); mWebview.setWebViewClient(mWebViewClient);
这样我们对页面加载的过程进行拦截,就能将一些文件替换成我们本地文件了。
我们提前下载的这些文件也是可以更新的,当手机处于wifi条件且空闲时,然后服务器主动把资源推送到本地进行增量更新,很多知名app都是这样做的。
上面这个重写的方法还可以做其它的事,比如修改请求连接,往链接里加一些标志位,服务端也可以通过标志位进行一些其它处理
public String addParams(String url) { if (!TextUtils.isEmpty(url) && !url.contains("change=") ){ if (url.contains("?")) { return url + "&change=1"; } else { return url + "?change=1"; } } else { return url; } } /** * WebView 可以拦截某一次的 request 来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。 * API21加入 * @param view WebView * @param request 当前产生 request 请求 * @return WebResourceResponse */ @Override public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) { String scheme = request.getUrl().getScheme().trim(); if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { return super.shouldInterceptRequest(view, new WebResourceRequest() { @Override public Uri getUrl() { return Uri.parse(addParams(request.getUrl().toString())); } @SuppressLint("NewApi") @Override public boolean isForMainFrame() { return request.isForMainFrame(); } @SuppressLint("NewApi") @Override public boolean hasGesture() { return request.hasGesture(); } @SuppressLint("NewApi") @Override public String getMethod() { return request.getMethod(); } @SuppressLint("NewApi") @Override public Map<String, String> getRequestHeaders() { return request.getRequestHeaders(); } }); } return super.shouldInterceptRequest(view, request);
在客户端进行这些优化处理之后,其实还可以在Html上面做优化:
1.一般我们在html文件的head里面单独引入css和内联js都不会阻塞html页面的解析,但是如果同时且先link css 然后加内联js,就会造成css的加载阻塞内联js执行,进而阻塞html解析。
所以编写html的时候要注意css的标签要靠前,css下面不要添加任何的内联js。请参照web页面加载优化建议
2.像React这样偏重的框架, 其中的js解析编译执行会占很多时间,在配置不是很高的手机上,很影响页面的渲染速度,所以在开发中一定要慎用,同时同一个app里在webview开发者方面尽量统一第三方框架,这样可以提高缓存的使用率
这里推荐一个美团知识分享网站