WebView让人叫苦连天,怎奈各大厂仍对其锲而不舍? 怎么既保证动态更新,又要保证加载速度
一、前言
WebView
自出生以来都是Android
应用开发中的一个重要组件,它用于加载和显示网页。
- 尽管
WebView
有许多问题和限制,但是各大厂家依然对其保持了锲而不舍的追求 - 尽管现在已有成熟的
小程序,Uni-app,Flutter ,React native,Weex
等完美替代方案,但是各大头部App
依然可见Webview
的倩影
本文重点介绍 WebView
混合开发动态更新6种方式,怎么既要保证加载速度,又要保证可动态更新:
二、WebView 最简单用法(方式一):
webView.loadUrl("http://www.google.com/");
直接加载服务端上面的网页,这样网页中包含的资源图片,js,cs
s完全由WebView
自身内部去加载,大多数情况下,网页只需要设计布局做成适配Android
系统屏幕的形式就可以了。
其最大的好处是:
- 完全动态在服务端部署,其客户端,只给提供了一个
Webview
套用网页的容器 - 兼容性:
WebView
提供了一种简单的方式来嵌入网页,并且可以在多种版本的Android
设备上使用。 - 功能丰富:除了基本的网页浏览,
WebView
还可以通过JavaScript
接口与网页内容交互,实现复杂的应用功能。 - 性能优化:相较于完全自己从头开始实现网页渲染的方式,
WebView
可以复用现代浏览器的高效渲染引擎。 - 开发快速:开发者可以利用现有的
Web
技术和框架(如JavaScript, HTML, CSS
)来构建应用界面,减少了从头开始的开发时间。 - 社区支持:
WebView
有广泛的社区支持,如果遇到问题,可以在Stack Overflow
等平台找到很多类似问题和解决方案。 - 更新迭代:随着
Android
系统的更新,WebView
组件会得到持续的更新和改进,以保证兼容性和性能。
但是:这样做的最大缺点就是:
页面加载慢,
渲染速度也慢,
导致体验差,
于是:就有了下面的方式出现了
三、资源全部内置到Assets下面 (方式二)
- 将
WebView
所需要的资源 图片,html,JS,CSS
全部内置到Android
的assets
下面, - 让其
WebView
加载耗时,从远程服务器,切换到本地内置,节省大部分网络消耗时间,只有渲染时间 - 具体做法:
加载assets下html模版:webView.loadUrl("file:///android_asset/test.html");
里面所引用的js,css,图片等相对路径就是assets下面
这样做的好处,大大提高了页面加载速度,让用户体验变得好很多了
但是:缺点是:全部内置了到apk内了,没法动态更新
于是:又有了下面方式:
四、部分固定资源本地化:(方式三)
- 部分资源本地化:包含哪些:固定的
JS
库,如:vue.js,swiper.min.js,iscroll.js,css
库如swiper.min.css
等, - 这些库资源拦截走本地:
拦截
WebViewClient
的shouldInterceptRequest
方法,让其加载本地资源:
@RequiresApi(Build.VERSION_CODES.N)
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest?): WebResourceResponse? {
val url = request?.url.toString()
val lastSlash: Int = url.lastIndexOf("/")
if (lastSlash != -1) {
val suffix: String = url.substring(lastSlash + 1)
if (suffix.endsWith(".css")) {
if (strOfflineResources.contains(suffix)) {
val mimeType = "text/css"
val offlineRes = "css/"
val inputs = assets.open("$offlineRes$suffix")
return WebResourceResponse(mimeType, "UTF-8", inputs)
} else {
android.util.Log.e("ImplWebViewClient", "request css :${url}")
}
}
if (suffix.endsWith(".js")) {
if (strOfflineResources.contains(suffix)) {
val mimeType = "application/x-javascript"
val offlineRes = "js/"
val inputs = assets.open("$offlineRes$suffix")
return WebResourceResponse(mimeType, "UTF-8", inputs)
} else {
android.util.Log.e("ImplWebViewClient", "request js :${url}")
}
}
if (suffix.endsWith(".png")) {
if (strOfflineResources.contains(suffix)) {
val mimeType = "image/png";
val offline_res = "img/";
val inputs = assets.open("$offlineRes$suffix")
return WebResourceResponse(mimeType, "UTF-8", inputs)
}
}
}
return super.shouldInterceptRequest(view, request)
}
- 甚至可以让其服务端只提供布局
Html模版
,UI界面
请求和原生交互,将网络请求方法,请求头,请求参数等全部通过交互方式传递给原生,
让其原生统一管理其登录token
或者某些头信息里面还有设备信息
等
要提前原生预置好原生交互的一套网络请求逻辑
- 这样的方式:再复杂的页面,一个网页
UI布局模版
基本也就25k
左右,请求数据全部原生提供 - 好处是:最大化的减少
WebView解释性语言的翻译过程
,让体验达到最佳
五、资源全部打包到插件apk里面 (方式四)
-
资源全部打包到插件apk里面assets下面,整体方式思路和方式二一样,唯一区别,方式二是在宿主包的assets下,现在是在插件apk的assets下面,其实这种方式我前面文章有讲过,
大型项目架构:全动态插件化+模块化+Kotlin+协程+Flow+Retrofit+JetPack+MVVM+极限瘦身+极限启动优化+架构示例+全网唯一
大型项目架构:解析全动态插件化框架WXDynamicPlugin是如何做到全动态化的? -
主要是要拿到插件包的
Resource
,然后拿到插件包Resource的Assets
-
具体操作: 获取插件中
Resourse
的核心代码如下:
fun getWebRes(): Resources {
val file = DynamicManageUtils.getDxFile(context, dldir, webResFileName)
val flags = (PackageManager.GET_META_DATA or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES
or PackageManager.GET_PROVIDERS or PackageManager.GET_RECEIVERS)
val packageManager = context.applicationContext.packageManager
val packageInfo = packageManager.getPackageArchiveInfo(file.absolutePath, flags)
val applicationInfo = packageInfo!!.applicationInfo
applicationInfo.publicSourceDir = file.absolutePath
applicationInfo.sourceDir = applicationInfo.publicSourceDir
MMKVHelp.saveWebResPath(file.absolutePath)
return packageManager.getResourcesForApplication(applicationInfo)
}
- 使用的时候:把
assets
替换成插件外部的Assets
class ImplWebViewClient : WebViewClient() {
private val strOfflineResources by lazy { MMKVHelp.getJsPath() }
private val webResAssets by lazy { PluginManager.instance.getWebRes().assets }
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = true
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
view.loadUrl("javascript:loadImage()")
}
@RequiresApi(Build.VERSION_CODES.N)
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest?): WebResourceResponse? {
val url = request?.url.toString()
val lastSlash: Int = url.lastIndexOf("/")
if (lastSlash != -1) {
val suffix: String = url.substring(lastSlash + 1)
if (suffix.endsWith(".css")) {
if (strOfflineResources.contains(suffix)) {
val mimeType = "text/css"
val offlineRes = "css/"
val inputs = webResAssets.open("$offlineRes$suffix")
return WebResourceResponse(mimeType, "UTF-8", inputs)
} else {
android.util.Log.e("ImplWebViewClient", "request css :${url}")
}
}
if (suffix.endsWith(".js")) {
if (strOfflineResources.contains(suffix)) {
val mimeType = "application/x-javascript"
val offlineRes = "js/"
val inputs = webResAssets.open("$offlineRes$suffix")
return WebResourceResponse(mimeType, "UTF-8", inputs)
} else {
android.util.Log.e("ImplWebViewClient", "request js :${url}")
}
}
}
return super.shouldInterceptRequest(view, request)
}
}
-
甚至整个
Webview
的调用处都可以插件化,和原生交互都可以插件化,
就是一个纯webView
调用代码插件化过程,可以参考我前面文章 Compose插件化:一个Demo带你入门Compose,同时带你入门插件化开发 -
这样所有资源包都在插件apk里面,甚至
webview
调用的地方也可以全部插件化了,实现全动态化了,不单单是webview
了,所有android
代码都可以全动态化了 -
此种方式:优点:全动态插件化式更新,
webview
的全动态化只是其中的一种功能 -
此种方式:缺点:每次更新资源需要
插件apk
全部下载更新,不太方便
于是又有了下面方案
六、资源全单独化可下载到本地SD卡 (方式五)
- 此种方式:所有资源全部可单独下载下来,无需整体打包
- 第一次怎么办?可以预置放入
assets
下面,下一次当SD卡上有了
,就不走assets
下面逻辑了 - SD卡加载具体事项很简单
WebView webView = findViewById(R.id.webview);
webView.getSettings().setAllowFileAccess(true);
// 假设你的HTML文件名为index.html,存储在SD卡的根目录下
File sdCard = Environment.getExternalStorageDirectory();
File htmlFile = new File(sdCard.getAbsolutePath(), "index.html");
// 使用WebView加载SD卡上的HTML文件
webView.loadUrl("file://" + htmlFile.getAbsolutePath());
- 另外:如果不是系统目录,需要加读取SD卡权限
七、不使用WebView,代码怎么直接调用Js方法呢? (方式六)
- 为什么介绍此种方式呢?
有一部分前端转到
android
上面来的,对纯android原生开发
不是很熟练,特别是对原生插件化开发不是很精通,但是有些部分功能,或者说某些方法,想动态修改,但是自己最精通的是JS
,怎么办呢?
- 介绍个工具:直接调用js文件内的方法
- 具体操作:
引入
implementation 'org.mozilla:rhino:1.7.15'
这样使用:
class MainActivity : AppCompatActivity() {
val jsContext by lazy { org.mozilla.javascript.Context.enter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
findViewById<MaterialButton>(R.id.btn).setOnClickListener {
lifecycleScope.launch {
val data = callJsFunction("add", "4")
Toast.makeText(this@MainActivity, data, Toast.LENGTH_LONG).show()
}
}
}
private suspend fun callJsFunction(functionName: String, vararg data: String): String {
return withContext(Dispatchers.IO) {
jsContext.setOptimizationLevel(-1) // 关闭优化
try {
// 获取全局对象
val scope: Scriptable = ContextFactory().enterContext().initStandardObjects()
// 读取JavaScript文件
//val fis = FileInputStream("xxxx/script.js")
val fis = assets.open("My.js")
val reader: Reader = InputStreamReader(fis, "UTF-8")
// 编译并执行脚本
val script: Script = jsContext.compileReader(reader, "script", 1, null)
script.exec(jsContext, scope)
val function = scope.get(functionName, scope) as org.mozilla.javascript.Function
val result = function.call(jsContext, scope, scope, data)
result.toString()
} catch (e: Exception) {
e.printStackTrace()
""
}
}
}
override fun onDestroy() {
super.onDestroy()
// 退出Rhino上下文
Context.exit()
}
}
- 外部js文件式这样的(当然可以是下载到sd卡上面的)
八、总结:
本文重点介绍了:WebView
动态化实现的6种方式:
- 方式一:基本使用最原始:直接加载
服务器url
,但是加载太慢 - 方式二:所有资源放入包内
assets
下面:加载快,固定的,失去了动态更新功能 - 方式三:部分固定资源仿佛
assets
下面,加载时拦截资源,先走本地,甚至网络数据请求来源可以全走原生,体验好,不能全动态更新 - 方式四:全动态化,所有资源打包成
插件apk
,调用webvie
w端及交互端也打包成全动态化,参考(二)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构 - 方式五:所有资源可以单个下载到本地,无需下载方式四中整体
- 方式六:就是
android部分方
法可以,直接写成js方法
,直接代码里面读取文件调用方法,这些不能涉及到android
相关组件,因为不使用webview
交互,外部js拿取不到,原生组件,哪些可用用呢:计算啊,数据转化处理等
,网页上的一些加密
等