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 支持……自己写一套太累了。业界主流就两个:GlideCoil

先看 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 Observable/Observer subscribeOn/observeOn zip/merge 操作符 CompositeDisposable Kotlin 协程 suspend 函数 viewModelScope withContext Flow 冷流 图片加载 Glide vs Coil 缓存策略 圆角/裁剪 GIF 支持 WebSocket 长连接 心跳机制 自动重连 指数退避 核心思想 异步线程切换 → 图片高效加载 → 实时双向通信 实践建议:新项目优先协程 + Coil + OkHttp WebSocket 老项目保持 Glide,逐步迁移协程

好了,这一章的内容就到这里。RxJava 和协程是异步编程的两把利器,Glide 和 Coil 是图片加载的左膀右臂,WebSocket 则是实时通信的基石。把这些工具用好,你的 App 在网络层面基本就稳了。


公众号:蓝海资料掘金营,微信 deep3321