网页浏览
网页浏览:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“网页浏览”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 WebView 的加载、JavaScript 交互、安全配置与性能优化——让 Android 应用内嵌完整的 Web 浏览能力。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用导航来源、URL边界、历史栈、离线与TLS失败独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么需要 WebView——当原生 UI 不够用时
一个成熟的 App 几乎不可能只靠原生控件。展示运营活动页、加载隐私协议、嵌入在线客服、甚至直接打开第三方网站——这些场景的共同答案是:在 App 里放一个浏览器。
Android 把 Chromium 内核打包成了 WebView 控件:一个小巧但完整的浏览器引擎。你可以把它当成一个 <iframe> 用在 App 里,加载 URL、执行 JavaScript、跟原生代码双向通信。
如果没有 WebView,这些需求只能用原生代码从头画——运营页每次改版都得发 App 新版本,隐私协议更新也得走应用商店审核。WebView 给 Android 开发者开了后门:不改 App 代码就能更新界面。
本章从加载网页开始,一路讲到 JavaScript 双向通信和安全配置——这是所有商用 App 都绕不开的技能。
WebView 是 App 内嵌的浏览器引擎,处在所有角色的正中央: loadUrl() / loadData() 把内容塞进去、 WebSettings 用 javaScriptEnabled 等开关划定能力边界; WebViewClient(shouldOverrideUrlLoading 拦跳转、 onPageFinished)与 WebChromeClient(进度 / 标题 / JS alert)接收它的事件回调;而 addJavascriptInterface 在 JS 与原生之间架起双向桥—— 强大但危险,等于把原生方法暴露给网页,务必谨慎控制注入对象的权限。① WebViewClient 拦截 URL
页面链接被点击时,WebView 默认会跳转到系统浏览器。通过
WebViewClient.shouldOverrideUrlLoading() 拦截 URL 请求并返回
false,让链接留在 WebView 内部跳转,用户体验就像在使用一个内置浏览器。
1. WebView 基础——加载网页
最简单的 WebView
在布局里放一个 <WebView>,然后 findViewById 拿到它,调用 loadUrl():
// activity_main.xml
//
class WebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val webView = findViewById<WebView>(R.id.webView)
webView.loadUrl("https://www.example.com")
}
}就这么简单——但这样写出来的 WebView 的行为会让你抓狂:点击网页里的链接?跳出到系统浏览器。页面加载成功/失败?没有任何回调。网页标题?拿不到。进度条?没有。
你需要两个 Client 来完成 WebView 的定制:WebViewClient 和 WebChromeClient。
WebViewClient——控制页面加载
↡拦截 URL 加载、处理页面请求和 SSL 错误的回调类 负责页面导航相关的事件。最常用的三个回调:
webView.webViewClient = object : WebViewClient() {
// 1. 拦截 URL 加载——让链接在 WebView 内部跳转,而不是跳系统浏览器
override fun shouldOverrideUrlLoading(
view: WebView?, request: WebResourceRequest?
): Boolean {
// 返回 false 让 WebView 自己处理;返回 true 拦截
return false
}
// 2. 页面开始加载
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
progressBar.visibility = View.VISIBLE
}
// 3. 页面加载完成
override fun onPageFinished(view: WebView?, url: String?) {
progressBar.visibility = View.GONE
}
// 4. 处理 SSL 错误——这里绝不能草率 proceed()
override fun onReceivedSslError(
view: WebView?, handler: SslErrorHandler?, error: SslError?
) {
// 生产环境应该提示用户并取消,而不是直接 proceed()
handler?.cancel()
showSslErrorDialog()
}
}WebChromeClient——处理 UI 相关回调
↡处理 WebView 中与浏览器 UI 相关的回调(进度条、标题、JS 对话框、文件选择等)负责进度、标题、JS 对话框等"浏览器 UI"相关的事件:
webView.webChromeClient = object : WebChromeClient() {
// 1. 获取加载进度(0-100)
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.progress = newProgress
if (newProgress == 100) progressBar.visibility = View.GONE
}
// 2. 获取网页标题
override fun onReceivedTitle(view: WebView?, title: String?) {
supportActionBar?.title = title
}
// 3. 处理 JavaScript alert()
override fun onJsAlert(
view: WebView?, url: String?, message: String?,
result: JsResult?
): Boolean {
AlertDialog.Builder(this@WebViewActivity)
.setMessage(message)
.setPositiveButton("OK") { _, _ -> result?.confirm() }
.show()
return true // 返回 true 表示已处理,不要系统的默认弹窗
}
}加载本地 HTML 字符串
除了 loadUrl(),还可以用 loadData() 或 loadDataWithBaseURL() 直接加载 HTML 字符串——这在显示富文本、隐私协议等场景非常实用:
val htmlContent = """
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>用户协议</h1>
<p>这是用 WebView 直接渲染的 HTML 内容。</p>
</body>
</html>
""".trimIndent()
// loadData 可能乱码;推荐用 loadDataWithBaseURL 指定编码
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)2. JavaScript 交互——原生与 Web 的桥梁
这是 WebView 最有价值的部分:让 Android 代码和网页里的 JavaScript 可以互相调用。
方向一:Android 调用 JavaScript
用 <Term def="在 WebView 的 JS 环境中执行一段 JavaScript 代码字符串并获取返回值">evaluateJavascript</Term> 在 WebView 里执行 JS:
// Android → JS:调 JS 函数,拿返回值
webView.evaluateJavascript(
"(function() { return document.title; })();"
) { result ->
// result 是 JSON 字符串,比如 "\"我的网页标题\""
val title = result.removeSurrounding("\"")
titleTextView.text = title
}evaluateJavascript 是异步的——它在 WebView 的渲染线程上执行 JS,结果通过回调返回。不要在 onPageFinished 之前调用它,否则 JS 环境还没准备好。
方向二:JavaScript 调用 Android
用 <Term def="将 Java/Kotlin 对象暴露给 WebView 中的 JavaScript,使 JS 可以直接调用原生方法">addJavascriptInterface</Term> 把 Kotlin 对象暴露给 JS:
// Kotlin 端:定义一个给 JS 调用的接口类
class WebAppInterface(private val context: Context) {
@JavascriptInterface
fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
@JavascriptInterface
fun getDeviceInfo(): String {
return "${Build.MANUFACTURER} ${Build.MODEL}"
}
}
// 注册:把对象以 "Android" 为名字暴露到 JS 全局作用域
webView.addJavascriptInterface(WebAppInterface(this), "Android")JS 端这样调用:
// Web 页面里的 JS 代码
document.getElementById("btn").onclick = function () {
// Android 是 addJavascriptInterface 注册的名字
Android.showToast("按钮被点了!");
var info = Android.getDeviceInfo();
console.log(info);
};WebView 设置——按需打开功能
WebView 默认禁用 JavaScript、禁用缩放、不保存表单数据。需要显式配置:
webView.settings.apply {
javaScriptEnabled = true // 开启 JavaScript(默认 false)
domStorageEnabled = true // 开启 DOM Storage(HTML5 localStorage)
allowFileAccess = false // 禁止访问文件系统(安全加固)
allowContentAccess = false // 禁止访问 content:// URI
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW // 禁止 HTTP/HTTPS 混用
cacheMode = WebSettings.LOAD_DEFAULT // 使用默认缓存策略
setSupportZoom(true) // 允许缩放
builtInZoomControls = true // 内置缩放控件
displayZoomControls = false // 隐藏缩放按钮(手势缩放仍有效)
useWideViewPort = true // 支持 viewport 标签
loadWithOverviewMode = true // 缩放内容以适应屏幕
}3. 返回键处理——让 WebView 的返回先于 Activity
WebView 里用户浏览了好几页后按返回键——默认行为是直接关闭 Activity。这体验很差。应该让 WebView 先消费返回事件:
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack() // 回到上一页
} else {
super.onBackPressed() // 没有历史了就退出 Activity
}
}canGoBack() 检查是否有可返回的页面历史;goBack() 返回上一页。对应的还有 canGoForward() 和 goForward()——和普通浏览器完全一样。
代码完整示例
下面是一个配置齐全的 WebView Activity 完整代码:
class WebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_webview)
webView = findViewById(R.id.webView)
progressBar = findViewById(R.id.progressBar)
configureWebView()
webView.loadUrl("https://www.example.com")
}
private fun configureWebView() {
// 基础设置
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = false
cacheMode = WebSettings.LOAD_DEFAULT
}
// 页面导航
webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
progressBar.visibility = View.VISIBLE
}
override fun onPageFinished(view: WebView?, url: String?) {
progressBar.visibility = View.GONE
}
override fun onReceivedSslError(
view: WebView?, handler: SslErrorHandler?, error: SslError?
) {
if (BuildConfig.DEBUG) handler?.proceed()
else handler?.cancel()
}
}
// UI 回调
webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.progress = newProgress
}
override fun onReceivedTitle(view: WebView?, title: String?) {
supportActionBar?.title = title
}
}
// JavaScript 接口
webView.addJavascriptInterface(
WebAppInterface(this), "Android"
)
}
override fun onBackPressed() {
if (webView.canGoBack()) webView.goBack()
else super.onBackPressed()
}
override fun onDestroy() {
// 销毁 WebView,释放资源
webView.destroy()
super.onDestroy()
}
}容易踩的坑
小结
- WebView 是 Android 内嵌的浏览器引擎——
loadUrl加载网页,loadDataWithBaseURL加载 HTML 字符串 WebViewClient控制页面加载行为(URL 拦截、SSL 错误处理);WebChromeClient处理 UI 回调(进度、标题、JS 弹窗)- Android→JS 用
evaluateJavascript;JS→Android 用addJavascriptInterface(加@JavascriptInterface注解) - 安全铁律:生产环境绝不
handler.proceed()SSL 错误;allowFileAccess设为 false;只暴露必要方法给 JS - 性能:开启硬件加速;
onDestroy中调webView.destroy()防止内存泄漏
练习
问题 1:“第29章 Browsing the Web and WebView”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- WebViewClient
WebView 的导航管家。拦截 URL 加载(
shouldOverrideUrlLoading)、处理页面开始/结束事件、处理 SSL 证书错误等。类比:浏览器地址栏的"逻辑层"。- WebChromeClient
WebView 的 UI 管家。负责进度条更新、标题获取、JavaScript alert/prompt 弹窗、文件选择器等跟"浏览器外观"相关的回调。类比:浏览器的标签页和弹窗控制。
- evaluateJavascript
在 WebView 的 JS 引擎里异步执行一段 JS 代码字符串,结果通过回调返回(JSON 格式)。必须在
onPageFinished之后调用,否则 JS 环境未就绪。- addJavascriptInterface
把一个 Kotlin/Java 对象绑定到 WebView 的 JS 全局作用域。JS 端通过绑定的名字(如 "Android")直接调 Kotlin 方法。方法必须标记
@JavascriptInterface(API 17+ 的安全要求)。- 混合内容 (Mixed Content)
一个 HTTPS 页面内部加载了 HTTP 协议的资源(图片、CSS、JS)。Android 5.0+ 默认阻止混合内容,因为 HTTP 资源可能被篡改,破坏 HTTPS 的安全性。
“网页浏览”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“网页浏览”必须回到可观察状态
“网页浏览”的学习结果不是记住类名,而是能预测“掌握 WebView 的加载、JavaScript 交互、安全配置与性能优化——让 Android 应用内嵌完整的 Web 浏览能力。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
29. Browsing the Web and WebView
在“网页浏览”中,“29. Browsing the Web and WebView”若只需打开公开网页,受维护的浏览器或Custom Tabs通常比自建高权限WebView边界更清晰。
One Last Bit of Flickr Data
在“网页浏览”中,“One Last Bit of Flickr Data”的API密钥与用户数据不得进入仓库或截图;测试使用受控fixture并记录服务合同版本。
The Easy Way: Implicit Intents
在“网页浏览”中,验证“The Easy Way: Implicit Intents”覆盖零个、一个和多个响应者,以及深链重复到达、目标被杀和无权限数据。
The Harder Way: WebView
在“网页浏览”中,“The Harder Way: WebView”若只需打开公开网页,受维护的浏览器或Custom Tabs通常比自建高权限WebView边界更清晰。
Proper Rotation with WebView
在“网页浏览”中,分析“Proper Rotation with WebView”区分WebView历史、Activity返回栈和配置重建,旋转保留页面不能靠吞掉所有configurationChanges。
WebView vs a Custom UI
在“网页浏览”中,“WebView vs a Custom UI”在应用内承载不可信网页内容;URL允许列表、导航委托、JavaScript桥和文件访问必须按最小能力配置。
For the More Curious: Injecting JavaScript Objects
在“网页浏览”中,“For the More Curious: Injecting JavaScript Objects”若只需打开公开网页,受维护的浏览器或Custom Tabs通常比自建高权限WebView边界更清晰。
For the More Curious: WebView Updates
在“网页浏览”中,“For the More Curious: WebView Updates”让资源匹配器根据locale与配置选择候选;字符串参数、复数、日期、RTL和回退链必须由目标locale实测,不能拼接英语语序。
For the More Curious: Chrome Custom Tabs (Another Easy Way)
在“网页浏览”中,“For the More Curious: Chrome Custom Tabs (Another Easy Way)”若只需打开公开网页,受维护的浏览器或Custom Tabs通常比自建高权限WebView边界更清晰。
Challenge: Using the Back Button for Browser History
在“网页浏览”中,验证“Challenge: Using the Back Button for Browser History”覆盖外部域、重定向、TLS错误、离线、后退与进程恢复,确认用户始终知道当前来源。
“网页浏览”验收回顾
“网页浏览”只有在导航来源、URL边界、历史栈、离线与TLS失败能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
← 上一页:broadcast intent · 下一页:定制视图与触摸事件 →
章专属可重放状态实验
先预测“打开页面、跳转、Back、旋转、离线与外部 scheme”发生后,WebView、Activity 返回栈与受信任 URL 策略应怎样改变当前 URL、历史、加载状态、Cookie、脚本与返回行为;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
网页浏览:状态合同
比较隐式 Intent、Custom Tabs 与 WebView 的导航、信任和生命周期边界
验证场景
第四版正式目录节点
webview · 正常任务
29. Browsing the Web and WebView:固定 SDK、设备配置和初始状态,触发“打开页面、跳转、Back、旋转、离线与外部 scheme”
冻结入口:29. Browsing the Web and WebView
记录WebView、Activity 返回栈与受信任 URL 策略的初始当前 URL、历史、加载状态、Cookie、脚本与返回行为
观察:URL 链、WebView 历史、SSL/加载错误、配置轨迹和安全断言中的“29. Browsing the Web and WebView”轨迹
预期:由WebView、Activity 返回栈与受信任 URL 策略提交当前 URL、历史、加载状态、Cookie、脚本与返回行为,并持续满足“只加载允许来源,Back 优先级和配置恢复不吞掉安全错误”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“打开页面、跳转、Back、旋转、离线与外部 scheme”。每一步只允许WebView、Activity 返回栈与受信任 URL 策略按职责提交状态,并持续核对“只加载允许来源,Back 优先级和配置恢复不吞掉安全错误”。
Deterministic event replay
网页浏览:事件轨迹
不变量:只加载允许来源,Back 优先级和配置恢复不吞掉安全错误
交付证据:URL 链、WebView 历史、SSL/加载错误、配置轨迹和安全断言
实验三:章专属反例与同输入恢复
注入“无白名单启用 JavaScript bridge,让不可信页面调用本地对象”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有URL 链、WebView 历史、SSL/加载错误、配置轨迹和安全断言一起恢复才算修复。
Fault · cancel · restore
网页浏览:反例与恢复
故障:无白名单启用 JavaScript bridge,让不可信页面调用本地对象
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“无白名单启用 JavaScript bridge,让不可信页面调用本地对象”
只加载允许来源,Back 优先级和配置恢复不吞掉安全错误
URL 链、WebView 历史、SSL/加载错误、配置轨迹和安全断言