1.前言
最近几年关于原生WebView与H5混合开发的项目越来越多,这种开发带来了很多便利,但也会有一些缺点,比如说通过WebView加载H5会有一定的卡顿现象,会影响用户体验。下面本文就此问题一一展开讨论。
2. 场景
根据日常需求一般是通过webView.loadUrl()
方法加载指定的网页,其大概流程如下:
image.png
- 创建WebView:通过Java代码创建WebView,设置相关的参数和属性
- 发起请求:通过底层内核会先检查缓存,然后决定是否创建对应的请求,建立对应的http请求。
- 页面解析:下载对应的DOM树和相关资源。
- 内容处理:根据上面的资源和树形结构,进行渲染展示。
以上是整个网页请求的大概过程,优化也是在这几个过程中分别作出细致对应的方案。
2.1 创建WebView
WebView创建的时机会对整个流程是有影响的,Webview的启动相对来说比较耗时,因此这个时候我们可以采用提前启动Webview,这样就可以在加载网页的时候已经准备好。比如我在做Browser的时候发现有Native页面点击网页icon,启动相对来说比较慢,那么采用的策略就是进入Native页面后,一段时间后提前创建Webview。一开始本以为这样就可以,但是浏览器在tab页面可以新建多个网页加载器,也就是会有多个webview。显然光是提前创建也不太好,后面采用创建一个WebView的池子来管理这些Webview
/**
* webview 复用池
*/
public class WebViewPool {
private static final String DEMO_URL = "https://www.baidu.com";
private static final String APP_CACAHE_DIRNAME = "webCache";
private static List<WebView> available = new ArrayList<>();
private static List<WebView> inUse = new ArrayList<>();
private static final byte[] lock = new byte[]{};
private static int maxSize = 2;
private int currentSize = 0;
private static long startTimes = 0;
private static volatile WebViewPool instance = null;
public static WebViewPool getInstance() {
if (instance == null) {
synchronized (WebViewPool.class) {
if (instance == null) {
instance = new WebViewPool();
}
}
}
return instance;
}
/**
* Webview 初始化
* 最好放在application oncreate里
*/
public static void init(Context context) {
for (int i = 0; i < maxSize; i++) {
WebView webView = new WebView(context);
initWebSeting(context, webView);
//webView.loadUrl(DEMO_URL);
available.add(webView);
}
}
/**
* 获取webview
*/
public WebView getWebView(Context context) {
synchronized (lock) {
WebView webView;
if (available.size() > 0) {
webView = available.get(0);
available.remove(0);
currentSize++;
inUse.add(webView);
} else {
webView = new WebView(context);
initWebSeting(context, webView);
inUse.add(webView);
currentSize++;
}
return webView;
}
}
/**
* 回收webview ,不解绑
*
* @param webView 需要被回收的webview
*/
public void removeWebView(WebView webView) {
webView.loadUrl("");
webView.stopLoading();
webView.setWebChromeClient(null);
webView.setWebViewClient(null);
webView.clearCache(true);
webView.clearHistory();
synchronized (lock) {
inUse.remove(webView);
if (available.size() < maxSize) {
available.add(webView);
} else {
webView = null;
}
currentSize--;
}
}
/**
* 回收webview ,解绑
*
* @param webView 需要被回收的webview
*/
public void removeWebView(ViewGroup viewGroup, WebView webView) {
viewGroup.removeView(webView);
webView.loadUrl("");
webView.stopLoading();
webView.setWebChromeClient(null);
webView.setWebViewClient(null);
webView.clearCache(true);
webView.clearHistory();
synchronized (lock) {
inUse.remove(webView);
if (available.size() < maxSize) {
available.add(webView);
} else {
webView = null;
}
currentSize--;
}
}
/**
* 设置webview池个数
*
* @param size webview池个数
*/
public void setMaxPoolSize(int size) {
synchronized (lock) {
maxSize = size;
}
}
private static void initWebSeting(Context context, WebView webView) {
ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
webView.setLayoutParams(params);
WebSettings webSettings = webView.getSettings();
//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
webSettings.setJavaScriptEnabled(true);
// 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
//支持插件
// webSettings.setPluginsEnabled(true);
//设置自适应屏幕,两者合用
webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小
webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小
//缩放操作
webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。
webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件
//其他细节操作
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); //关闭webview中缓存
webSettings.setAllowFileAccess(true); //设置可以访问文件
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
webSettings.setDatabaseEnabled(true); //开启 database storage API 功能
webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能
String cacheDirPath = context.getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置 Application Caches 缓存目录
//页面白屏问题
webView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent));
webView.setBackgroundResource(R.color.white);
}
}
需要Webview的时候直接在此pool中取即可。
2.2 发起请求
在Webview真正发起请求的之前,Webview会进行缓存检查,而对于Webview来说可以可以设置缓存webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
在DOM树和相关图片资源下载的时候,这个其实是相对来说比较耗时的,那么一般有以下几个优化的思路
- DOM树形成固定的模板,通过http请求出具体数据,填充到模板
- 资源拦截:通过
shouldInterceptRequest
拦截一些图片和css资源,通过本地返回。 - 图片加载: 在Webview进行初始化的时候设置默认不可以加载图片,在
pagefinished
方法中设置图片自动加载。
2.3 内容处理
关于内容处理这个其实需要服务器配合,简单来说当用户通过WebView来加载某个网页A,此时网页会出现一些内容,当用户点击此网页中内容的时候会进入下一页B。那么在用户刚进入A的时候,此时服务器是可以预知到下一个页面B的内容,因为B的链接是知道的,那么此时服务器可以通过预加载
的方式请求到相关内容,内容回来之后无论是置于CDN还是推到客户端都可以随业务来定,这样相对来说是会提高加载速度的。
总结
整体来说WebView的加载优化大概以上几个方向:
- 活用缓存
- 提前加载
- 尝试拦截
这是Webview这块可以处理的,当然如果可以自己处理内核,那么可以优化的空间会更多,在此就不做展开。