秒开WebView,5种混合开发动态更新方式,直击痛点,有你想要的?

1,610 阅读8分钟

caj.jpeg

WebView让人叫苦连天,怎奈各大厂仍对其锲而不舍? 怎么既保证动态更新,又要保证加载速度

一、前言

WebView自出生以来都是Android应用开发中的一个重要组件,它用于加载和显示网页。

  • 尽管WebView有许多问题和限制,但是各大厂家依然对其保持了锲而不舍的追求
  • 尽管现在已有成熟的小程序,Uni-app,Flutter ,React native,Weex等完美替代方案,但是各大头部App依然可见Webview的倩影

本文重点介绍 WebView 混合开发动态更新6种方式,怎么既要保证加载速度,又要保证可动态更新:

webview22.png

二、WebView 最简单用法(方式一):

webView.loadUrl("http://www.google.com/");
直接加载服务端上面的网页,这样网页中包含的资源图片,js,css完全由WebView自身内部去加载,大多数情况下,网页只需要设计布局做成适配Android系统屏幕的形式就可以了。
其最大的好处是:

  1. 完全动态在服务端部署,其客户端,只给提供了一个Webview套用网页的容器
  2. 兼容性:WebView提供了一种简单的方式来嵌入网页,并且可以在多种版本的Android设备上使用。
  3. 功能丰富:除了基本的网页浏览,WebView还可以通过JavaScript接口与网页内容交互,实现复杂的应用功能。
  4. 性能优化:相较于完全自己从头开始实现网页渲染的方式,WebView可以复用现代浏览器的高效渲染引擎。
  5. 开发快速:开发者可以利用现有的Web技术和框架(如JavaScript, HTML, CSS)来构建应用界面,减少了从头开始的开发时间。
  6. 社区支持:WebView有广泛的社区支持,如果遇到问题,可以在Stack Overflow等平台找到很多类似问题和解决方案。
  7. 更新迭代:随着Android系统的更新,WebView组件会得到持续的更新和改进,以保证兼容性和性能。

但是:这样做的最大缺点就是:
页面加载慢,
渲染速度也慢,
导致体验差,

于是:就有了下面的方式出现了

三、资源全部内置到Assets下面 (方式二)

  • WebView所需要的资源 图片,html,JS,CSS 全部内置到Androidassets下面,
  • 让其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等,
  • 这些库资源拦截走本地: 拦截WebViewClientshouldInterceptRequest方法,让其加载本地资源:

@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里面 (方式四)

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卡上面的) 1c69d59c-64e9-4fa2-9637-0703ae24fe60.jpeg

八、总结:

本文重点介绍了:WebView 动态化实现的6种方式:

  1. 方式一:基本使用最原始:直接加载服务器url,但是加载太慢
  2. 方式二:所有资源放入包内assets下面:加载快,固定的,失去了动态更新功能
  3. 方式三:部分固定资源仿佛assets下面,加载时拦截资源,先走本地,甚至网络数据请求来源可以全走原生,体验好,不能全动态更新
  4. 方式四:全动态化,所有资源打包成插件apk,调用webview端及交互端也打包成全动态化,参考(二)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
  5. 方式五:所有资源可以单个下载到本地,无需下载方式四中整体
  6. 方式六:就是android部分方法可以,直接写成js方法,直接代码里面读取文件调用方法,这些不能涉及到android相关组件,因为不使用webview交互,外部js拿取不到,原生组件,哪些可用用呢:计算啊,数据转化处理等,网页上的一些加密

感谢阅读:

欢迎 点赞、收藏、关注

这里你会学到不一样的东西