# cachewebview **Repository Path**: perry_fan/cachewebview ## Basic Information - **Project Name**: cachewebview - **Description**: 简单易用,高性能Hybrid组件 及 首屏直出用户秒开体验的解决方案 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2023-05-15 - **Last Updated**: 2023-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 一. 简介 >+ 本项目提供有 简单易用,高性能Hybrid组件 及 首屏直出用户秒开体验的解决方案 >+ 功能:H5预加载、H5资源缓存、首屏直出等 >+ 便于监控加载性能,接入线上监控平台。
## 二. 架构 ### 架构简图 drawing
### CacheWebView时序图 drawing

## 三. 优化方案 ### 1. 引入 CacheWebView 组件 #### 1)引入组件 ```maven implementation 'io.gitee.perry_fan:cachewebview:1.0.1' repositories { mavenCentral() } ``` #### 2)用 CacheWebView 来替代原生 WebView 引入到项目中 #### 3)设置缓存模式 ```kotlin // 缓存模式:必选配置 val cacheMode = if (WebSettingUtil.isNetworkConnected(this)) WebSettings.LOAD_DEFAULT else WebSettings.LOAD_CACHE_ELSE_NETWORK webView.settings.cacheMode = cacheMode webView.setCacheMode(CacheMode.CACHE) // 自定义可选配置 val config: CacheConfig = CacheConfig.Builder(this) .setCacheDir(cacheDir.toString() + File.separator + "custom") .setDiskCacheSize(200 * 1024 * 1024) // 自定义缓存大小 .setExtensionFilter(CustomMimeTypeFilter()) // 自定义缓存格式文件 .build() webView.setCacheMode(CacheMode.CACHE, config) ``` #### 4)原理解读   H5加载过程中有大量静态资源需下载后,交予容器渲染,每次加载均会重复请求。本方案在容器回调方法中进行拦截。定义拦截指定格式的资源如(js、css、png、jpg、webp、ico...等),交由原生网络框架进行加载,并本地化缓存。   通过资源链接url的 md5 值作为唯一标识,当资源名称变更则默认为新资源,缓存方式有:内存缓存、磁盘缓存均采用 LruCache 算法进行优化、还有OkHttp的网络缓存。再次读取时可极大缩减资源重复加载导致的网络请求耗时。缓存区域设置在应用的 cache目录下,无需申请文件读写权限,并设置100MB为默认缓存空间大小(优于原生自有的5MB空间)。   由于首次进入页面才会进行静态资源下载缓存,用户首次仍会感受到加载速度稍慢,因而选择在H5业务的前置页预加载指定业务url并执行缓存静态资源。
### 2. 预加载 #### 1)application 中引擎预加载 ```kotlin // application onCreate()方法中 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PreloadEngineUtil.preloadWebView(this) } ```   webview初始化耗时的部分很多在 WebViewChromiumAwInit 完成,在我们的应用创建时预先执行这部分初始化工作,当主线程空闲即完成Chromium引擎的加载。缩短容器初始化容器耗时。 #### 2)容器预创建 并 预加载业务Url   思想是“以空间换时间”,通过内存的少量消耗,换取对webview加载耗时的减少。 进入主页后/打开H5页面前置页,预创建一个webview容器。在打开H5相关业务时可直接复用该容器,缩短原构建耗时。 另外,在退出相关页面后,清空当前业务内容并重建容器加入缓存池中为下一次H5业务使用做准备。 #### a. 初始化 / 获取CacheWebView ```kotlin WebViewCacheHolder.acquire(context) ``` 使用方式: ```kotlin // H5前置页 / 应用首页调用 private fun preloadUrl(url: String) { val webView: CacheWebView = WebViewCacheHolder.acquire(this) val cacheMode = if (WebSettingUtil.isNetworkConnected(this)) WebSettings.LOAD_DEFAULT else WebSettings.LOAD_CACHE_ELSE_NETWORK WebSettingUtil.webSetting(webView) webView.settings.cacheMode = cacheMode webView.preload(this, url) } ``` #### b. 释放资源防泄漏 / 并变更容器上下文为下次使用准备 ```kotlin WebViewCacheHolder.release(webview) ``` 使用方式: ```kotlin // WebActivity中调用 override fun onDestroy() { if (webView != null) { WebViewCacheHolder.release(webView) } super.onDestroy() } ```
### 3. 首屏优化   为达成首屏业务直出,无白屏的体验。本方案在部分 **首屏入口交互样式变化较少场景中应用**,达成直出交互效果。 #### 1)在首次页面加载完成后,存储当前页面截屏,并存入应用缓存中。 #### 2)在其后进入时,直接加载本次截屏(需判断是否为入口页),当容器渲染加载完成url时将截屏隐藏,展示当前H5内容。 ```kotlin fun saveImageToCache(context: Context, bmp: Bitmap, urlContent: String): Boolean { // 首先保存图片 val storePath: String = context.cacheDir.absolutePath + File.separator.toString() + context.packageName val appDir = File(storePath) if (!appDir.exists()) { appDir.mkdir() } val fileName = "$urlContent.jpg" val file = File(appDir, fileName) try { val fos = FileOutputStream(file) //通过io流的方式来压缩保存图片 val isSuccess = bmp.compress(Bitmap.CompressFormat.PNG, 100, fos) fos.flush() fos.close() return isSuccess } catch (e: Exception) { e.printStackTrace() } return false } private fun cacheImageCheck() { var path = "" if (targetUrl == UrlManager.webUrl) { path = ImgUtils.getImageFromCache(this@WebActivity, UrlManager.webContent) } if (!TextUtils.isEmpty(path)) { val bm = BitmapFactory.decodeFile(path) if (bm != null) { imgViewCache.visibility = View.VISIBLE imgViewCache.setImageBitmap(bm) } mHandler.postDelayed(Runnable { if (this@WebActivity.isFinishing) { return@Runnable } imgViewCache.visibility = View.GONE }, 6000) } else { imgViewCache.visibility = View.GONE } } ``` #### 3)白屏判断:截屏的时机为当前页不为白屏,具体判断方法定义为,定时判断左上、中间、右下取点,颜色不完全相同即认为达到保存时机。 ```kotlin fun checkPictureIsSameColor(bitmap: Bitmap): Boolean { val random = Random() val bitmapHeight = bitmap.height val bitmapWidth = bitmap.width val colorOne = bitmap.getPixel(random.nextInt(bitmapWidth), random.nextInt(bitmapHeight)) val colorTwo = bitmap.getPixel(random.nextInt(bitmapWidth / 2), random.nextInt(bitmapHeight / 2)) val colorThree = bitmap.getPixel(random.nextInt(bitmapWidth / 3), random.nextInt(bitmapHeight / 3)) return colorOne == colorTwo && colorTwo == colorThree && colorOne == colorThree } ```
### 4. 其他 #### 1)blockNetworkImage 处理 未加载完成前 ```kotlin webSettings.blockNetworkImage = true ``` 加载完成后即 onPageFinished方法调用后 ```java view.settings.blockNetworkImage = false ``` #### 2)WebView setting配置等,如webSettings.domStorageEnabled = true #### 3)H5打离线包,根据策略下载分发,匹配到本地模板即直接加载本地资源 (需自行添加)
## 四. 监控方案 > 容器 Js 注入的方式,在容器侧 定量监控 H5 性能 #### 1)H5性能统计API [Performance.Timing](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/timing) + [PerformancePaintTiming](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformancePaintTiming) + [PerformanceNavigationTiming](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceNavigationTiming) #### 2)数据指标:fp、fcp、du 等   由于官方不再推荐使用 `Performance.Timing` 特性,因而采用推荐的后两种API为主, 采集字段为 `fp`、`fcp` 由 `PerforamancePaintTiming` 获取 、`du` 由 `PerformanceNavigationTiming`的 duration 获取。   因兼容性问题采集不到的使用 `PerformanceTiming` 中的 `du = loadEventEnd - navigationStart, fp = domInteractive - navigationStart, fcp = loadEventEnd- navigationStart` 作为兜底方案;   另外可根据需要,新增 dns、tcp、ttfbTime 等监控指标。 ```kotlin webView.addJavascriptInterface(this, "web") override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) // 性能监控 view.evaluateJavascript(UrlManager.JS_MONITOR, null) } const val JS_MONITOR = "javascript:web.setH5Performance(" + "JSON.stringify(window.performance.timing), window.location.href , " + "JSON.stringify(window.performance.getEntriesByType('paint'))," + "JSON.stringify(window.performance.getEntriesByType('navigation')))" @JavascriptInterface fun setH5Performance(timing: String?, curUrl: String?, paint: String?, navigation: String?) { // 具体数据处理,数据监控工具接入 ... } ``` > 例如:{fp(白屏时间)=344, fcp(渲染时间)=344, du(总时长)=656}
## 五. 优化效果对比
优化前
优化后
> Demo 应用,在app目录下可直接运行