本章只围绕 Markwon + commonmark-java
(不再引用 pulldown_cmark 或任何 Native 解析器)
5.1 性能瓶颈地图
| 环节 | 典型耗时点 | 运行时症状 |
|---|---|---|
| 解析 | Parser.parse(markdown) 字符串遍历 | 打开文章首帧慢 |
| 渲染 | MarkwonVisitor 递归生成 span / View | 首屏卡顿 |
| 布局 | TextView.onMeasure() 行高计算 | 滚动掉帧 |
| 图片 | Glide / Coil 解码、重采样 | 首屏空白 |
| GC | 短生命周期 StringBuilder / span | 抖动、短帧 |
5.2 解析层优化(Markwon 纯 Java)
5.2.1 后台线程解析
suspend fun parseAsync(md: String) = withContext(Dispatchers.Default) {
org.commonmark.parser.Parser.builder().build().parse(md)
}
- 规矩:主线程永不直接
Parser.parse()。 - 解析结果
Node体积小,可直接放入内存或磁盘LruCache。
5.2.2 结果缓存
object AstCache : LruCache<String, Node>(30) { // key = md SHA-256
fun getOrParse(md: String): Node =
get(md.sha256()) ?: parseAsync(md).also { put(md.sha256(), it) }
}
- 30 条缓存 ≈ 数百 KB,足够大部分场景。
- 网络接口若返回 ETag,可用 ETag 做缓存键。
5.2.3 增量解析(流式)
简化策略:每次来新文本,仅解析新增部分,旧 Node 保持不动。
class IncrementalParser {
private val parser = org.commonmark.parser.Parser.builder().build()
private val pieces = mutableListOf<Node>()
/** 输入新增 Markdown 段落 */
fun feed(chunk: String) {
pieces += parser.parse(chunk)
}
/** 合并所有 Node,交给 MarkwonVisitor 渲染 */
fun combined(): Node {
val doc = org.commonmark.node.Document()
pieces.forEach { doc.appendChild(it.firstChild) }
return doc
}
}
5.3 渲染层优化
5.3.1 View 复用(块级)
fun <D, V : View> LinearLayout.renderBlock(
index: Int,
data: D,
create: () -> V,
bind: V.(D) -> Unit
) {
val child = (getChildAt(index) as? V) ?: create().also { addView(it, index) }
if (child.getTag(R.id.info) != data) { // 脏检查
child.bind(data)
child.setTag(R.id.info, data)
}
}
- 代码块 / 表格 View 复用 → 帧耗时 ~50%↓。
5.3.2 Spannable 合并
- 不要在循环里
builder.append(char)。 - 收集连续普通文本 → 一次 append。
- 分隔点只有样式切换才插入 span。
5.3.3 图片占位 + 异步刷新
class GlideImagePlugin(private val glide: RequestManager) : AbstractMarkwonPlugin() {
override fun configureImages(b: ImagesPlugin.Builder) {
b.imageLoader { dest, iv ->
glide.load(dest)
.placeholder(R.drawable.placeholder)
.into(iv)
}
}
}
5.4 流式渲染:自适应匀速 + 渐显
场景:LLM / SSE / WebSocket 连续产出 Markdown。
5.4.1 BufferManager —— 动态速率算法
class BufferManager(private val tickMs: Long = 40) {
private val buf = StringBuilder()
private val _flow = MutableSharedFlow<String>(extraBufferCapacity = 128)
val flow = _flow.asSharedFlow()
suspend fun feed(text: String) = synchronized(buf) { buf.append(text) }
fun start(scope: CoroutineScope) = scope.launch {
while (isActive) {
val slice = synchronized(buf) { nextSlice() }
if (slice.isNotEmpty()) _flow.emit(slice)
delay(tickMs)
}
}
private fun nextSlice(): String {
val size = buf.length
if (size == 0) return ""
val targetMs = when {
size <= 50 -> 3_000
size <= 150 -> 8_000
else -> size * 60
}
val chars = max(1, (size * tickMs / targetMs).toInt())
return buf.substring(0, chars).also { buf.delete(0, it.length) }
}
}
- 多 → 慢 / 少 → 快,始终保持阅读节奏可控。
- 40 ms tick ≈ 25 fps,动画流畅。
5.4.2 FadeTextWriter —— 渐显
class FadeTextWriter(private val tv: TextView, private val markwon: Markwon) {
private val sb = SpannableStringBuilder()
fun append(md: String) {
val span = AlphaForegroundColorSpan(tv.currentTextColor, 0f)
val start = sb.length
sb.append(markwon.toMarkdown(md))
val end = sb.length
sb.setSpan(span, start, end, 0)
tv.text = sb
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 300
addUpdateListener { span.alpha = it.animatedValue as Float; tv.text = sb }
}.start()
}
}
5.4.3 整合协程
val buffer = BufferManager().apply { start(lifecycleScope) }
val writer = FadeTextWriter(textView, markwon)
lifecycleScope.launchWhenStarted {
buffer.flow.collect { writer.append(it) }
}
fun onSseChunk(chunk: String) = lifecycleScope.launch { buffer.feed(chunk) }
5.5 大文档分页 / RecyclerView
5.5.1 切片
val pages: List<List<Node>> = ast.children.chunked(200)
- 每页 ≤ 200 个节点,保证单页渲染 < 16 ms。
5.5.2 Paging3 Adapter
class MdAdapter(private val markwon: Markwon) :
PagingDataAdapter<List<Node>, MdAdapter.VH>(DIFF) {
override fun onBindViewHolder(holder: VH, pos: Int) {
val page = getItem(pos) ?: return
val sb = SpannableStringBuilder()
page.forEach { markwon.render(it, sb) }
holder.tv.text = sb
}
…
}
5.6 监控与调优
| 步骤 | 工具 | 目标 |
|---|---|---|
| 帧率监控 | Choreographer 帧日志 | UI ≤ 16 ms |
| Hot Spot | Trace.beginSection + Perfetto | 找最长段 |
| 内存泄漏 | LeakCanary | Span 不泄漏 Activity |
| I/O | OKHttp EventListener | 图片首包 / 平均耗时 |
5.7 小结
- 解析在线程池 + LruCache,首帧即秒开。
- View 复用 / span 合并,首屏与滚动帧率大幅提升。
- BufferManager + 渐显,让流式 Markdown 打字机般平滑输出。
- 分页 + Paging3,超长文档滑动不卡。
- 工具链 → 帧率 → Trace → 内存,一步步定位并修复问题。
按照本章示例复制到项目,即可在纯 Markwon 环境下实现高性能渲染与流式展示。

1712

被折叠的 条评论
为什么被折叠?



