Android 流式 AI 响应实战:OkHttp 与 Markdown 解析 TextView 深度整合
在移动应用中实现 AI 对话的流式响应效果,已经成为提升用户体验的重要技术手段。不同于传统的一次性数据返回,流式响应能够模拟人类思考过程,逐字显示内容,同时支持富文本格式。本文将深入探讨如何构建一个完整的 Android 解决方案,从网络层到 UI 层的全链路实现。
1. Server-Sent Events (SSE) 技术解析与 OkHttp 实现
SSE 是一种基于 HTTP 的服务器推送技术,特别适合 AI 对话这类需要实时更新但不需要双向通信的场景。与 WebSocket 相比,SSE 具有实现简单、自动重连等优势。
1.1 OkHttp 的 SSE 客户端封装
class SSEClient(private val okHttpClient: OkHttpClient) { private var eventListener: SSEEventListener? = null private var call: Call? = null fun connect(url: String, listener: SSEEventListener) { this.eventListener = listener val request = Request.Builder() .url(url) .build() call = okHttpClient.newCall(request) call?.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { listener.onFailure(e) } override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { listener.onFailure(IOException("Unexpected code $response")) return } val source = response.body?.source() ?: return listener.onFailure(IOException("Empty response body")) while (true) { val line = source.readUtf8Line() ?: break when { line.startsWith("data:") -> { val data = line.substring(5).trim() listener.onEvent(data) } line.startsWith("event:") -> { val eventType = line.substring(6).trim() listener.onEventType(eventType) } } } } }) } fun disconnect() { call?.cancel() eventListener = null } interface SSEEventListener { fun onEvent(data: String) fun onEventType(eventType: String) fun onFailure(e: Exception) } }关键实现要点:
- 使用 OkHttp 的
Source逐行读取事件流 - 区分不同事件类型(thinkingProcess/finalResult/done)
- 处理连接中断和异常情况
1.2 性能优化与错误处理
常见问题处理方案:
| 问题类型 | 解决方案 | 实现建议 |
|---|---|---|
| 连接中断 | 指数退避重连 | 初始延迟 1s,最大延迟 30s |
| 数据解析错误 | JSON 校验与容错 | 使用try/catch包裹解析逻辑 |
| 内存泄漏 | 生命周期管理 | 在onDestroy中释放资源 |
| 线程安全 | 主线程更新 | 通过Handler或LiveData转发 |
提示:在实际项目中,建议为 SSE 连接添加心跳检测机制,定时发送 ping 事件保持连接活跃。
2. 支持 Markdown 的自定义 TextView 实现
2.1 核心功能设计
class AITextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr) { private val contentBuilder = SpannableStringBuilder() private val handler = Handler(Looper.getMainLooper()) private var typingSpeed = 80L // 默认打字速度 init { movementMethod = ScrollingMovementMethod() setTextIsSelectable(true) } fun appendStreamContent(content: String) { handler.post { // 处理 Markdown 格式 processMarkdown(content) // 自动滚动到底部 scrollToBottom() } } private fun processMarkdown(text: String) { // 实现加粗、链接等 Markdown 解析 val processedText = parseMarkdown(text) contentBuilder.append(processedText) setText(contentBuilder) } private fun scrollToBottom() { (parent as? ScrollView)?.post { fullScroll(ScrollView.FOCUS_DOWN) } } fun clearContent() { contentBuilder.clear() text = "" } fun setTypingSpeed(speed: Long) { typingSpeed = speed } }2.2 Markdown 解析实现细节
支持的 Markdown 元素:
- 加粗文本:
**加粗内容** - 超链接:
[显示文本](http://example.com) - 内联代码:
`代码` - 列表项:
- 列表内容
private fun parseMarkdown(text: String): SpannableString { val spannable = SpannableString(text) // 处理加粗 val boldPattern = "\\*\\*(.*?)\\*\\*".toRegex() boldPattern.findAll(text).forEach { match -> val start = match.range.first val end = match.range.last + 1 spannable.setSpan( StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } // 处理链接 val linkPattern = "\\[(.*?)\\]\\((.*?)\\)".toRegex() linkPattern.findAll(text).forEach { match -> val linkText = match.groupValues[1] val linkUrl = match.groupValues[2] val start = match.range.first val end = match.range.last + 1 spannable.removeSpan(match.range) spannable.replace(start, end, linkText) spannable.setSpan( object : ClickableSpan() { override fun onClick(widget: View) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl)) context.startActivity(intent) } override fun updateDrawState(ds: TextPaint) { ds.color = ContextCompat.getColor(context, R.color.link_color) ds.isUnderlineText = true } }, start, start + linkText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } return spannable }3. 网络层与 UI 层的深度整合
3.1 数据流架构设计
[SSE 服务器] → [OkHttp SSE 客户端] → [ViewModel 数据处理层] → [LiveData 观察者] → [自定义 TextView 渲染]关键整合代码:
class AIChatViewModel(application: Application) : AndroidViewModel(application) { private val _chatContent = MutableLiveData<ChatEvent>() val chatContent: LiveData<ChatEvent> = _chatContent private val sseClient = SSEClient(OkHttpClient.Builder().build()) fun startStreaming(query: String) { val url = "https://api.example.com/chat?sse=true&q=${URLEncoder.encode(query, "UTF-8")}" sseClient.connect(url, object : SSEClient.SSEEventListener { override fun onEvent(data: String) { val json = JSONObject(data) val event = ChatEvent( type = json.optString("event", "message"), content = json.getString("content"), isFinal = json.optBoolean("is_final", false) ) _chatContent.postValue(event) } override fun onEventType(eventType: String) { // 处理不同事件类型 } override fun onFailure(e: Exception) { _chatContent.postValue(ChatEvent.error(e.message ?: "Unknown error")) } }) } override fun onCleared() { sseClient.disconnect() } } data class ChatEvent( val type: String, val content: String, val isFinal: Boolean, val isError: Boolean = false ) { companion object { fun error(message: String): ChatEvent { return ChatEvent("error", message, true, true) } } }3.2 动画与交互优化
逐字显示效果实现:
private fun displayTextWithAnimation(fullText: String) { val handler = Handler(Looper.getMainLooper()) var currentLength = 0 val runnable = object : Runnable { override fun run() { if (currentLength <= fullText.length) { val partialText = fullText.substring(0, currentLength) textView.appendStreamContent(partialText) currentLength++ handler.postDelayed(this, typingSpeed) } } } handler.post(runnable) }性能优化技巧:
- 使用
RecyclerView替代简单ScrollView处理长对话 - 实现视图回收机制避免内存溢出
- 对 Markdown 解析结果进行缓存
4. 生产环境进阶实践
4.1 安全加固措施
关键安全考虑:
- HTTPS 通信:强制使用 TLS 1.2+ 加密
- 数据校验:验证服务器证书链
- 输入过滤:防止 XSS 攻击
- 速率限制:控制消息更新频率
// 安全配置示例 val okHttpClient = OkHttpClient.Builder() .sslSocketFactory(createSSLSocketFactory(), createTrustManager()) .hostnameVerifier { hostname, session -> hostname == "api.example.com" } .addInterceptor(RateLimitInterceptor(5, 1)) // 5次/秒 .build()4.2 监控与调试方案
调试工具配置:
# 使用 Charles 抓包调试 adb reverse tcp:8888 tcp:8888监控指标:
- 连接稳定性(重连次数)
- 消息延迟(从发送到显示)
- 内存占用(防止泄漏)
- 渲染性能(FPS 监测)
4.3 跨平台兼容策略
不同 Android 版本适配方案:
| API 级别 | 适配要点 | 解决方案 |
|---|---|---|
| API 21+ | TLS 配置 | 自定义 SSLSocketFactory |
| API 24+ | 文本渲染 | 使用 PrecomputedText |
| API 26+ | 自动填充 | 禁用密码字段自动填充 |
| 所有版本 | 黑暗模式 | 动态颜色适配 |