5月28日 00:28

如何正确管理WebView的生命周期?

创建与初始化

WebView的创建时机和初始化方式直接影响应用启动速度和内存占用。核心原则:延迟创建、按需初始化、避免主线程阻塞。

kotlin
// 推荐:懒加载方式创建WebView class WebViewFragment : Fragment() { private var webView: WebView? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) webView = WebView(requireContext()).apply { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.cacheMode = WebSettings.LOAD_DEFAULT settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW webViewClient = CustomWebViewClient() webChromeClient = CustomWebChromeClient() } } }

初始化时有几个容易踩的坑:

  • 不要在Application中提前创建WebView实例,这会显著增加冷启动时间。如果只是想预热内核,可以在后台线程创建后立即销毁
  • setJavaScriptEnabled(true) 必须显式调用,默认是关闭的,很多页面功能依赖JS运行
  • setDomStorageEnabled(true) 对于现代网页几乎是必须的,大量网站使用localStorage
  • 避免在构造函数中做耗时配置,放在onViewCreatedonCreate之后执行

活动状态管理

WebView必须与Activity/Fragment的生命周期方法同步调用,否则会出现音频后台继续播放、定时器持续运行、不可见时仍消耗CPU等问题。

kotlin
override fun onResume() { super.onResume() webView?.onResume() webView?.resumeTimers() } override fun onPause() { webView?.pauseTimers() webView?.onPause() super.onPause() }

这两个方法的作用范围必须区分清楚:

  • onPause()/onResume() 是实例级别,只影响当前WebView的暂停和恢复
  • pauseTimers()/resumeTimers() 是全局级别,会暂停或恢复应用内所有WebView的JavaScript定时器和布局渲染
  • pauseTimers() 要谨慎使用,因为它会影响同一进程内的所有WebView实例,包括其他页面中正在使用的WebView

实际开发中更推荐的做法是使用Lifecycle组件自动管理:

kotlin
class WebViewLifecycleObserver(private val webView: WebView?) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { webView?.onResume() webView?.resumeTimers() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { webView?.pauseTimers() webView?.onPause() } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { webView?.destroy() } }

这样只需要在Activity中注册观察者,不必手动在每个生命周期回调中同步调用。

销毁与释放

WebView的销毁是最容易出内存泄漏的环节。销毁顺序不对或引用没清理,WebView会持有Activity的Context导致整个页面无法被GC回收。

kotlin
override fun onDestroyView() { webView?.let { wv -> // 第一步:先从父容器移除,断开View树引用 (wv.parent as? ViewGroup)?.removeView(wv) // 第二步:停止加载并清理状态 wv.stopLoading() wv.settings.javaScriptEnabled = false wv.clearHistory() wv.clearCache(true) wv.loadUrl("about:blank") wv.removeAllViews() // 第三步:销毁WebView实例 wv.destroy() } webView = null // 关键:置空引用 super.onDestroyView() }

loadUrl("about:blank") 这一步经常被遗漏,它的作用是中断当前页面的JS执行和资源加载,确保destroy()时不会有回调继续触发。

常见泄漏场景及排查:

  • Activity被销毁但WebView仍持有Context引用 → 使用独立进程或ApplicationContext
  • JavaScript回调通过addJavascriptInterface持有Activity引用 → 销毁前调用removeJavascriptInterface
  • WebView还在加载时直接调用destroy()会抛异常 → 必须先stopLoading()

内存泄漏防范

WebView是Android中内存泄漏的重灾区,尤其在Fragment中使用时风险更高。以下三种方案按推荐程度排序:

方案一:独立进程(最推荐)

xml
<activity android:name=".WebViewActivity" android:process=":webview" />

进程独立后,Activity销毁时整个WebView进程可以被系统杀掉回收,从根本上杜绝泄漏。缺点是进程间通信需要走AIDL或Messenger,复杂度上升。

方案二:动态添加 + ApplicationContext

kotlin
class SafeWebViewActivity : AppCompatActivity() { private var webView: WebView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val container = FrameLayout(this) setContentView(container) // 关键:使用ApplicationContext避免持有Activity引用 webView = WebView(this.applicationContext) container.addView(webView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) } override fun onDestroy() { webView?.let { wv -> (wv.parent as? ViewGroup)?.removeView(wv) wv.destroy() } webView = null super.onDestroy() } }

注意:ApplicationContext会导致WebView中的弹窗(如文件选择器)无法弹出Activity,需要单独处理onShowFileChooser等回调。

方案三:WebView池复用

kotlin
object WebViewPool { private const val MAX_POOL_SIZE = 2 private val pool = Stack<WebView>() fun obtain(context: Context): WebView { return if (pool.isNotEmpty()) { pool.pop() } else { WebView(context.applicationContext) } } fun recycle(webView: WebView) { if (pool.size >= MAX_POOL_SIZE) { webView.destroy() return } webView.stopLoading() webView.clearHistory() webView.loadUrl("about:blank") pool.push(webView) } }

池化方案适合高频打开WebView的场景(如信息流),但要注意控制池大小,避免常驻内存过多。

配置变更处理

屏幕旋转等配置变更会导致Activity重建,如果WebView状态没有保存恢复,用户会丢失当前页面和滚动位置。

kotlin
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) webView?.saveState(outState) } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) savedInstanceState?.let { webView?.restoreState(it) } }

saveState/restoreState保存的是WebView的前进后退历史和表单数据,但不保存页面内容本身。对于复杂页面,更好的做法是避免Activity重建:

xml
<activity android:name=".WebViewActivity" android:configChanges="orientation|screenSize|keyboardHidden|layoutDirection|locale" />

这样配置变更时Activity不会销毁重建,WebView状态自然保留。但要注意此时布局需要自行适配新配置,系统不会自动重新创建View。

如果项目使用ViewModel,还可以结合ViewModel持有WebView的数据状态,在重建后恢复URL和滚动位置:

kotlin
class WebViewViewModel : ViewModel() { var currentUrl: String = "" var scrollPosition: Int = 0 }

异常处理与安全

WebView加载外部内容时,必须处理各种加载异常和安全风险,否则应用可能崩溃或被恶意网页利用漏洞攻击。

kotlin
webView.webViewClient = object : WebViewClient() { override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError? ) { // 只处理主页面错误,子资源(图片、CSS等)加载失败不应替换整个页面 if (request?.isForMainFrame == true) { view?.loadUrl("file:///android_asset/error.html") } } override fun onReceivedHttpError( view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse? ) { // 处理HTTP错误码(如404、500) if (request?.isForMainFrame == true) { val statusCode = errorResponse?.statusCode ?: return // 根据状态码展示不同的错误页面 } } override fun onReceivedSslError( view: WebView?, handler: SslErrorHandler?, error: SslError? ) { // 严禁忽略SSL证书错误,这是常见的中间人攻击漏洞 handler?.cancel() } override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? ): Boolean { val url = request?.url?.toString() ?: return false // 只允许HTTP/HTTPS协议,拦截scheme跳转防止隐式Intent攻击 return when { url.startsWith("http://") || url.startsWith("https://") -> { view?.loadUrl(url) true } else -> true // 拦截tel:, sms:, intent:等协议 } } }

安全配置清单:

  • setAllowFileAccess(false) — 禁止WebView访问本地文件系统
  • setAllowContentAccess(false) — 禁止访问ContentProvider
  • setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW) — 禁止HTTPS页面加载HTTP资源
  • API 17以下不要使用addJavascriptInterface,存在远程代码执行漏洞(CVE-2012-6636)
  • 对用户输入的URL做白名单校验,防止加载恶意页面

预加载与性能优化

WebView首次初始化耗时可达200-500ms,预加载策略可以显著提升页面打开速度。不同场景适合不同策略:

轻量预初始化:内核预热

kotlin
class MyApp : Application() { override fun onCreate() { super.onCreate() // 在后台线程预初始化WebView内核,开销极小 Thread { WebView(this).destroy() }.start() } }

这种方式只是提前初始化了WebView的底层Chromium内核,不创建常驻实例,适合大多数应用。

中量方案:WebView池

kotlin
// 在Application中预创建1-2个WebView放入池中 class MyApp : Application() { override fun onCreate() { super.onCreate() WebViewPool.preload(this) } }

适合信息流等高频打开WebView的场景,但要注意常驻内存开销,池大小建议不超过2。

重量方案:独立进程预加载

适合对稳定性要求极高的大型应用(如微信、支付宝),在独立进程中预创建WebView,通过IPC通信传递加载请求。架构复杂但隔离性最好。

三种方案对比:

策略预加载耗时内存开销适用场景
内核预热200-500ms极低普通应用
WebView池<50ms中等信息流、高频WebView
独立进程<50ms大型应用、稳定性优先

选择建议:大多数应用用内核预热即可;打开WebView频率高的场景推荐WebView池;对稳定性有极端要求的考虑独立进程方案。

追问:WebView与Native通信有哪些方式?

WebView与Native的通信是面试高频追问,核心方案有三类:

  1. JavaScript Interface:Native通过addJavascriptInterface注入对象,JS直接调用。简单但API 17以下有安全漏洞,需要加@JavascriptInterface注解
  2. shouldOverrideUrlLoading:JS通过修改window.location触发Native拦截。安全但只能传递字符串,且URL长度有限制
  3. evaluateJavascript / loadUrl("javascript:..."):Native主动调用JS方法。evaluateJavascript可获取返回值,loadUrl方式不能

实际项目中推荐组合使用:JS调Native用JavaScript Interface(注意安全校验),Native调JS用evaluateJavascript(可获取回调结果)。

标签:Webview