12、网络编程(下):RxJava与协程在网络请求中的应用、图片加载框架(Glide、Coil)、WebSocket与实时通信
好,咱们接着聊网络编程的下半场。上一章我们把 OkHttp 和 Retrofit 的底子打牢了,这一章要解决两个实际问题:异步线程切换 和 图片加载,再聊聊实时通信的 WebSocket。
说实话,刚入行那会儿,我写网络请求都是直接在 Activity 里 new Thread,然后 handler.sendMessage。代码又臭又长,还容易内存泄漏。后来用了 RxJava,感觉打开了新世界的大门。再后来 Kotlin 协程普及了,我又慢慢转向了协程。嗯,技术总是在迭代,但核心思想没变——把异步逻辑写得像同步一样优雅。
12.1 RxJava 在 Retrofit 中的实战
RxJava 的核心就三个词:Observable(被观察者)、Observer(观察者)、操作符。配合 Retrofit 使用,能让网络请求的线程切换和结果处理变得非常简洁。
先看一个典型的例子。假设我们要请求用户列表,然后更新 UI:
// 定义 Retrofit 接口
interface ApiService {
@GET("users")
fun getUsers(): Observable<List<User>>
}
// 在 ViewModel 或 Activity 中调用
apiService.getUsers()
.subscribeOn(Schedulers.io()) // 网络请求在 IO 线程
.observeOn(AndroidSchedulers.mainThread()) // 结果回调在主线程
.subscribe({ users ->
// 更新 UI
adapter.submitList(users)
}, { error ->
// 处理错误
Log.e("TAG", "请求失败", error)
})
这里有个细节我特别想强调:subscribeOn 和 observeOn 的位置。subscribeOn 只需要写一次,它决定了整个链路上游(数据产生方)的线程。而 observeOn 可以写多次,每次切换下游的线程。
核心原则:subscribeOn 只管上游,observeOn 只管下游。如果你在 subscribeOn 之后又调了一次 subscribeOn,只有第一次生效。
我在项目中遇到过一个问题:多个接口需要并行请求,等所有结果都返回后再合并展示。用 RxJava 的 zip 操作符非常方便:
val observable1 = apiService.getUserInfo()
val observable2 = apiService.getUserPosts()
Observable.zip(observable1, observable2,
BiFunction<UserInfo, List<Post>, CombinedData> { userInfo, posts ->
CombinedData(userInfo, posts)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ combined ->
// 两个请求都完成了
showData(combined)
}, { error ->
// 任何一个请求失败都会走到这里
handleError(error)
})
你想想看,如果用传统的回调方式,得写两个回调,还得自己维护一个计数器。RxJava 一行 zip 就搞定了。
个人习惯:我一般会在 BaseViewModel 里封装一个通用的 dispose 方法,在 onCleared() 里调用 compositeDisposable.clear(),防止内存泄漏。RxJava 的订阅如果不及时取消,Activity 销毁了回调还在执行,很容易崩。
12.2 Kotlin 协程:更现代的异步方案
RxJava 虽然强大,但学习曲线有点陡。Kotlin 协程出来后,我慢慢把新项目都迁到了协程。说白了,协程就是轻量级的线程,但它不需要回调,代码写起来像同步一样。
Retrofit 从 2.6.0 开始原生支持协程的 suspend 函数:
// 接口定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
// 在 ViewModel 中调用
viewModelScope.launch {
try {
val users = apiService.getUsers() // 直接调用,不需要回调
// 自动在主线程更新 UI
_users.value = users
} catch (e: Exception) {
// 统一错误处理
_error.value = e.message
}
}
这里有个关键点:viewModelScope.launch 默认在 主线程 启动协程。但网络请求是耗时操作,不能卡主线程。怎么办?Retrofit 内部已经帮我们处理了——suspend 函数默认在 IO 线程执行网络请求,返回结果时自动切回主线程。
如果你需要手动切换线程,可以用 withContext:
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
// 耗时操作,比如读写数据库
database.queryUsers()
}
// 回到主线程更新 UI
textView.text = result.toString()
}
我曾经踩过的坑:在 Fragment 或 Activity 里直接用 GlobalScope.launch 启动协程。这会导致协程的生命周期和组件不一致,组件销毁了协程还在跑,轻则空指针,重则内存泄漏。一定要用 lifecycleScope 或 viewModelScope。
协程的另一个杀手锏是 Flow,它类似于 RxJava 的 Observable,但更轻量。比如监听网络状态变化:
fun observeNetworkState(): Flow<Boolean> = callbackFlow {
val receiver = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(true)
}
override fun onLost(network: Network) {
trySend(false)
}
}
connectivityManager.registerNetworkCallback(builder.build(), receiver)
awaitClose {
connectivityManager.unregisterNetworkCallback(receiver)
}
}
// 在 ViewModel 中收集
viewModelScope.launch {
networkRepository.observeNetworkState().collect { isConnected ->
_networkState.value = isConnected
}
}
Flow 的好处是支持 背压 和 冷流,而且和协程的取消机制天然集成。我个人觉得,对于新项目,优先用协程 + Flow,除非你有很多操作符的需求才考虑 RxJava。
12.3 图片加载框架:Glide vs Coil
图片加载是每个 App 都绕不开的坎。加载网络图片、缓存、缩放、圆角、GIF 支持……自己写一套太累了。业界主流就两个:Glide 和 Coil。
先看 Glide,老牌框架,Google 官方推荐,功能非常全面:
// 基本用法
Glide.with(context)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.placeholder) // 占位图
.error(R.drawable.error) // 错误图
.circleCrop() // 圆形裁剪
.into(imageView)
// 自定义配置
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存所有版本
.override(300, 300) // 指定宽高
.centerCrop()
.into(imageView)
Glide 的缓存策略很智能。默认情况下,它会缓存 原始图 和 转换后的图。如果你设置了 override(300, 300),它会生成一张 300x300 的缩略图缓存起来,下次直接加载缓存,省带宽也省内存。
再来看 Coil,它是 Kotlin 协程原生的图片加载库,代码更简洁:
// Coil 基本用法
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
crossfade(true) // 淡入效果
size(300, 300)
transformations(CircleCropTransformation())
}
// 或者用 ImageLoader
val imageLoader = ImageLoader(context)
imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.target(imageView)
.build()
)
Coil 最大的优势是 轻量,包体积只有 Glide 的 1/3 左右。而且它完全基于 Kotlin 协程,没有回调嵌套,和现代 Android 开发风格很搭。
| 对比维度 | Glide | Coil |
|---|---|---|
| 包体积 | 约 500KB | 约 150KB |
| 底层实现 | 基于 OkHttp + 自定义缓存 | 基于 OkHttp + 协程 |
| GIF 支持 | 原生支持 | 需要额外库 |
| API 风格 | 链式调用 | Kotlin DSL |
| 学习成本 | 中等 | 低 |
我的选择建议:如果项目已经用了 Glide,没必要强行迁移。如果是新项目,而且团队 Kotlin 熟练度较高,我推荐 Coil。它和协程的配合太丝滑了,代码量能减少 30% 以上。
12.4 WebSocket 与实时通信
HTTP 是请求-响应模式,客户端不请求,服务器就不能主动发消息。但像聊天、股票行情、游戏对战这些场景,需要服务器主动推送。这时候就要用 WebSocket。
WebSocket 的原理很简单:建立一条 TCP 长连接,客户端和服务器都可以随时发消息。OkHttp 原生支持 WebSocket,不需要额外引入库。
// 创建 WebSocket 客户端
val client = OkHttpClient()
val request = Request.Builder()
.url("wss://example.com/chat")
.build()
val ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// 连接建立成功
webSocket.send("Hello Server!")
}
override fun onMessage(webSocket: WebSocket, text: String) {
// 收到文本消息
runOnUiThread {
// 更新 UI
chatAdapter.addMessage(text)
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// 连接失败或断开
Log.e("WS", "连接失败", t)
// 考虑重连
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
// 连接正常关闭
}
})
// 记得在不需要时关闭
ws.close(1000, "Goodbye")
这里有个很实际的问题:网络不稳定导致断开怎么办? 我做过一个即时通讯项目,最开始没做重连机制,用户反馈经常收不到消息。后来加了心跳和自动重连:
class WebSocketManager(private val url: String) {
private var webSocket: WebSocket? = null
private var reconnectCount = 0
private val maxReconnect = 10
fun connect() {
val client = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS) // 30秒心跳
.build()
val request = Request.Builder().url(url).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
if (reconnectCount < maxReconnect) {
reconnectCount++
// 指数退避重连
val delay = (Math.pow(2.0, reconnectCount.toDouble()) * 1000).toLong()
Handler(Looper.getMainLooper()).postDelayed({
connect()
}, delay)
}
}
override fun onOpen(ws: WebSocket, response: Response) {
reconnectCount = 0 // 重置重连计数
}
})
}
fun send(message: String) {
webSocket?.send(message)
}
fun disconnect() {
webSocket?.close(1000, "Client closed")
webSocket = null
}
}
为什么要用指数退避?因为如果服务器宕机了,你每秒重连一次,会把服务器打崩。先等 2 秒,再等 4 秒,8 秒……给服务器恢复的时间,也节省客户端电量。
我曾经犯过的错:在 Activity 的 onDestroy 里没有关闭 WebSocket,导致 Activity 泄漏。WebSocket 持有 Activity 的引用,Activity 无法被 GC 回收。一定要在 onDestroy 或 onStop 里调用 disconnect()。
最后,用一张图总结本章的知识体系:
好了,这一章的内容就到这里。RxJava 和协程是异步编程的两把利器,Glide 和 Coil 是图片加载的左膀右臂,WebSocket 则是实时通信的基石。把这些工具用好,你的 App 在网络层面基本就稳了。
公众号:蓝海资料掘金营,微信 deep3321