HTTP与后台任务
HTTP与后台任务:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“HTTP与后台任务”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“学会在 Android 上发起 HTTP 请求,掌握后台线程的基本模式,理解为什么 NetworkOnMainThreadException 是 Android 安全模型的保护机制。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用请求或消息时间线、取消、乱序与失败恢复独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么 Android 对网络请求管这么严?
你的 App 想在主屏幕上显示天气——得先连网下载天气数据。如果你让主线程(负责渲染界面的那条流水线)去干这件事,会发生什么?网络可能很慢,甚至断掉——主线程卡在那里等数据,界面就会冻结。用户点什么都反应,几秒后系统弹出一个 "应用无响应(ANR)" 的对话框——你的 App 被用户卸载的日子就不远了。
用工厂流水线类比:主线程是那条不停运转的前台流水线,负责不停地刷新界面、响应用户点击。网络请求是一条慢悠悠的后台流水线——它把货物(数据)从仓库(服务器)拉回来需要时间。把"后台流水线"跟"前台流水线"绑在一起 = 整条线停摆等货到。正确的做法:前台继续转,后台拉货,货到了通知前台来取。
这章要解决的核心问题:怎么在 Android 上安全地发起网络请求,同时保持界面流畅?
Android 的网络请求基础设施
第一步:声明网络权限
在 Android 上连网,必须先在 AndroidManifest.xml 中声明权限:
<uses-permission android:name="android.permission.INTERNET" />从 Android 6.0(API 23)开始,INTERNET 属于普通权限——在 Manifest 里声明即可,不需要运行时弹窗请求用户授权。但如果你忘了声明,程序不会崩溃——所有网络请求会静默失败,IOException 里写着 "Permission denied"。
第二步:理解主线程红线
Android 从 3.0(Honeycomb)开始,强制规定:不允许在主线程(UI 线程)做任何网络操作。违反 = 当场抛出 NetworkOnMainThreadException。这不是刁难你——这是保护机制:
- 网络请求可能耗时 200ms,也可能耗时 20 秒(服务器挂了、用户在地铁里信号差)
- 主线程每 16ms 就要刷新一次界面(60fps)
- 主线程一旦被网络请求卡住超过 5 秒 → ANR(Application Not Responding)
是用户最讨厌的东西之一——比你 App 长得丑严重 100 倍。
Dispatchers.IO)。正确节奏是: 主线程发起 → 切后台拉数据 → 拿到结果 → runOnUiThread / Handler.post 回主线程刷新界面。直接在主线程做网络请求,等来的只会是那个 ANR 对话框。OkHttp:Android 世界的"HttpClient"
Android 生态里最主流的网络请求库是 ↡OkHttp 是 Square 公司开发的 HTTP 客户端库,提供连接池复用、Gzip 压缩、响应缓存、拦截器等能力。它是 Android 网络层的基石——Retrofit 的底层就是 OkHttp。。它虽然不是 Android SDK 自带的,但几乎每个 Android 项目都在用。
// build.gradle.kts(模块级)
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}最基本的 OkHttp 请求三步走:
// 第 1 步:创建客户端(全局共享一个实例)
val client = OkHttpClient()
// 第 2 步:构建请求
val request = Request.Builder()
.url("https://api.example.com/data")
.build()
// 第 3 步:发起请求(这是同步调用,会阻塞当前线程!)
val response = client.newCall(request).execute()
val body = response.body?.string()代码逐段拆解:完整的后台网络请求
方案 1:Thread + Handler(经典模式)
class MainActivity : AppCompatActivity() {
// 1. 创建与主线程 Looper 关联的 Handler
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 2. 启动后台线程
thread {
val data = fetchDataFromNetwork() // 在后台线程执行网络请求
val parsedResult = parseJson(data) // 在后台线程解析 JSON
// 3. 切回主线程更新 UI
handler.post {
textView.text = parsedResult
}
}
}
private fun fetchDataFromNetwork(): String {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.github.com/users/octocat")
.build()
val response = client.newCall(request).execute()
return response.body?.string() ?: ""
}
private fun parseJson(json: String): String {
val jsonObject = JSONObject(json)
return jsonObject.getString("login") // 从 GitHub API 取用户名
}
}关键点分析:
thread { }是 Kotlin 的扩展函数,等价于Thread { ... }.start()- 网络请求和 JSON 解析都在后台线程——主线程完全不受影响
handler.post { }把 UI 更新投递到主线程的消息队列——这是 Android 唯一的合法更新 UI 方式
方案 2:用 Gson 替代 org.json 手动解析
// build.gradle.kts
implementation("com.google.code.gson:gson:2.10.1")
// 定义数据类
data class GitHubUser(
val login: String,
val id: Long,
@SerializedName("avatar_url") val avatarUrl: String
)
// 一行解析
val user = Gson().fromJson(json, GitHubUser::class.java)
// 更新 UI
handler.post {
textView.text = "用户名: ${user.login}"
// 加载头像...
}@SerializedName 注解解决了 JSON 键名 avatar_url(下划线)到 Kotlin 属性名 avatarUrl(驼峰)的映射问题——这是 Java/Android 世界里 JSON 解析最常见的需求之一。
① 检查网络连接
调用 ConnectivityManager.getActiveNetworkInfo()
检查当前网络是否可用。如果返回
null,直接提示用户"无网络连接"而不是让后续请求抛
UnknownHostException。建议封装为 isNetworkAvailable(): Boolean
工具方法。
容易踩的坑
小结
- Android 禁止主线程网络请求——违反抛出
NetworkOnMainThreadException,主线程阻塞超 5 秒触发 ANR - 网络请求必须在 Manifest 声明
INTERNET权限;Android 9+ 默认禁止明文 HTTP,只允许 HTTPS - OkHttp 是 Android 上最主流的 HTTP 客户端库:创建 Client → 构建 Request → 执行 Call
- 经典后台模式:
Thread执行网络/解析 →Handler.post切回主线程更新 UI - AsyncTask 已被废弃(Android 11),不要在新代码中使用——用 Coroutines 或 WorkManager 替代
练习
问题 1:“第24章 HTTP and Background Tasks”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- OkHttp
Square 公司开发的 HTTP 客户端库。它给 Android 提供连接池复用(不用每次请求重新建连)、Gzip 压缩(省流量)、拦截器(统一加认证头或日志)等功能。Retrofit 的底层就是 OkHttp。详见本章"OkHttp"一节。
- ANR(Application Not Responding)
当主线程被长时间阻塞(前台 Activity 5 秒、广播 10 秒、后台 Service 更长),系统弹出"应用无响应"对话框。这是用户最讨厌的东西——用 ANR 作为警钟,把耗时操作扔到后台线程。详见本章"主线程红线"一节。
- NetworkOnMainThreadException
Android 3.0 起强制抛出的异常。如果你在主线程发网络请求,系统直接 crash 你的 App——这是保护你远离 ANR 的"强制止损"。详见本章"主线程红线"一节。
- Gson
Google 开发的 JSON 解析库。一行代码就能把 JSON 字符串变成 Kotlin/Java 对象。
@SerializedName注解处理字段名不一致的问题(如下划线 vs 驼峰)。详见本章"用 Gson 替代 org.json"一节。
“HTTP与后台任务”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“HTTP与后台任务”必须回到可观察状态
“HTTP与后台任务”的学习结果不是记住类名,而是能预测“学会在 Android 上发起 HTTP 请求,掌握后台线程的基本模式,理解为什么 NetworkOnMainThreadException 是 Android 安全模型的保护机制。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
24. HTTP and Background Tasks
在“HTTP与后台任务”中,分析“24. HTTP and Background Tasks”要区分Activity返回栈、系统task与进程,flags改变栈行为但不会自动提供业务幂等。
Creating PhotoGallery
在“HTTP与后台任务”中,“Creating PhotoGallery”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。
Networking Basics with Retrofit
在“HTTP与后台任务”中,“Networking Basics with Retrofit”把HTTP状态、响应体和解析错误转换为明确UI状态;成功码、空结果、超时、取消和畸形JSON必须走不同分支。
Fetching JSON from Flickr
在“HTTP与后台任务”中,分析“Fetching JSON from Flickr”沿请求参数、线程、仓库缓存、ViewModel与列表绑定追踪同一查询,避免旧响应覆盖新输入。
Networking Across Configuration Changes
在“HTTP与后台任务”中,“Networking Across Configuration Changes”把HTTP状态、响应体和解析错误转换为明确UI状态;成功码、空结果、超时、取消和畸形JSON必须走不同分支。
Displaying Results in RecyclerView
在“HTTP与后台任务”中,“Displaying Results in RecyclerView”通过显式Intent启动目标并以extras/result交换最小数据;发送方、接收方和进程重建都必须验证缺失、类型错误和重复返回。
For the More Curious: Alternate Parsers and Data Formats
在“HTTP与后台任务”中,“For the More Curious: Alternate Parsers and Data Formats”把HTTP状态、响应体和解析错误转换为明确UI状态;成功码、空结果、超时、取消和畸形JSON必须走不同分支。
For the More Curious: Canceling Requests
在“HTTP与后台任务”中,“For the More Curious: Canceling Requests”用于推翻“学会在 Android 上发起 HTTP 请求,掌握后台线程的基本模式,理解为什么 NetworkOnMainThreadException 是 Android 安全模型的保护机制。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
For the More Curious: Managing Dependencies
在“HTTP与后台任务”中,“For the More Curious: Managing Dependencies”用于推翻“学会在 Android 上发起 HTTP 请求,掌握后台线程的基本模式,理解为什么 NetworkOnMainThreadException 是 Android 安全模型的保护机制。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
Challenge: Adding a Custom Gson Deserializer
在“HTTP与后台任务”中,验证“Challenge: Adding a Custom Gson Deserializer”固定查询,只改变网络延迟、分页边界或响应字段,核对请求次数、取消、去重和可恢复错误。
Challenge: Paging
在“HTTP与后台任务”中,“Challenge: Paging”的API密钥与用户数据不得进入仓库或截图;测试使用受控fixture并记录服务合同版本。
Challenge: Dynamically Adjusting the Number of Columns
在“HTTP与后台任务”中,“Challenge: Dynamically Adjusting the Number of Columns”用于推翻“学会在 Android 上发起 HTTP 请求,掌握后台线程的基本模式,理解为什么 NetworkOnMainThreadException 是 Android 安全模型的保护机制。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
“HTTP与后台任务”验收回顾
“HTTP与后台任务”只有在请求或消息时间线、取消、乱序与失败恢复能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
← 上一页:深入学习intent和任务 · 下一页:Looper、Handler和HandlerThread →
章专属可重放状态实验
先预测“发起请求、旋转、断网、重试、分页与快速切换查询”发生后,PhotoGallery Repository、网络 call 与界面状态所有者应怎样改变查询、请求 ID、加载状态、结果页、错误与取消;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
HTTP与后台任务:状态合同
用 Retrofit 获取 Flickr JSON,在配置重建与取消中只提交当前响应
验证场景
第四版正式目录节点
http-background · 正常任务
24. HTTP and Background Tasks:固定 SDK、设备配置和初始状态,触发“发起请求、旋转、断网、重试、分页与快速切换查询”
冻结入口:24. HTTP and Background Tasks
记录PhotoGallery Repository、网络 call 与界面状态所有者的初始查询、请求 ID、加载状态、结果页、错误与取消
观察:请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言中的“24. HTTP and Background Tasks”轨迹
预期:由PhotoGallery Repository、网络 call 与界面状态所有者提交查询、请求 ID、加载状态、结果页、错误与取消,并持续满足“旧请求不能覆盖新查询,离线和解析错误都有可恢复状态”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“发起请求、旋转、断网、重试、分页与快速切换查询”。每一步只允许PhotoGallery Repository、网络 call 与界面状态所有者按职责提交状态,并持续核对“旧请求不能覆盖新查询,离线和解析错误都有可恢复状态”。
Deterministic event replay
HTTP与后台任务:事件轨迹
不变量:旧请求不能覆盖新查询,离线和解析错误都有可恢复状态
交付证据:请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言
实验三:章专属反例与同输入恢复
注入“查询 B 已显示后,较慢的查询 A 回调覆盖当前列表”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言一起恢复才算修复。
Fault · cancel · restore
HTTP与后台任务:反例与恢复
故障:查询 B 已显示后,较慢的查询 A 回调覆盖当前列表
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“查询 B 已显示后,较慢的查询 A 回调覆盖当前列表”
旧请求不能覆盖新查询,离线和解析错误都有可恢复状态
请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言