1.开发者模式webview实现

原标题:WebView 经历的各种干货方案分享孙先森Blog链接:1 前言来掘金的第一篇博客,分享下自己开发过程中对 WebView 的一些实践思路后续也会随缘在掘金更新博客,方便自己回忆的同时也能够跟大家互相交流。

2.开发者选项webview实现要开吗

WebView 系列将从零开始构建一个 Demo,所以一些不重要的代码写的略为粗糙,重在分享思路,本次博客源码会放在本系列博客最后一篇里,如有设计不足,请大家多多指教 ? ? ?2 新建工程

3.webview实现有什么用

用 AS 新建一个 Demo 工程,并且创建一个 Module 用于存放 WebView 相关代码,app 工程依赖 module_web3 WebView 基础封装 基础设置 WebSettings新建一个工具类对 WebView 进行一些基础设置,后面用。

4.webview如何使用

objectWebUtil { /** * 获取 WebView 缓存文件目录 */ fungetWebViewCachePath(context: Context) : String{ returncontext.filesDir.absolutePath +

5.开发人员选项webview

"/webCache"} fundefaultSettings(context:Context, webView: WebView) { // 白色背景webView.setBackgroundColor(Color.TRANSPARENT)

6.webview有用吗

webView.setBackgroundResource(R.color.white)webView.overScrollMode = View.OVER_SCROLL_NEVER webView.isNestedScrollingEnab

7.webview作用

led =false// 默认支持嵌套滑动// 设置自适应屏幕,两者合用webView.settings.useWideViewPort = truewebView.settings.loadWithO

8.开发者模式webview是什么

verviewMode =true// 是否支持缩放,默认为truewebView.settings.setSupportZoom( false) // 是否使用内置的缩放控件webView.settin

9.开发者选项webview实现

gs.builtInZoomControls =false// 是否显示原生的缩放控件webView.settings.displayZoomControls = false// 设置文本缩放 默认 1

10.开发者选项webview

00webView.settings.textZoom = 100// 是否保存密码webView.settings.savePassword = false// 是否可以访问文件webView.sett

ings.allowFileAccess =true// 是否支持通过js打开新窗口webView.settings.javaCanOpenWindowsAutomatically = true// 是

否支持自动加载图片webView.settings.loadsImagesAutomatically = truewebView.settings.blockNetworkImage = false//

设置编码格式webView.settings.defaultTextEncodingName = "utf-8"webView.settings.layoutAlgorithm = WebSetting

s.LayoutAlgorithm.NORMAL// 是否启用 DOM storage APIwebView.settings.domStorageEnabled = true// 是否启用 datab

ase storage API 功能webView.settings.databaseEnabled = true// 配置当安全源试图从不安全源加载资源时WebView的行为webView.setti

ngs.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW// 设置缓存模式webView.settings.cacheMode = WebSettings.LOAD_DEFAULT

// 开启 Application Caches 功能webView.settings.setAppCacheEnabled( true) // 设置 Application Caches 缓存目录val

cachePath = getWebViewCachePath(context) valcacheDir = File(cachePath) // 设置缓存目录if(!cacheDir.exists && !cacheDir.isDirectory) {

cacheDir.mkdirs } webView.settings.setAppCachePath(cachePath)} } WebChromeClient 封装继承WebChromeClient

主要实现了网页日志输出、js 弹窗拦截classBaseWebChromeClient: WebChromeClient{ privatevalTAG = "BaseWebChromeClient"/**

* 网页控制台输入日志 */ overridefunonConsoleMessage(consoleMessage:ConsoleMessage) : Boolean{ Log.d(TAG, "onConsoleMessage ->

${consoleMessage.message}" ) returnsuper.onConsoleMessage(consoleMessage)} /** * 网页警告弹框 */ overridefun

onJsAlert( view: WebView, url: String, message: String, result:JsResult) : Boolean{ AlertDialog.Builder(view.context)

.setTitle( "警告") .setMessage(message) .setPositiveButton("确认") { dialog, which -> dialog?.dismiss result.confirm

} .setNegativeButton( "取消") { dialog, which ->dialog?.dismiss result.cancel } .create .show returntrue

} /** * 网页弹出确认弹窗 */ overridefunonJsConfirm( view:WebView, url: String, message: String, result: JsResult

) : Boolean{ AlertDialog.Builder(view.context) .setTitle("警告") .setMessage(message) .setPositiveButton(

"确认") { dialog, which -> dialog?.dismiss result.confirm} .setNegativeButton( "取消") { dialog, which ->

dialog?.dismiss result.cancel } .create .show returntrue}} WebViewClient 封装WebViewClient 算是这一小节的重点了,在自定义

WebViewClient 中主要实现了两件事:网页资源文件(js、css等)缓存管理网页中的图片和原生App共用缓存(说简单点 App 用 Glide 加载图片,WebView 中的图片同样通过 Glide 加载。

classBaseWebViewClient: WebViewClient{ privatevalfileApiService bylazy { Retrofit.Builder .build .create(FileApiService::

class. java) } /** * 证书校验错误 */ @SuppressLint("WebViewClientOnReceivedSslError") overridefunonReceivedSslError

( view: WebView, handler: SslErrorHandler, error: SslError) { AlertDialog.Builder(view.context) .setTitle(

"提示") .setMessage( "当前网站安全证书已过期或不可信\n是否继续浏览?") .setPositiveButton( "继续浏览") { dialog, which -> dialog?.dismiss

handler.proceed } .setNegativeButton("返回上一页") { dialog, which -> dialog?.dismiss handler.cancel } .create

.show } @RequiresApi(Build.VERSION_CODES.M)overridefunonReceivedError( view: WebView, request: WebResourceRequest

, error: WebResourceError) { if(request.isForMainFrame) { onReceivedError( view, error.errorCode, error.deion.toString,

request.url.toString) } } overridefunonReceivedError( view: WebView?, errorCode: Int, deion: String?,

failingUrl: String?) { super.onReceivedError(view, errorCode, deion, failingUrl) } overridefunshouldOverrideUrlLoading

( view:WebView, request: WebResourceRequest) : Boolean{ returnshouldOverrideUrlLoading(view, request.url.toSt

ring)} overridefunshouldOverrideUrlLoading(view: WebView, url: String) : Boolean{ valscheme = Uri.parse(ur

l).scheme ?:returnfalsewhen(scheme) { "http", "https"-> view.loadUrl(url) // 处理其他协议//"tel" -> {}} return

true} overridefunshouldInterceptRequest( view: WebView, request: WebResourceRequest) : WebResourceRespons

e? {varwebResourceResponse: WebResourceResponse? = null// 如果是 assets 目录下的文件if(isAssetsResource(reques

t)) {webResourceResponse = assetsResourceRequest(view.context, request) } // 如果是可以缓存的文件if(isCacheResou

rce(request)) {webResourceResponse = cacheResourceRequest(view.context, request) } if(webResourceRespo

nse ==null) { webResourceResponse = super.shouldInterceptRequest(view, request) } returnwebResourceRespo

nse} privatefunisAssetsResource(webRequest: WebResourceRequest) : Boolean{ valurl = webRequest.url.toStri

ngreturnurl.startsWith( "file:///android_asset/") } /** * assets 文件请求 */ privatefunassetsResourceRequest

( context:Context, webRequest: WebResourceRequest) : WebResourceResponse? { valurl = webRequest.url.toStri

ngtry{ valfilenameIndex = url.lastIndexOf( "/") + 1valfilename = url.substring(filenameIndex) valsuffixI

ndex = url.lastIndexOf(".") valsuffix = url.substring(suffixIndex + 1) valwebResourceResponse = WebReso

urceResponse(getMimeTypeFromUrl(url), "UTF-8", context.assets. open( " $suffix/ $filename" ) ) webResourceRes

ponse.responseHeaders = mapOf("access-control-allow-origin"to "*") returnwebResourceResponse } catch(e:

Exception) {e.printStackTrace } returnnull} /** * 判断是否是可以被缓存等资源 */ privatefunisCacheResource(webRequest:

WebResourceRequest) : Boolean{ valurl = webRequest.url.toString valextension = MimeTypeMap.getFileExtensio

nFromUrl(url)returnextension == "ico"|| extension == || 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"} /** * 可缓存文件请求 */ privatefuncacheResourceRequest( context: Context, webRequest: WebResourceRequest

) : WebResourceResponse? { varurl = webRequest.url.toString varmimeType = getMimeTypeFromUrl(url)// WebView 中的图片利用 Glide 加载(能够和App其他页面共用缓存)

if(isImageResource(webRequest)) { returntry{ valfile = Glide.with(context).download(url).submit.getval

webResourceResponse = WebResourceResponse(mimeType, "UTF-8", file.inputStream) webResourceResponse.responseHeaders = mapOf(

"access-control-allow-origin"to "*") webResourceResponse } catch(e: Exception) { e.printStackTrace null

} } /** * 其他文件缓存逻辑 * 1.寻找缓存文件,本地有缓存直接返回缓存文件* 2.无缓存,下载到本地后返回 * 注意!!! * 一定要确保文件下载完整,我这里采用下载完成后给文件加 "success-" 前缀的方法

*/ valwebCachePath = WebUtil.getWebViewCachePath(context)valcacheFilePath = webCachePath + File.separator +

"success-"+ url.encodeUtf8.md5.hex// 自定义文件命名规则valcacheFile = File(cacheFilePath) if(!cacheFile.exists || !cacheFile.isFile)

{// 本地不存在 则开始下载// 下载文件valsourceFilePath = webCachePath + File.separator + url.encodeUtf8.md5.hex vals

ourceFile = File(sourceFilePath)runBlocking { try{ fileApiService.download(url, webRequest.requestHead

ers).use {it.byteStream.use { inputStream -> sourceFile.writeBytes(inputStream.readBytes) } } // 下载完成后增加

"success-" 前缀 代表文件无损 【防止io流被异常中断导致文件损坏 无法判断】sourceFile.renameTo(cacheFile) } catch(e: Exception) { e.p

rintStackTrace// 发生异常删除文件sourceFile.deleteOnExit cacheFile.deleteOnExit } } } // 缓存文件存在则返回if(cacheFile.ex

ists && cacheFile.isFile) {valwebResourceResponse = WebResourceResponse(mimeType, "UTF-8", cacheFile.i

nputStream)webResourceResponse.responseHeaders = mapOf( "access-control-allow-origin"to "*") returnwebR

esourceResponse} returnnull} /** * 判断是否是图片 * 有些文件存储没有后缀,也可以根据自家服务器域名等等 */ privatefunisImageResource(webReq

uest:WebResourceRequest) : Boolean{ valurl = webRequest.url.toString valextension = MimeTypeMap.getFileE

xtensionFromUrl(url)returnextension == "ico"|| extension == || extension == "gif"|| extension == "jpeg"

|| extension == "jpg"|| extension == "png"|| extension == "svg"|| extension == "webp"} /** * 根据 url 获取

文件类型*/ privatefungetMimeTypeFromUrl(url: String) : String { try{ valextension = MimeTypeMap.getFileExtens

ionFromUrl(url)if(extension.isNotBlank && extension != "null") { if(extension == "json") { return"applic

ation/json"} returnMimeTypeMap.getSingleton.getMimeTypeFromExtension(extension) ?: "*/*"} } catch(e: Exc

eption) {e.printStackTrace } return"*/*"} } 生命周期引入lifecycle 依赖:androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0

androidx.lifecycle:lifecycle-livedata-ktx:2.4.0新建BaseWebView 类继承自 WebView 并且实现LifecycleEventObserver接口。

classBaseWebView@JvmOverloadsconstructor( context: Context, attrs: AttributeSet? =null) : WebView(context, attrs), LifecycleEventObserver {

init { // WebView 调试模式开关setWebContentsDebuggingEnabled( true) // 不显示滚动条isVerticalScrollBarEnabled = false

isHorizontalScrollBarEnabled =false// 初始化设置WebUtil.defaultSettings(context, this) } /** * 获取当前url */

overridefungetUrl: String? {returnsuper.getOriginalUrl ?: returnsuper.getUrl } overridefuncanGoBack:

Boolean{ valbackForwardList = copyBackForwardListvalcurrentIndex = backForwardList.currentIndex - 1if

(currentIndex >= 0) { valitem = backForwardList.getItemAtIndex(currentIndex)if(item?.url == "about:blank"

) { returnfalse} } returnsuper.canGoBack} /** * 设置 WebView 生命管控(自动回调生命周期方法) */ funsetLifecycleOwner(owner:

LifecycleOwner) { owner.lifecycle.addObserver(this) } /** * 生命周期回调 */ overridefunonStateChanged(source:

LifecycleOwner, event: Lifecycle. Event) { when(event) { Lifecycle.Event.ON_RESUME -> onResume Lifecycle.Event.ON_STOP -> onPause

Lifecycle.Event.ON_DESTROY -> {source.lifecycle.removeObserver( this) onDestroy } } } /** * 生命周期 onResume

*/ @SuppressLint("SetJavaEnabled") overridefunonResume{ super.onResume settings.javaEnabled = true} /**

* 生命周期 onPause */overridefunonPause{ super.onPause } /** * 生命周期 onDestroy * 父类没有 需要自己写 */ funonDestroy

{ settings.javaEnabled =false} /** * 释放资源操作 */ funrelease{ (parent asViewGroup?)?.removeView( this) removeAllViews

stopLoading setCustomWebViewClient(null) setCustomWebChromeClient( null) loadUrl( "about:blank") clearHistory

} funsetCustomWebViewClient(client: BaseWebViewClient?) { if(client == null) { super.setWebViewClient(WebViewClient)

} else{ super.setWebViewClient(client) } } funsetCustomWebChromeClient(client: BaseWebChromeClient?)

{ super.setWebChromeClient(client)} } 4 WebView 缓存池 WebView 基础封装就搞定了,当然我写的 Demo 中的一些设置可能和实际项目中的逻辑不符,根据自己的需要修改即可,重在

思路WebView 池化的 优点: 缩短初次创建的时间不用重复创建,复用 WebView 节省内存缺点: 如放在 Application 中初始化会耗时,且官方并不推荐使用 Application 的 context 初始化 WebView。

如放在 Activity 中利用 Activity 的 Context 进行初始化,会造成内存泄漏context 问题对于 Context 问题可以通过MutableContextWrapper很好的解决,。

MutableContextWrapper 可以在初始化后随时修改上下文初始化操作放入 Application 中,临时将 WebView 的 Context 设置为 Application,当 Activity 需要获取 WebView 时替换成对应 Activity 的 Context,当 Activity 退出时为了防止内存泄漏再将 WebV。

iew 的 Context 替换回ApplicationContextWebView 回收、复用复用池新建WebViewPool 复用池类classWebViewPoolprivateconstructor

{ object{ privateconst valTAG = "WebViewPool"@Volatileprivatevarinstance: WebViewPool? = nullfungetInstance

: WebViewPool { returninstance ?: synchronized( this) { instance ?: WebViewPool.also { instance = it }

} } } privatevalsPool = Stack privatevallock = byteArrayOf privatevarmaxSize =1/** * 设置 webview 池容量

*/ funsetMaxPoolSize(size: Int) { synchronized(lock) { maxSize = size } } /** * 初始化webview 放在list中*/

funinit(context: Context, initSize: Int= maxSize) { for(i in0until initSize) { valview = BaseWebView(MutableContextWrapper(context))

view.webChromeClient = BaseWebChromeClient view.webViewClient = BaseWebViewClientsPool.push(view) } }

/** * 获取webview */ fungetWebView(context: Context) : BaseWebView { synchronized(lock) {valwebView: BaseWebView

if(sPool.size > 0) { webView = sPool.pop Log.d(TAG, "getWebView from pool") } else{ webView = BaseWebView(MutableContextWrapper(context))

Log.d(TAG, "getWebView from create") } valcontextWrapper = webView.context asMutableContextWrapper contextWrapper.baseContext = contex

t// 默认设置webView.webChromeClient = BaseWebChromeClient webView.webViewClient = BaseWebViewClient return

webView } } /** * 回收 WebView */ funrecycle(webView: BaseWebView) { // 释放资源webView.release // 根据池容量判断是否销毁 【也可以增

加其他条件 如手机低内存等等】valcontextWrapper = webView.context asMutableContextWrapper contextWrapper.baseContext

= webView.context.applicationContextsynchronized(lock) { if(sPool.size < maxSize) { sPool.push(webView

)} else{ webView.destroy } } } } 初始化Application 中:// 根据手机 CPU 核心数(或者手机内存等条件)设置缓存池容量WebViewPool.getInstance.

setMaxPoolSize(min(Runtime.getRuntime.availableProcessors,3)) WebViewPool.getInstance.init(applicatio

nContext)获取在 Activity 中:// 从缓存池获取privatevalmWebView bylazy { WebViewPool.getInstance.getWebView( this

) }// 设置生命周期监听mWebView.setLifecycleOwner( this) // 添加到 RelativeLayout 容器中mBinding.webContainer.addView(

mWebView, RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutP

arams.MATCH_PARENT) ) 回收上面封装的 WebView 已经实现了生命周期回调,那么直接在BaseWebView 的 onDestory 方法中进行回收:funonDestroy{

// 省略其他代码...WebViewPool.getInstance.recycle( this) } 5 WebView 加载速度优化 预加载 上述的BaseWebViewClient 中已经在shouldInterce

ptRequest方法中实现了资源拦截,拿到 url 由我们自己来进行下载或者读取本地缓存,那么在合适的时机将 Web 中的一些耗时的资源提前按照定义的规则下载到缓存目录,比如:公司首页的 Web 用 vue 实现,并且引入了 echarts 等等,将这些 js 文件提前下载后,当用户打开 WebView 时

shouldInterceptRequest 就直接返回本地缓存文件了由于BaseWebViewClient已经实现了下载、读取本地缓存文件的逻辑,这里就不再贴代码了本地模板的样式,比如头条的新闻详情页,大多数都是相同的样式,仅仅文字内容不同,那么可以在 assets 目录下内置一个 html 模板以及 css 样式,WebView 初始化后就可以直接加载这个 html 。

模板,当用户打开详情页时只需要请求数据,并且 set 到指定的 div 中当用户关闭页面时不释放WebView 的资源仅清除 html 中加载的数据,直接回收到复用池中这样加载详情页的速度会大大提升简单实现

下面实现一个新闻本地模板加载数据,为了效果更好加载本地模板的 WebView 新建一个新的复用池专门管理首先定义,很简单继承自BaseWebView 重写释放资源和回收方法即可,释放资源时仅通过 js 调用清空模板数据:。

class(context: Context, attrs: AttributeSet? =null) : BaseWebView(context, attrs) { overridefunrelease

{ (parent asViewGroup?)?.removeView( this) removeAllViews evaluateJava( "java:clearData") {} } override

funonDestroy{ ool 和 WebViewPool 区分开了this) } } 接着定义复用池,将上面的WebViewPool 复制一份,将其中的BaseWebView 类型改为,并且 init

初始化方法新增一句加载本地模板代码,为了节约篇幅仅贴出 init 方法:classlateWebViewPoolprivateconstructor{ // 省略其他代码...funinit(context:

Context, initSize: Int= maxSize) { for(i in0until initSize) { val// 初始化时就加载模板view.loadUrl() sPool.push(view)

} } } 同样在 Application 中做初始化和WebViewPool是一样的就不贴代码了下面轮到编写 hmtl 部分了,首先编写 css 样式,为了操作 html 元素方便 demo 中也引入了 jQuery,css 比较简单这里就不贴代码了可以从源码中获取。

接着编写template_news.html模板文件 < metacontent= "width=device-width, initial-scale=1.0, 。

maximum-scale=1.0, user-scalable=0"name= "viewport"> <!-- 引入

css --> < h2class

= "news_title"> < divclass= "news_content"

> // 填充数据方法functionsetNewsData( title, tag, content) { $( .news_title

).html(title); $(.news_tag).html(tag); $( .news_content).html(content); } // 清除数据方法functionclearData{

$( .news_title).html(``); $( .news_tag).html( ``); $( .news_content).html( ``); } 设置数据时就有两种选择了:

获取到新闻数据后通过桥接调用 js 中的 setNewsData方法填充数据获取数据的方法也写入到 html 模板中加载 js 的同时获取到数据这里仅演示第一种方法:// 获取到新闻数据后 填充数据mWebView.evaluateJava(

"java:setNewsData(` $title`, ` $tag`, ` $content`)" ) {} 效果图图片加载优化上述BaseWebClient 中已经实现了 Web 中的图片使用 Glide

加载,但是有些特殊情况,一些图片特别多的网页如果一次性把图片都加载出来那肯定会发生卡顿在网页开发中,有很多种图片懒加载的框架,这里以jquery.lazyload.js 为例对本地模板进行图片懒加载优化。

先说下思路:模板中先引入jquery.lazyload.js,获取到要显示的 html 并且 set 到模板之后遍历 html,找出所有的 img 标签根据 jquery.lazyload.js 文档对其进行修改实现图片懒加载。

【延伸一下】遍历 html 标签拿到所有 img 后,也可以通过桥接将图片 url 传递给原生,实现网页图片浏览功能首先引入jquery.lazyload.js:模板文件中引入:< src= ./js/jquery.lazyload.js。

> 在 标签中增加方法: // 设置图片懒加载functionsetImageLazyload{ jQuery( img).each( function{ var

url = jQuery( this).attr( src); jQuery( this).attr( data-original, url).removeAttr( "src"); }) jQuery(

document).ready( function{ jQuery( img).lazyload({ placeholder: data:image/gif;base64,xxx, // 占位图 base64 太长了 就不贴了

effect: fadeIn, skip_invisible: true}); }); } functionsetNewsData( title, tag, content) { // 省略其他代码...

// 填充数据完成后 调用 setImageLazyloadsetImageLazyload } 6 WebView 白屏检测原理在合适时机获取 WebView 的截图,转为 Bitmap 遍历像素点,当非

除了截图的性能损耗,像素点检测也是白屏检测中比较耗时的场景,经过实验,我们把 WebView 截图的图片进行缩小到原图的 1/6,遍历检测图片的像素点,当非白色的像素点大于 5% 的时候我们就认为是非白屏的情况,可以相对高效检测准确得出详情页是否发生了白屏。

简单实现首先在BaseWebView中定义一个白屏异常回调接口:classBaseWebView@JvmOverloadsconstructor( context: Context, attrs: AttributeSet? =

null) : WebView(context, attrs), LifecycleEventObserver {// 省略其他代码... interfaceBlankMonitorCallback{

funonBlank} privatevarmBlankMonitorCallback: BlankMonitorCallback? = nullfunsetBlankMonitorCallback(callback:

BlankMonitorCallback) { this.mBlankMonitorCallback = callback } } 接着定一个内部类继承自 Runnable ,在run方法中实现白屏检测任务:

classBaseWebView@JvmOverloadsconstructor( context: Context, attrs: AttributeSet? = null) : WebView(context, attrs),

LifecycleEventObserver {// 省略其他代码... inner classBlankMonitorRunnable: Runnable {overridefunrun{ valtask

= Thread {// 根据宽高的 1/6 创建 bitmapvaldstWidth = measuredWidth / 6valdstHeight = measuredHeight / 6valsn

apshot = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ARGB_8888)// 绘制 view 到 bitmapvalcanvas = Canvas(snapshot)

draw(canvas) // 像素点总数valpixelCount = (snapshot.width * snapshot.height).toFloat varwhitePixelCount =

0// 白色像素点计数varotherPixelCount = 0// 其他颜色像素点计数// 遍历 bitmap 像素点for(x in0until snapshot.width) {for(y in

0until snapshot.height) { // 计数 其实记录一种就可以if(snapshot.getPixel(x, y) == -1) { whitePixelCount++} else{

otherPixelCount++ } } } // 回收 bitmapsnapshot.recycle if(whitePixelCount == 0) { return@Thread} // 计算白色像素点占比 (计算其他颜色也一样)

valpercentage: Float= whitePixelCount / pixelCount * 100// 如果超过阈值 触发白屏提醒if(percentage >95) { post { mBlankMonitorCallback?.onBlank

} } } task.start } } } 继续在BaseWebView 中定义BlankMonitorRunnable对象并且增加执行和取消白屏检测的方法:classBaseWebView@JvmOverloads

constructor( context: Context, attrs: AttributeSet? =null) : WebView(context, attrs), LifecycleEventObserver {

// 省略其他代码... privatevalmBlankMonitorRunnablebylazy { BlankMonitorRunnable } /** * 调用后 * 5s 后开始执行白屏检测任务 时间可以适当修改

*/ funpostBlankMonitorRunnable{ Log.d(TAG,"白屏检测任务 5s 后执行") removeCallbacks(mBlankMonitorRunnable) postDelayed(mBlankMonitorRunnable,

5000) } /** * 取消白屏检测任务 */ funremoveBlankMonitorRunnable{ Log.d(TAG, "白屏检测任务取消执行") removeCallbacks(mBlankMonitorR

unnable)} } 现在只剩下在合适的时机调用方法即可,Demo 中给每个页面都加上了白屏检测,所以直接在BaseWebViewClient 中实现:classBaseWebViewClient:

WebViewClient{ // 省略其他代码...overridefunonPageStarted(view: WebView, url: String, favicon: Bitmap) { super

.onPageStarted(view, url, favicon)if(view isBaseWebView){ view.postBlankMonitorRunnable } } overridefun

onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) // 页面加载完成后取消任务// 放在这里其实不是最佳 页面加载完后发

生异常导致白屏的情况就检测不到了 这里只是个demoif(view isBaseWebView){ view.removeBlankMonitorRunnable } } } 使用时在 Activity 中对 W

ebView 设置白屏监听即可:mWebView.setBlankMonitorCallback( object: BaseWebView.BlankMonitorCallback { override

funonBlank{ AlertDialog.Builder( this@WebActivity) .setTitle( "提示") .setMessage( "检测到页面发生异常,是否重新加载?")

.setPositiveButton("重新加载") { dialog, _ -> dialog.dismiss mWebView.reload } .setNegativeButton( "返回上一页"

) { dialog, _ ->dialog.dismiss onBackPressed } .create .show } }) 7 最后 本篇博客主要实现了:WebView 缓存管理、和原生部分共用图片缓存。

WebView 生命周期回调WebView 复用池 回收 复用网页秒开(主要是本地模板这种情况)、图片懒加载白屏检测这些东西也不难,很早网络上就有大佬分享这些思路,算是站在巨人的肩膀上总结实践一波吧由于篇幅原因下一

篇再接着分享WebView 独立进程以及跨进程通信的实现,文中的源码: 如果我的博客分享对你有点帮助,不妨点个赞支持下参考满满的WebView优化干货,让你的H5实现秒开体验Android WebView H5 秒开方案总结。

fragmjectcom/miaowmiaow/fragmjectAndroidProject-Kotlin最后推荐一下我做的网站,玩Android:: 全网最优雅Android列表项可见性检测 精选国外大厂面试题,你

能说出正确答案吗?一看就会 单Activity+多Fragment框架下的通信问题 扫一扫如果你想要跟大家分享你的文章,欢迎投稿~┏(^0^)┛明天见!