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 倍。

主线程 vs 工作线程:耗时活儿丢后台,更新界面回主线程主线程(UI 线程)唯一能碰 View16ms / 帧要流畅阻塞 > 5s → ANR工作线程(后台)跑网络请求 / IOOkHttpDispatchers.IOUI 线程发起请求切到工作线程做网络请求拿到结果切回主线程更新 UI切后台runOnUiThread / post 回主线程✗ 错误做法:主线程直接做网络请求主线程 .execute()卡住 > 5s应用无响应(ANR)「App 没有响应」等待 / 关闭
主线程是唯一能碰界面的那条线,每 16ms 要出一帧,被卡超过 5 秒就触发 ANR;网络和 IO 这类耗时活儿必须丢到工作线程(OkHttp / 协程 Dispatchers.IO)。正确节奏是: 主线程发起 → 切后台拉数据 → 拿到结果 → runOnUiThread / Handler.post 回主线程刷新界面。直接在主线程做网络请求,等来的只会是那个 ANR 对话框。

OkHttp:Android 世界的"HttpClient"

Android 生态里最主流的网络请求库是 。它虽然不是 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 解析最常见的需求之一。

分步1 / 4

① 检查网络连接

调用 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、设备配置和初始状态,触发“发起请求、旋转、断网、重试、分页与快速切换查询”

状态所有者PhotoGallery Repository、网络 call 与界面状态所有者
受控状态查询、请求 ID、加载状态、结果页、错误与取消
触发事件发起请求、旋转、断网、重试、分页与快速切换查询

冻结入口: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与后台任务:事件轨迹

选择一次状态转换1 / 5

不变量:旧请求不能覆盖新查询,离线和解析错误都有可恢复状态

交付证据:请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言

实验三:章专属反例与同输入恢复

注入“查询 B 已显示后,较慢的查询 A 回调覆盖当前列表”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言一起恢复才算修复。

Fault · cancel · restore

HTTP与后台任务:反例与恢复

故障:查询 B 已显示后,较慢的查询 A 回调覆盖当前列表

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“查询 B 已显示后,较慢的查询 A 回调覆盖当前列表”

3. 检查所有者一致

旧请求不能覆盖新查询,离线和解析错误都有可恢复状态

4. 核对结果一致

请求 ID、URL、HTTP 状态、解析错误、取消日志和列表断言

讨论

评论区加载中…