Android开发-WebView的缓存处理和性能优化 实现H5页面秒开【四】

前言

老早之前就想总结下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 缓存处理 性能优化【四】

Android之WebView 使用注意点 JS注入漏洞问题 内存优化【五】

 

6.性能优化及缓存处理

现在不管什么样的APP,在里面嵌入H5页面已经是非常普通的了,感觉你的APP没有H5就OUT了这种,大家都这么热衷于H5的使用,肯定是有它的合理之处,有句话怎么说来着,存在即合理;能这么广泛的使用,很重要的一点得益于现在通信技术的发展,你很难想象在4G技术出来以前在手机APP里使用H5,那加载速度让你生不如死;当然现在手机硬件的提升也很重要。Webview的开发能让开发商用最低的成本实现Android,Ios,Web之间的复用,并且能很好的对功能及时作出更新,从长远眼光来看,这必然是发展的趋势。

对我们开发者来说,面对的问题肯定也是越来越多,很重要的一点就是在使用WebView的时候它的加载速度和流量消耗问题。

1.当我们在使用WebView去加载页面的时候,很明显的能感受到它的加载速度比Native慢,也就是加载速度问题;一个很明显的现象就是启动H5页面会有一长段白屏时间

2.加载网页的时候,里面引用的图片,js,css等全是通过网络下载的,也就是流量的消耗问题

接下来就针对加载速度和流量消耗来谈谈,其实这两点都可以从缓存的使用出发,合理的使用缓存可以避免频繁的进行网络请求,加快响应速度;同时网络请求少了,流量消耗自然就低了;当然了还有一些其它优化点,往下看


缓存使用

打开一个H5页面,通常会经历如下过程

初始化WebView --- 请求页面 -- 下载数据 -- 解析HTML -- 请求js/css资源 -- dom渲染 -- 解析JS并执行 -- JS请求数据 -- 解析渲染 -- 下载渲染图片

一些简单的H5页面可能没有后面JS请求数据这一步,比如一些展示性的页面,广告页等;但是大部分跟用户交互的页面应该是有的,比如根据用户登录信息,JS发送REST请求给服务器,再做相应的渲染

H5页面在dom渲染这一步就能基本显示出来或者部分显示了,在这之前用户看到的都是白屏,等到图片等资源下载渲染完后整个页面才完整显示,

所以想做到首屏秒开就是要减少这个阶段所花费的时间,优化分为两部分:前端优化和客户端优化

前端优化

这里可以优化的点有:

  • 使用gzip对资源进行压缩,降低网络传输量
  • 预解析DNS,减少域名数量,加快请求速度
  • 使用HTTP缓存机制,减少网络请求
  • 优化HTML内部JS/CSS加载顺序,减少渲染时间

这里面对页面启动速度影响最大的就是网络请求了,所以优化的重点就是合理利用HTTP缓存机制,具体可以参考OKHttp3-- HTTP缓存机制解析,这里说几点

  • Expires:该字段是存在于服务器返回的响应头中,目的是告诉浏览器该资源的过期时间;也就是说当浏览器再次请求的时候如果当前时间早于这个过期时间,那么就不需要请求了,直接使用缓存;如果晚于这个时间,那么再向浏览器请求数据;该字段存在于HTTP/1.0中
  • Cache-control:它是当前浏览器缓存中非常重要的一个字段,作用与Expires差不多,存在于响应头,都是标注当前资源的有效期;但是它有很多的值,可以指定较为复杂的缓存规则,如果与Expires同时存在,Cache-control的优先级高
  • Etag:服务器在响应客户端请求时,会在响应头带上该字段;它表示该资源在服务器中的唯一标识,生成规则由服务器决定,在Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的
  • If-None-Match:这是在请求头中的字段,值就是Etag的值;当客户端判断资源过期时(通常使用Cache-Control标识的max-age),如果发现缓存的响应有Etag头部声明,那再次向服务器请求时带上If-None-Match头部,值就是Etag的值,web服务器收到请求后发现有If-None-Match头,就将其与存在服务端的Etag值进行比较;如果匹配,说明该资源没有修改,那就返回304,告诉客户端可以继续使用缓存;如果不匹配,说明资源修改过,那就返回200,重新响应该资源给客户端
  • Last-Modified:标识资源在服务器上的最后修改时间,随着响应头带给客户端
  • If-Modified-Since:这是在请求头中的字段,值就是Last-Modified的值;当客户端判断资源过期时,同时缓存的响应头没有Etag声明,如果发现头部有Last-Modified声明,则再次向服务器请求资源时,在请求头带上 If-Modified-Since头部,值就是Last-Modified的值;服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源被改动过,则响应整片资源给客户端,响应码是 200;若最后修改时间较旧,说明资源无修改,则响应304 ,告知浏览器继续使用缓存

这样前端能做到的缓存策略就是:HTML文件根据HTTP缓存机制向服务器询问是否需要更新,JS/CSS/Image资源文件不请求更新,直接使用缓存;不过服务端对资源文件需要给定一个版本号或者hash值并生成对应的资源url,只要资源文件有更新,url肯定就会变化,那对应的HTML就需要更新,这样就会请求新的资源URL,资源就会自动更新了;至于其它的一些数据,比如json数据,可以使用LocalStorage缓存,这在JS里可以控制

客户端优化

接下来轮到在Android APP上进行优化了,继续说缓存,在客户端拥有更加自由的缓存策略,因为客户端可以拦截H5页面所有的网络请求,由自己管理缓存:在客户端拦截网络请求,首次请求HTML文件数据后缓存下来,后面直接使用缓存;然后在后台发起请求询问服务器是否更新缓存

这种策略就是HTML文件在用客户端的缓存,至于其余资源沿用上面讲的前端缓存方式,这样一个H5页面第二次访问从HTML到JS/CSS/Image等资源都是使用缓存,无需等待网络请求,大大提升启动速度

离线包

上面的方案是解决H5后续使用加载速度问题,但是第一次打开H5页面,本地没有缓存,这样所有数据都要从网络下载,体验还是很差;这时候就可以使用离线包处理

  • 假如我们的业务场景是整个H5模块,那么后端同学可以使用构建工具把整个H5模块的所有相关页面和资源打包成一个压缩包,同时对文件进行加密处理,避免被运行商和第三方劫持篡改,这个压缩包就是离线包
  • 客户端可以在APP启动或者某个时机默默的下载整个离线包,做解压解密操作
  • 打开某个H5页面时根据配置清单打开离线包里面的入口页面;同时Webview可以拦截所有的网络请求,对于存在离线包里的文件,直接读取,否则走HTTP协议缓存机制
  • 离线包也可以根据版本号向服务器请求两个版本的差异文件,进行合并,实现增量更新

公共资源包

每个离线包都会用到一些公共的JS框架、CSS样式、图标等资源,这些如果重复出现在离线包中,就很浪费网络流量,增加下载时间,可以将这些文件做一个公共资源包下发给客户端


预加载WebView

Android在4.4以前使用Webkit作为webview内核,之后做了优化以chromium替代,但是我们在使用的时候第一次打开Webview的时候,还是会感觉很慢,而且这种慢还是没有开始请求数据,仅仅是内核的启动初始化,这一块我们开发者要去修改这块,提高内核启动效率不太现实,尽管国内厂家也做了自己的优化,像腾讯的X5内核,相比于原生的有很大的提升,而且提供的功能更多,已经开放了。

但是我们如果就用原生内核,怎么避免在使用webview加载页面出现过久初始化导致的白屏的情况呢?

当App启动的时候,就在Application里初始化一个Webview,对,就是直接new;当需要用的时候就直接取这个单例形式的webview去加载网页,这样就把webview初始化的等待时间变得用户无感知;不过每次使用的时候需要清空上次使用的页面内容


拦截网络请求

对于页面中的大量图片等资源,可以提前下载下来,然后拦截网络请求,加载本地资源

/**
     * 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);

上面这个重写的方法还可以做其它的事,比如修改请求连接,往链接里加一些标志位,服务端也可以通过标志位进行一些其它处理

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);

Webview缓存机制

其实WebView有自带了一些缓存机制

1.浏览器缓存机制

2.APPlication Cache

3.Dom Storage

4.Web Sql Database

5.Indexed Database

6.File System

第一种:浏览器缓存机制

这个协议主要是前端同学设置,android客户端开发无须关心,在上面的【前端优化】一节已提到

这种缓存机制只要是正规的浏览器基本都支持,不过手机缓存的存储空间有限(在data/data/包名 目录下),随时可能被清除。

第二种:APPlication Cache

这种缓存机制可以说是对浏览器缓存机制的补充,原理相似,都是以文件为单位,有文件更新机制;这个机制需要前端设置,客户端也需要进行一些设置才能起作用。是一个专门为Web App离线使用的缓存机制,不过对于我们客户端开发官方已经不推荐使用了,

在APP上设置如下

//Android 私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录。
mSetting.setAppCachePath(MyApplication.getInstance().getCacheDir().getAbsolutePath());
//设置是否启用缓存,不过需要设置好缓存路径,默认false
mSetting.setAppCacheEnabled(true);

在编写Html代码的时候需要指定manifest属性    ,这样页面就能使用app cache

<html manifest="demo.appcache">

</html>

一个完整的appcache文件包含3个section

CACHE MANIFEST

# 2018-06-12

/demo.js

NETWORK:

*

FALLBACK:

/fail.html

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暂时不支持

缓存模式

说完了缓存机制的使用,webview还有缓存模式的设置

//设置缓存模式
mSetting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

WebView提供了如下几种模式

/**
 * 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.当我们存储静态资源文件,比如js,使用浏览器缓存、    APP Cache缓存

    2.存储临时简单数据 使用dom storage

    3.存储复杂大量数据 使用indexedDB

清除缓存

当你设置缓存后的效果就是即使断网了,依然能加载出上次浏览的内容;但是有时候你不需要这种效果,那就涉及到清除webview缓存了

//清空所有Cookie
CookieSyncManager.createInstance(getApplicationContext());  //Create a singleton CookieSyncManager within a context
CookieManager cookieManager = CookieManager.getInstance(); // the singleton CookieManager instance
cookieManager.removeAllCookie();// Removes all cookies.
CookieSyncManager.getInstance().sync(); // forces sync manager to sync now

mWebView.setWebChromeClient(null);
mWebView.setWebViewClient(null);
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearCache(true);
WebStorage.getInstance().deleteAllData();

 

Html开发优化

1.一般我们在html文件的head里面单独引入css和内联js都不会阻塞html页面的解析,但是如果同时且先link css 然后加内联js,就会造成css的加载阻塞内联js执行,进而阻塞html解析。

所以编写html的时候要注意css的标签要靠前,css下面不要添加任何的内联js。请参照web页面加载优化建议

2.像React这样偏重的框架, 其中的js解析编译执行会占很多时间,在配置不是很高的手机上,很影响页面的渲染速度,所以在开发中一定要慎用,同时同一个app里在webview开发者方面尽量统一第三方框架,这样可以提高缓存的使用率

这里推荐一个美团知识分享网站

https://tech.meituan.com/

总结

本篇文章从前端优化,客户端缓存,离线包等方面讨论了如何优化H5启动,大致思路就是使用缓存,预加载,拦截网络请求使用本地资源,尽量在用户打开之前就准备好所有资源,加快启动速度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值