一、WebView组件封装系列文章
- WebView组件封装(一)------怎样使用全局缓存池管理提高WebView加载速度
- WebView组件封装(二)------怎样用设计模式封装WebView,轻松实现个性化定制,让你的App网页更加顺畅
- WebView组件封装(三)------PkWebView的使用文档
- WebView组件封装(四)------三级缓存实现H5页面秒开之OkHttp缓存拦截器源码
Github地址:github.com/Peakmain/Pk... 欢迎大家来踩哦
二、H5页面秒开效果对比
三、架构
三级缓存架构
四、优化方案
1、预加载WebView
1.1 原因
- 创建WebView属于一种比较耗时的操作,尤其是第一次创建时需要初始化浏览器内核,会耗时几百毫秒,之后再次创建WebView会快很多,但仍需要几十毫秒
- 为了避免每次使用都需要同步等待WebView创建完成,可以选择合适的时机预加载WebView并存入缓存池,用的时候直接去缓存池中取
1.2 技术方案
-
Application时初始化WebView放入缓存池
- WebView创建属于比较耗时的操作,不能放入主线程,因为会影响主线程任务
- 通过MessageQueue的IdleHandler来触发创建
- 为了安全合规,请一定一定要在同意隐私政策之后初始化
-
WebView创建Context使用MutableContextWrapper
- MutableContextWrapper是系统提供Context包装类,内部包含一个baseContext,MutableContextWrapper内部方法都是baseContext来实现的
- MutableContextWrapper中的baseContext是允许被替换的,所以一开始我们可以使用Application作为baseContext,等到WebView和Activity绑定时再替换
-
H5通信,通过shouldOverrideUrlLoading拦截url进行处理
- 传统方案通常是指通过WebView的
addJavaScriptInterface()
方法将Java对象注入到JavaScript。通常会存在几个问题- 安全风险 :使用
addJavascriptInterface()
注入 Java 对象到 WebView 中,会使得 JavaScript 可以直接调用这些 Java 对象的方法,如果处理不当,会导致安全漏洞问题,例如恶意的H5页面可能会调用Android应用中的敏感方法,从而导致安全问题 - 兼容性问题 :
addJavascriptInterface()
方法存在兼容性问题,不同版本的 Android 系统对该方法的支持不一致,可能会导致在某些设备或者某些 Android 版本上无法正常使用
- 安全风险 :使用
- 通过shouldOverrideUrlLoading拦截url进行处理的好处
- 自定义URL Schemem:可以自定义自己的URL Scheme,例如:"pkWebView://action",然后再H5页面中通过这个URL Scheme触发Android相应操作,如启动某Activity,打开相册等功能
- 拦截跳转: 可以拦截 H5 页面中的链接点击,进而在 Android 应用中实现特定的处理逻辑,比如跳转到另一个页面、执行特定的操作等
- 与原生代码交互: 通过特定的 URL 规则,可以实现 H5 页面与 Android 原生代码的交互,例如传递参数、调用原生方法等
- 传统方案通常是指通过WebView的
1.3 初始化的示例代码
kotlin
PkWebViewInit.Builder(application)
//设置LoadingView
.setLoadingWebViewState(LoadingWebViewState.HorizontalProgressBarLoadingStyle)
//设置userAgent
.setUserAgent(userAgent)
//设置WebView的数量
.setWebViewCount(5)
//设置接收H5通信的协议
.setHandleUrlParamsCallback(HandlerUrlParamsImpl())
.registerEntities(
NewHotelHandle::class.java,
NewAuthorityEmptyHandle::class.java,
NewPageActionHandle::class.java,
NewUserActionHandle::class.java
)
//创建WebView
.build()
2、三级缓存原理
2.1 背景
借用阿里巴巴淘宝系技术一张图
我们可以大概绘制下H5链路
- WebView加载整个过程需要完成多个网络请求和IO操作,WebView加载HTML、CSS、JS并进行解析这段时间都是耗时,之后通过JS从服务端获取正文数据,拿到数据后还需要完成解析JSON、应用CSS等一系列耗时操作
- 移动端的系统版本、处理器速度不是我们控制的,且极容易受网络波动影响,用户使用体验是完全不可控的
2.2 预加载技术方案
上面我们已经完成WebView初始化
和组件创建完成
。我们来看下其他的东西如何优化
2.2.1 如何进行预加载?
- 当webView设置loadUrl的时候,会请求资源包括js、css、html、字体等
- 我们可以通过shouldInterceptRequest去拦截其资源请求,将资源缓存到本地,下次进入时直接取缓存即可
java
WebView webView = findViewById(R.id.webView);
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
// 拦截资源请求
String url = request.getUrl().toString();
if (url.endsWith(".js") || url.endsWith(".css") || url.endsWith(".html") || url.endsWith(".woff") || url.endsWith(".woff2")) {
// 缓存资源到本地
cacheResource(url);
}
// 返回 null,表示继续加载原始的资源
return null;
}
});
webView.loadUrl("https://www.example.com");
2.2.2 何时预加载?
- 在上面的预加载过程中,我们为了不影响主线程任务,将创建WebView放到了MessageQueue的idleHandler中
- 现在在首页直接获取缓存池中的WebView,WebView可能为空,如何解决这个问题呢?
- 我们可以继续使用MessageQueue中的addIdleHandler,重写queueIdle方法
- 返回
true
:表示空闲处理器仍然希望继续监听空闲状态,并可能在之后继续执行某些任务。系统会保留该空闲处理器,继续在空闲状态时调用它的queueIdle()方法
- 返回
false
:表示空闲处理器已经完成了它的任务,并不再需要监听空闲状态。系统会将该空闲处理器从消息队列中移除,不再调用它的queueIdle()
方法
kotin
fun preLoadUrl(context: Context?, url: String) {
Looper.myQueue().addIdleHandler(object : MessageQueue.IdleHandler {
override fun queueIdle(): Boolean {
val webView = getWebView(context) ?: return true
webView.preLoadUrl(url)
WebViewPool.instance.releaseWebView(webView)
return false
}
})
}
2.3 三级缓存技术方案
2.3.1. 类图
- 使用的是责任链设计模式,参考的OKHttp五大拦截器源码
- 以下三级缓存器类图
2.3.2. 两种创建WebResourceResponse区别
java
public WebResourceResponse(String mimeType, String encoding,
InputStream data) {
}
public WebResourceResponse(String mimeType, String encoding, int statusCode,
@NonNull String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
}
两种区别在于下面多了状态码(200、304等),状态信息("OK")、响应头,5.0以后更推荐参数多的
2.3.3. 内存缓存:主要使用LRU(最近最少使用)缓存,如果缓存的数据存在则直接返回内存缓存,如果不存在调用磁盘缓存,并将磁盘缓存缓存到内存缓存
kotlin
override fun cacheInterceptor(chain: ICacheInterceptor.Chain): WebResource? {
val request = chain.request()
mLruCache?.let {
val resource = it.get(request.key)
if (checkResourceValid(resource)) {
LogWebViewUtils.i("读取内存缓存:${request.url}")
return resource
}
}
val resource = chain.process(request)
//内存缓存资源
if (mLruCache != null){
LogWebViewUtils.i("内存缓存缓存数据:${request.url}")
mLruCache?.put(request.key, resource)
}
return resource
}
2.3.4. 磁盘缓存
利用的是第三方库大佬jakewharton的DiskLruCache
- 如果磁盘缓存存在,则直接读取缓存数据
- 如果不存在交于网络缓存
- 缓存到本地磁盘中
kotlin
override fun cacheInterceptor(chain: ICacheInterceptor.Chain): WebResource? {
val request = chain.request()
createLruCache()
var webResource = getWebResourceFromDiskCache(request.key)
if (webResource != null && isContentTypeCacheable(webResource)) {
LogWebViewUtils.i("读取磁盘缓存:${request.url}")
return webResource
}
webResource = chain.process(request)
//磁盘进行缓存
if (webResource != null ) {
LogWebViewUtils.i("磁盘缓存缓存数据:${request.url}")
cacheToDisk(request.key, webResource)
}
return webResource
}
@Synchronized
private fun createLruCache() {
if (mDiskLruCache != null && !mDiskLruCache!!.isClosed) {
return
}
if (context == null) return
var cacheConfig = CacheWebViewManager.instance.getCacheConfig()
if (cacheConfig == null) {
cacheConfig = CacheConfig.Builder(context).build()
}
val dir =
if (!TextUtils.isEmpty(cacheConfig.getCacheDir())) cacheConfig.getCacheDir() else cacheConfig.getDefaultCache()
val version = cacheConfig.getVersion()
val cacheSize = cacheConfig.getDiskCacheSize()
try {
mDiskLruCache = DiskLruCache.open(dir?.let { File(it) }, version, 2, cacheSize)
} catch (e: Exception) {
e.printStackTrace()
}
}
2.3.5. 网络缓存
WebView 获取图片和其他资源的传统方案有以下两种
- H5端自己通过网络去下载资源。优点:实现简单,各端之间只关注自己的业务即可。缺点:两端之间无法共享缓存
- H5向原生发送协议,利用原生下载和缓存能力来获取资源。优点:可以实现两端之间的缓存共享。缺点:需要H5主动触发Native执行。这段时间会
加重白屏效果
解决方案
- 图片缓存利用Glide去加载,Glide内部自身有四级缓存(活动缓存、内存缓存、磁盘缓存、网络缓存)
kotlin
fun loadImage(context:Context,request: CacheRequest): WebResource? {
val url = request.url
try {
// 使用 Glide 加载图片
val bitmap = Glide.with(context)
.asBitmap()
.load(url)
.listener(object :RequestListener<Bitmap>{
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Bitmap>?,
isFirstResource: Boolean,
): Boolean {
LogWebViewUtils.e("图片加载失败:${e?.message}")
return false
}
override fun onResourceReady(
resource: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean,
): Boolean { if (dataSource == DataSource.MEMORY_CACHE) {
// 图片来自内存缓存
// 处理内存缓存的逻辑
LogWebViewUtils.i("图片缓存来自内存缓存:${request.url}")
} else if (dataSource == DataSource.DATA_DISK_CACHE) {
// 图片来自磁盘缓存
// 处理磁盘缓存的逻辑
LogWebViewUtils.i("图片缓存来自磁盘缓存:${request.url}")
} else {
// 图片来自网络
// 处理网络加载的逻辑
LogWebViewUtils.i("图片缓存来自网络缓存:${request.url}")
}
return false
}
})
.submit()
.get()
// 创建 WebResource 对象并设置数据
val remoteResource = WebResource()
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
remoteResource.originBytes = outputStream.toByteArray()
remoteResource.responseCode = 200
remoteResource.message = "OK"
remoteResource.isModified = true // 如果需要根据实际情况设置是否修改了
// 设置响应头
val headersMap = HashMap<String, String>()
// 添加你需要的响应头信息
remoteResource.responseHeaders = headersMap
return remoteResource
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
- 非图片资源使用OKHttp去加载资源,如html、css、js、字体等
kotlin
fun getResource(request: CacheRequest, isContentType: Boolean): WebResource? {
val url = request.url
val acceptLanguage =
Locale.getDefault().toLanguageTag()
val builder = Request.Builder()
.removeHeader(USER_AGENT)
.addHeader(USER_AGENT, request.userAgent)
.addHeader("Accept", "*/*")
.addHeader("Accept-Language", acceptLanguage)
val headers = request.headers
if (headers != null && headers.isNotEmpty()) {
for ((header, value) in headers) {
if (!isNeedStripHeader(header)) {
builder.removeHeader(header)
builder.addHeader(header, value)
}
}
}
val request = builder.url(url!!)
.get().build()
var response: Response? = null
val remoteResource = WebResource()
response = createOkHttpClient().newCall(request).execute()
if (isInterceptorThisRequest(response)) {
remoteResource.responseCode = response.code
remoteResource.message = response.message
remoteResource.isModified = response.code != HttpURLConnection.HTTP_NOT_MODIFIED
val responseBody: ResponseBody? = response.body
if (responseBody != null) {
remoteResource.originBytes = responseBody.bytes()
}
remoteResource.responseHeaders = WebViewUtils.instance.generateHeadersMap(response.headers)
return remoteResource
}
return null
}