优化WebView实现H5秒开

news2024/12/23 18:38:37

WebView与原生对比差在哪里?

这里引用百度APP图片来说明。

百度的开发人员将这一整个过程划分为了四个阶段,并统计出了各个阶段的平均耗时。

可以看到,在初始化组件阶段就花费了 260 ms,首次创建耗时均值为 500 ms,毫无疑问这是我们要优化的第一点。而最耗时的当属正文加载&渲染和图片加载两个阶段。为什么会这么耗时呢,因为这两个阶段需要进行多次网络请求、JS 调用、IO 读写。所以这里也是我们需要优化的地方。

可以得出优化方向:

1. WebView预创建和复用。

2. 渲染优化(JS、CSS、图片)。

3. 模板优化(拆分、预热、复用)。

WebView预创建和复用

WebView 的创建是比较耗时的,首次创建耗时几百毫秒,因此预创建和复用尤为重要。

大致逻辑是先创建WebView并缓存起来,等到需要的时候直接取出来,代码如下:

class WebViewManager private constructor() {

    // 为了阅读体验,省略部分代码

    private val webViewCache: MutableList<WebView> = ArrayList(1)

    private fun create(context: Context): WebView {
        val webView = WebView(context)
    // ......
        return webView
    }

    fun prepare(context: Context) {
        if (webViewCache.isEmpty()) {
            Looper.myQueue().addIdleHandler {
                webViewCache.add(create(MutableContextWrapper(context)))
                false
            }
        }
    }

    fun obtain(context: Context): WebView {
        if (webViewCache.isEmpty()) {
            webViewCache.add(create(MutableContextWrapper(context)))
        }
        val webView = webViewCache.removeFirst()
        val contextWrapper = webView.context as MutableContextWrapper
        contextWrapper.baseContext = context
        webView.clearHistory()
        webView.resumeTimers()
        return webView
    }

    fun recycle(webView: WebView) {
        try {
            webView.stopLoading()
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView.clearHistory()
            webView.pauseTimers()
            webView.webChromeClient = null
            webView.webViewClient = null
            val parent = webView.parent
            if (parent != null) {
                (parent as ViewGroup).removeView(webView)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (!webViewCache.contains(webView)) {
                webViewCache.add(webView)
            }
        }
    }

    fun destroy() {
        try {
            webViewCache.forEach {
                it.removeAllViews()
                it.destroy()
                webViewCache.remove(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}

这里需要注意以下几点:

预加载时机的选择

WebView 的创建是比较耗时的,为了使预加载不会影响到当前主线程任务,我们选择 IdleHandler 来执行预创建,以保证不会影响到当前主线程任务。详细请看 prepare(context: Context) 方法。

Context的选择

每个 WebView 需要和对应的 Activity Context 实例进行绑定,为了保证预加载的 WebView Context 和最终的 Context 之间的一致性,我们通过 MutableContextWrapper 来解决这个问题。

MutableContextWrapper 允许外部替换它的 baseContext ,因此 prepare(context: Context)方法可以传 applicationContext 进行预创建,等到实际调用时再进行替换,详细请看 obtain(context: Context) 方法。

复用和销毁

在页面关闭前调用 recycle(webView: WebView) 进行回收,在应用退出前调用 destroy() 进行销毁。

复用WebView返回空白

在调用 recycle(webView: WebView) 进行回收时,我们会调用 loadDataWithBaseURL(null, "", "text/html", "utf-8", null) 清除页面内容,导致复用时的加载栈底就是这个空白页面,所以我们需要在返回时对栈底进行判断,如果为空则直接返回,代码如下:

fun canGoBack(): Boolean {
    val canBack = webView.canGoBack()
    if (canBack) webView.goBack()
    val backForwardList = webView.copyBackForwardList()
    val currentIndex = backForwardList.currentIndex
    if (currentIndex == 0) {
        val currentUrl = backForwardList.currentItem.url
        val currentHost = Uri.parse(currentUrl).host
        //栈底不是链接则直接返回
        if (currentHost.isNullOrBlank()) return false
    }
    return canBack
}

渲染优化(JS、CSS、图片)

WebView 在加载内容的时候会进行多次网络请求、JS 调用、IO 读写。我们可以借由内核的 shouldInterceptRequest 回调,拦截资源请求由客户端进行下载,并以管道方式填充到内核的 WebResourceResponse中,这里引用百度APP图片来说明。

预置离线包

精简并抽取公共的 JS 和 CSS 文件作为通用资源,将抽取的资源存放在 assets 下,再通过约定的规则去匹配,代码如下:

webView.webViewClient = object : WebViewClient() {

    // 为了阅读体验,省略部分代码

    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        if (view != null && request != null) {
            if(canAssetsResource(request)){
                return assetsResourceRequest(view.context, request)
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}
private fun assetsResourceRequest(
    context: Context, 
    webRequest: WebResourceRequest
): WebResourceResponse? {

    // 为了阅读体验,省略部分代码

    try {
        val url = webRequest.url.toString()
        val filenameIndex = url.lastIndexOf("/") + 1
        val filename = url.substring(filenameIndex)
        val suffixIndex = url.lastIndexOf(".")
        val suffix = url.substring(suffixIndex + 1)
        val webResourceResponse = WebResourceResponse()
        webResourceResponse.mimeType = getMimeTypeFromUrl(url)
        webResourceResponse.encoding = "UTF-8"
        webResourceResponse.data = context.assets.open("$suffix/$filename")
        return webResourceResponse
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

接口更新缓存资源

除了预置离线包外,我们还可以通过接口请求,获取最新的缓存资源,以及通过请求资源的类型自行缓存,代码如下:

webView.webViewClient = object : WebViewClient() {

    // 为了阅读体验,省略部分代码

    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        if (view != null && request != null) {
            if(canCacheResource(request)){
                return cacheResourceRequest(view.context, request)
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}
private fun canCacheResource(webRequest: WebResourceRequest): Boolean {

    // 为了阅读体验,省略部分代码

    val url = webRequest.url.toString()
    val extension = getExtensionFromUrl(url)
    return extension == "ico" || extension == "bmp" || extension == "gif"
            || extension == "jpeg" || extension == "jpg" || extension == "png"
            || extension == "svg" || extension == "webp" || extension == "css"
            || extension == "js" || extension == "json" || extension == "eot"
            || extension == "otf" || extension == "ttf" || extension == "woff"
}
private fun cacheResourceRequest(
    context: Context, 
    webRequest: WebResourceRequest
): WebResourceResponse? {

    // 为了阅读体验,省略部分代码

    try {
        val url = webRequest.url.toString()
        val cachePath = CacheUtils.getCacheDirPath(context, "web_cache")
        val filePathName = cachePath + File.separator + url.encodeUtf8().md5().hex()
        val file = File(filePathName)
        if (!file.exists() || !file.isFile) {
            runBlocking {
                        // 开启网络请求下载资源
                download(HttpRequest(url).apply {
                    webRequest.requestHeaders.forEach { putHeader(it.key, it.value) }
                }, filePathName)
            }
        }
        if (file.exists() && file.isFile) {
            val webResourceResponse = WebResourceResponse()
            webResourceResponse.mimeType = getMimeTypeFromUrl(url)
            webResourceResponse.encoding = "UTF-8"
            webResourceResponse.data = file.inputStream()
            webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
            return webResourceResponse
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

我们通过canCacheResource(webRequest: WebResourceRequest)来判断是否是需要缓存的资源。

再根据URL去获取缓存中文件,否则开启网络请求下载资源,详细请看 cacheResourceRequest(context: Context, webRequest: WebResourceRequest) 。

这边仅对图片、字体、CSS、JS、JSON进行缓存,可根据项目实际情况缓存更多类型资源。

模板优化(拆分、预热、复用)

关于模板,代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>title</title>
        <link rel="stylesheet" type="text/css" href="xxx.css">
    <script>
        function changeContent(data){
            document.getElementById('content').innerHTML=data;
        }
    </script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

客户端加载模板代码(温馨提示:上面只是例子,实际模板根据情况拆分),加载完成后再调用 JS 方法注入数据。

webView.evaluateJavascript("javascript:changeContent('<p>我是HTML</p>')") {}

数据哪里来呢?这里以列表页跳转详情页举个例子,仅供参考:

列表页接口返回列表数据的时候带上详情内容,跳转详情页的时候带上内容数据。优点简单粗暴,缺点耗费流量。

当然还有其他方法这里不再多说,可根据自己的实际需求进行选择。

CDN 加速、DNS 等其他优化

网上资料很多,这里就不列举出来了,请自己去查找。谢谢!

示例代码:

https://github.com/miaowmiaow/fragmject

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/372143.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7-vue-2

vue3.2 自定义全局指令、局部指令 // 在src目录下新建一个directive文件&#xff0c;在此文件夹下新建一个index.js文件夹&#xff0c;接着输入如下内容 const directives (app) > {//这里是给元素取得名字&#xff0c;虽然是focus&#xff0c;但是实际引用的时候必须以v…

为赋能,创共赢~ 〖TFS_CLUB社区〗-〖星荐官计划〗来袭~ 期待各位小伙伴的加入~

文章目录❤️‍&#x1f525; TFS社区介绍❤️‍&#x1f525; 星荐官计划在直播结束之后&#xff0c;有几位小伙伴跟我说&#xff0c;想法是好的&#xff0c;但是会很难搞。试想一下如果真的是很容易做的事情&#xff0c;那岂不是人人都可以做&#xff1f;正因为难做&#xff…

PyQt5数据库开发2 5.2 QSqlRelationalTableModel

目录 一、Qt窗体设计 1. 新建Qt项目 2. 添加组件 3. 添加资源 4. 添加Action 5. 添加工具栏 6. 添加菜单项 7. 添加退出功能 二、SQL Server下建表插数据 1. 建立表 2. 插入数据 3. 单表数据 4. 联合查询 三、代码实现 1. 新建项目目录 2. 编译窗体文件和资…

[计算机网络(第八版)]第一章 概述(章节测试/章节作业)

随堂作业 练习版(无答案版) 1.2 因特网概述 1【单选题】因特网的前身是1969年创建的第一个分组交换网 A、internetB、InternetC、NSFNETD、ARPANET 2【单选题】因特网采用的核心技术是 A、TCP/IPB、局域网技术C、远程通信技术D、光纤技术 1.3 三种交换方式&#xff1a;电路…

mysql数据库表的创建与查看

mysql数据库表的创建与查看 一、mysql查看 查看所有数据库 show databases切换数据库 use 数据库名查看该数据库下所有的表名 show tables查看表的结构 desc 表名二、mysq创建 创建数据库 create database 数据库名;创建数据库设置编码 drop database if EXISTS dbname; creat…

Gehpi的网络布局

Gehpi的网络布局1. 力引导布局2. 辅助布局布局是网络可视化中的重要概念&#xff0c;指将点和边通过某种策略进行排布&#xff0c;应尽可能满足以下4个原则&#xff1a; 节点均匀分布在有限的区域内避免边的交叉和弯曲保持边的长度一致整体布局能反映图内在的特性 Gephi的布局…

flask入门-3.Flask操作数据库

3. Flask操作数据库 1. 连接数据库 首先下载 MySQL数据库 其次下载对应的包: pip install pymysql pip install flask-sqlalchemy在 app.py 中进行连接测试 from flask import Flask, request, render_template from flask_sqlalchemy import SQLAlchemyhostname "1…

Facebook广告投放运营中的关键成功因素是什么?

在当今数字化的时代&#xff0c;广告投放已经成为了各种企业获取市场份额和增加品牌曝光的重要手段之一。Facebook作为全球最大的社交媒体平台之一&#xff0c;其广告投放运营的成功&#xff0c;将直接影响企业的品牌推广和市场营销效果。本文将探讨Facebook广告投放运营中的关…

浅谈光流跟踪之KLT稀疏光流跟踪算法

0 简介 在学习vins-mono过程中&#xff0c;算法前端采用基于KLT光流的跟踪方法&#xff0c;对光流和KLT光流进行简单的总结。包括LK光流跟踪算法原理&#xff0c;基于金字塔改进的LK光流跟踪&#xff0c;KLT光流跟踪算法&#xff0c;以及在光流跟踪时使用的角点检测算法Harris…

追梦之旅【数据结构篇】——详解C语言实现链队列

详解C语言实现链队列~&#x1f60e;前言&#x1f64c;整体实现内容分析&#x1f49e;预备小知识&#x1f64c;1.链队列头文件编写&#x1f64c;2.链队列功能文件&#xff08;Queue.c &#xff09;编写&#xff1a;&#x1f64c;1&#xff09;初始化函数实现2&#xff09;销毁函…

【一些回忆】2022.02.26-2023.02.26 一个普通男孩的365天

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f91e; 作者&#xff1a;那就叫我亮亮叭 &#x1f4d5; 专栏&#xff1a;一些回忆 为什么选择在这个时间节点回忆一下呢&#xff1f; 一是因为今天距离2023高考仅剩1…

华为OD机试题,用 Java 解【删除字符串中出现次数最少的字符】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

Linux内核内存相关问题,这一篇让你彻底了解

linux 内存是后台开发人员&#xff0c;需要深入了解的计算机资源。合理的使用内存&#xff0c;有助于提升机器的性能和稳定性。本文主要介绍 linux 内存组织结构和页面布局&#xff0c;内存碎片产生原因和优化算法&#xff0c;linux 内核几种内存管理的方法&#xff0c;内存使用…

排错工具ping和trace(电子科技大学TCP/IP实验四)

一&#xff0e;实验目的 1、了解网络连通性测试的方法和工作原理 2、了解网络路径跟踪的方法和工作原理 3、掌握 MTU 的概念和 IP 分片操作 4、掌握 IP 分组生存时间&#xff08;TTL&#xff09;的含义和作用 5、掌握路由表的作用和路由查找算法 二&#xff0e;预备知识 …

[SQL Statements] 基本的SQL知识 之DDL针对数据库的基本操作

SQL Statements SQL语句的学习 之 DDL针对数据库的基本操作 什么是database 在 MySQL 中&#xff0c;Database&#xff08;数据库&#xff09;是一组有组织的数据集合&#xff0c;可以存储和管理相关数据的容器。一个数据库可以包含多个表&#xff08;Table&#xff09;&…

如何用 chatGPT,给大家来一个自我介绍

大家好&#xff0c;我是不吃西红柿的无线机械键盘&#xff0c;我的名字叫 Keychron K3 Pro。今天&#xff0c;我通过西红柿主人的手&#xff0c;使用 chatGPT 来介绍一下我自己。我的与众不同 我是由精密机械元件制作而成&#xff0c;并采用抗键渗设计&#xff0c;以提供更快、…

电子科技大学网络协议(TCP/IP作业答案)--网工(五次作业汇总)

目录 作业1&#xff1a;OSI/RM、TCP/IP编址和底层网络技术 作业2&#xff1a;IP地址规划与路由选择 作业3&#xff1a;ARP、IP、ICMP 作业4&#xff1a;UDP、Routing Protocol 作业五 作业1&#xff1a;OSI/RM、TCP/IP编址和底层网络技术 物理地址属于OSI/RM的哪一层&…

关于Linux内存管理的详细介绍,这篇万字长文值得你的收藏点赞

Linux内存管理一、Linux内存管理概述1.1 什么是内存管理1.2 内存管理的重要性1.3 内存管理的组成部分二、物理内存管理2.1 什么是物理内存2.2 物理内存管理方式2.2.1 连续内存管理2.2.2 非连续内存管理2.3 物理内存管理相关的函数及示例三、虚拟内存管理3.1 什么是虚拟内存3.2 …

HTML常用基础内容总结

文章目录一、对HTML的感性认知前置知识什么是web前端&#xff0c;什么是web后端前端技术栈、后端技术栈开发与运行的区别浏览器的功能是什么简介写一个简单可运行的的html代码前端开发方式二、VSCode的简单使用三、常用的HTML标签最最基本的HTML结构HTML代码特点注释标签标题标…

「JVM 高效并发」Java 内存模型

Amdahl 定律代替摩尔定律成为了计算机性能发展的新源动力&#xff0c;也是人类压榨计算机运算能力的最有力武器&#xff1b; 摩尔定律&#xff0c;描述处理器晶体管数量与运行效率之间的发展关系&#xff1b;Amdahl 定律&#xff0c;描述系统并行化与串行化的比重与系统运算加…