Android开发 您所在的位置:网站首页 app加载h5页面很慢 Android开发

Android开发

2024-01-29 00:28| 来源: 网络整理| 查看: 265

前言

老早之前就想总结下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 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

一个完整的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启动,大致思路就是使用缓存,预加载,拦截网络请求使用本地资源,尽量在用户打开之前就准备好所有资源,加快启动速度



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有