WebView组件封装(五)——实现H5页面秒开方案总结

一、WebView组件封装系列文章

Github地址:github.com/Peakmain/Pk... 欢迎大家来踩哦

二、H5页面秒开效果对比

三、架构

三级缓存架构

四、优化方案

1、预加载WebView

1.1 原因

  • 创建WebView属于一种比较耗时的操作,尤其是第一次创建时需要初始化浏览器内核,会耗时几百毫秒,之后再次创建WebView会快很多,但仍需要几十毫秒
  • 为了避免每次使用都需要同步等待WebView创建完成,可以选择合适的时机预加载WebView并存入缓存池,用的时候直接去缓存池中取

1.2 技术方案

  1. Application时初始化WebView放入缓存池

    • WebView创建属于比较耗时的操作,不能放入主线程,因为会影响主线程任务
    • 通过MessageQueue的IdleHandler来触发创建
    • 为了安全合规,请一定一定要在同意隐私政策之后初始化
  2. WebView创建Context使用MutableContextWrapper

    • MutableContextWrapper是系统提供Context包装类,内部包含一个baseContext,MutableContextWrapper内部方法都是baseContext来实现的
    • MutableContextWrapper中的baseContext是允许被替换的,所以一开始我们可以使用Application作为baseContext,等到WebView和Activity绑定时再替换
  3. 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 原生代码的交互,例如传递参数、调用原生方法等

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
}

参考资料

相关推荐
诸神黄昏EX20 分钟前
Android 分区相关介绍
android
大白要努力!1 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
天天扭码3 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
余生H3 小时前
transformer.js(三):底层架构及性能优化指南
javascript·深度学习·架构·transformer
凡人的AI工具箱3 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
运维&陈同学4 小时前
【zookeeper01】消息队列与微服务之zookeeper工作原理
运维·分布式·微服务·zookeeper·云原生·架构·消息队列