一、背景
在生成式 AI 技术快速发展的当下,大模型驱动的智能应用正重塑人机交互方式。智能客服、知识问答系统、AI 助手等场景中,Markdown 凭借简洁高效的结构化表达能力,成为 AI 内容呈现的核心载体。 但在 Android 原生开发中,实现高性能的 Markdown 实时渲染仍面临挑战:既要保证流式增量输出的流畅性,又要支持灵活的自定义样式适配,这对渲染引擎的性能和可扩展性提出了更高要求。 Android 端成熟的 Markdown 渲染框架 Markwon(Github:github.com/noties/Mark... CommonMark-Java 实现核心解析,其插件化架构具备天然的扩展能力。出于避免重复造轮子的考量,本文先剖析 CommonMark-Java 与 Markwon 的核心原理,再阐述如何基于 Markwon 扩展实现自定义渲染需求。
二、CommonMark-Java 框架
CommonMark-Java 是符合 CommonMark 标准的轻量级 Markdown 解析库,核心能力是将 Markdown 文本转换为结构化 AST(抽象语法树),支持扩展协议但不提供渲染能力,适用于 Java/Kotlin 项目的基础解析需求。
1、Node节点
Node 是 CommonMark-Java 的核心抽象类,代表 Markdown 解析后生成的 AST(抽象语法树,可理解为 Markdown 文档的结构化树形表示)节点,所有 Markdown 元素(如段落、标题、代码块等)均继承自该类。
代码:
java
public abstract class Node {
private Node parent = null;
private Node firstChild = null;
private Node lastChild = null;
private Node prev = null;
private Node next = null;
public abstract void accept(Visitor visitor);
// 省略get/set方法...
}
Node 类的核心特性:
- 表示 Markdown 文档结构中的一个元素;
- 采用双向链表结构(通过 prev、next 维护同级节点关系);
- 同时维护父子关系(通过 parent、firstChild、lastChild);
- 支持访问者模式遍历 AST;
- 仅 Block(块级)节点支持包含子节点,内联节点无父子层级。
1.1、Block(块级节点)
Block 是 Node 的子类,作为所有块级节点的父类,代表 Markdown 文档中的独立块级元素(如段落、标题、代码块等)。
scala
public abstract class Block extends Node {
// 所有块级节点共享的基础属性和方法
}
Block的核心特点:
- 块级语义:占据独立的垂直空间,前后默认换行;
- 容器特性:可包含其他块级或内联节点;
- 独立性:在文档流中形成独立的结构单元。
在CommonMark-Java中,Block 的核心子类:
- Document:AST 根节点,无对应语法;
- Heading:标题(#/##/...),包含 level 属性(1-6 级);
- Paragraph:段落,最基础的文本容器;
- BlockQuote:引用块(> 开头),可嵌套其他块级节点;
- ListBlock:列表容器,子类为 OrderedList(有序)、BulletList(无序),子节点为 ListItem;
- ListItem:列表项,可包含其他块级节点;
- FencedCodeBlock:围栏代码块(```/~~~),包含 literal(代码内容)、info(语言标识);
- IndentedCodeBlock:缩进代码块(4 个空格 / 1 个制表符),包含 literal;
- HtmlBlock:HTML 块,包含 literal(原始 HTML 内容);
- ThematicBreak:分割线(---/*** /___),无内容仅作视觉分割。
1.2、Inline Nodes
内联节点是 AST 的组成部分,用于表示行内级元素(如文本、加粗、链接等),依附于块级节点存在(如段落内的加粗文本),无父子层级。
核心内联节点:
- Text:普通文本,包含 literal 属性;
- Emphasis:强调文本(斜体),包含 delimiter(* /_);
- StrongEmphasis:加粗文本,包含 delimiter(** /__);
- Link:链接,包含 destination(URL)、title(可选标题);
- Image:图片,包含 title 属性;
- Code:行内代码,包含 literal;
- HtmlInline:行内 HTML(如 ),包含 literal;
- SoftLineBreak:软换行(行尾 2 个空格 / 普通换行),渲染为空格 / 换行;
- HardLineBreak:硬换行(行尾 \),渲染为强制换行。
1.3、CustomNode
用于扩展自定义节点,满足 CommonMark 标准外的个性化解析需求。
1.4、小结
CommonMark-Java 生成的 AST 树形结构,与 Android 的 View Tree(视图树)高度相似:
- Block 节点 ≈ ViewGroup(可包含子节点);
- 内联节点 ≈ View(无嵌套)。 理解这一对应关系,能大幅降低后续扩展开发的理解成本。
2、解析流程
CommonMark-Java 的核心解析入口是 Parser 类(基于 Builder 模式构建),核心逻辑是将 Markdown 文本拆分为 "块级解析" 和 "内联解析" 两步:
核心调度代码如下:
java
public Node parseReader(Reader input) throws IOException {
if (input == null) {
throw new NullPointerException("input must not be null");
}
DocumentParser documentParser = createDocumentParser();
Node document = documentParser.parse(input);
return postProcess(document);
}
private DocumentParser createDocumentParser() {
return new DocumentParser(blockParserFactories, inlineParserFactory, delimiterProcessors);
}
最终解析逻辑由两个接口实现:
- BlockParser:负责块级节点的解析;
- InlineParser:负责内联节点的解析。 两者协同生成完整的 AST。
三、Markwon 框架
Markwon 基于 CommonMark-Java 的解析能力,扩展了 Android 端的 Markdown 渲染能力,核心采用 "插件化架构 + Span 渲染" 设计,支持自定义渲染规则和样式。
1、工作流程
Markwon 的渲染流程分为 6 个阶段,整体逻辑围绕 "解析 AST→预处理→渲染 Span→后处理→绑定到 TextView" 展开:
1.1、配置截断
Markwon 的核心配置通过 MarkwonBuilderImpl 的 build 方法完成,核心是初始化解析器、主题、配置、Visitor、Span 工厂,并通过插件扩展能力:
java
@NonNull
@Override
public Markwon build() {
if (plugins.isEmpty()) {
throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " +
"method to add them");
}
// please note that this method must not modify supplied collection
// if nothing should be done -> the same collection can be returned
final List<MarkwonPlugin> plugins = preparePlugins(this.plugins);
final Parser.Builder parserBuilder = new Parser.Builder();
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
for (MarkwonPlugin plugin : plugins) {
plugin.configureParser(parserBuilder);
plugin.configureTheme(themeBuilder);
plugin.configureConfiguration(configurationBuilder);
plugin.configureVisitor(visitorBuilder);
plugin.configureSpansFactory(spanFactoryBuilder);
}
final MarkwonConfiguration configuration = configurationBuilder.build(
themeBuilder.build(),
spanFactoryBuilder.build());
// @since 4.1.1
// @since 4.1.2 - do not reuse render-props (each render call should have own render-props)
final MarkwonVisitorFactory visitorFactory = MarkwonVisitorFactory.create(
visitorBuilder,
configuration);
return new MarkwonImpl(
bufferType,
textSetter,
parserBuilder.build(),
visitorFactory,
configuration,
Collections.unmodifiableList(plugins),
fallbackToRawInputWhenEmpty
);
}
Markwon 默认通过 create/builder 方法初始化,并自动添加 CorePlugin 核心插件:
java
public static Markwon create(@NonNull Context context) {
return builder(context)
.usePlugin(CorePlugin.create())
.build();
}
@NonNull
public static Builder builder(@NonNull Context context) {
return new MarkwonBuilderImpl(context)
// @since 4.0.0 add CorePlugin
.usePlugin(CorePlugin.create());
}
1.2、解析阶段
将 Markdown 字符串转换为 AST(Node 对象),核心复用 CommonMark-Java 的 Parser 能力,且解析前会通过插件预处理文本:
java
@NonNull
@Override
public Node parse(@NonNull String input) {
// make sure that all plugins are called `processMarkdown` before parsing
for (MarkwonPlugin plugin : plugins) {
input = plugin.processMarkdown(input);
}
return parser.parse(input);
}
1.3、预处理阶段
渲染前通过插件修改 AST、收集全局信息,核心调用插件的 beforeRender 方法:
java
@NonNull
@Override
public Spanned render(@NonNull Node node) {
for (MarkwonPlugin plugin : plugins) {
plugin.beforeRender(node);
}
...
return spanned;
}
插件可在此阶段执行:
- 修改 AST 结构(添加 / 删除 / 替换节点);
- 收集全局渲染信息(如统计代码块数量);
- 准备渲染上下文数据(如图片加载缓存)。
1.4、渲染阶段
将 AST 转换为 Android 的 Spanned 文本(核心是 Span 渲染),核心逻辑是通过访问者模式遍历 AST,为每个节点生成对应 Span:
java
@NonNull
@Override
public Spanned render(@NonNull Node node) {
...
// @since 4.1.1 obtain visitor via factory
final MarkwonVisitor visitor = visitorFactory.create();
node.accept(visitor);
...
return spanned;
}
遍历核心逻辑(MarkwonVisitorImpl):
java
private void visit(@NonNull Node node) {
// 获取节点对应的处理器
final NodeVisitor<Node> nodeVisitor = (NodeVisitor<Node>) nodes.get(node.getClass());
if (nodeVisitor != null) {
nodeVisitor.visit(this, node); // 生成对应Span
} else {
visitChildren(node); // 递归遍历子节点
}
}
1.5、后处理阶段
渲染完成后通过插件优化 Spanned 文本,核心调用插件的 afterRender 方法:
java
@NonNull
@Override
public Spanned render(@NonNull Node node) {
...
for (MarkwonPlugin plugin : plugins) {
plugin.afterRender(node, visitor);
}
//noinspection UnnecessaryLocalVariable
final Spanned spanned = visitor.builder().spannableStringBuilder();
return spanned;
}
1.6、应用阶段
将 Spanned 文本绑定到 TextView,核心是 setParsedMarkdown 方法,支持插件在绑定前后自定义逻辑:
java
@Override
public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) {
setParsedMarkdown(textView, toMarkdown(markdown));
}
@Override
public void setParsedMarkdown(@NonNull final TextView textView, @NonNull Spanned markdown) {
for (MarkwonPlugin plugin : plugins) {
plugin.beforeSetText(textView, markdown);
}
// @since 4.1.0
if (textSetter != null) {
textSetter.setText(textView, markdown, bufferType, new Runnable() {
@Override
public void run() {
// on-complete we just must call `afterSetText` on all plugins
for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView);
}
}
});
} else {
// if no text-setter is specified -> just a regular sync operation
textView.setText(markdown, bufferType);
for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView);
}
}
}
2、Plugin机制(扩展核心)
Plugin 是 Markwon 扩展性的核心,采用插件化架构设计,允许开发者自定义解析、渲染、绑定全流程的逻辑。
2.1、MarkwonPlugin接口定义
java
public interface MarkwonPlugin {
// 全局配置阶段:注册扩展能力
default void configure(@NonNull Registry registry) {}
// 配置构建阶段:自定义全局配置
default void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {}
// 渲染器配置:自定义节点Visitor
default void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {}
// 渲染前:修改AST/收集信息
default void beforeRender(@NonNull Node node) {}
// 渲染后:优化Spanned文本
default void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {}
// Span工厂配置:自定义节点Span样式
default void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {}
// 文本绑定前:预处理TextView
default void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {}
// 文本绑定后:后置处理(如图片懒加载)
default void afterSetText(@NonNull TextView textView) {}
}
- 配置阶段:configure() → configureConfiguration() → configureVisitor() → configureSpansFactory()
- 渲染阶段:beforeRender() → 核心渲染 → afterRender()
- 应用阶段:beforeSetText() → 设置文本 → afterSetText()
2.2、CorePlugin(核心默认插件)
CorePlugin 是 Markwon 的内置核心插件,其核心职责是为 CommonMark 标准的基础节点注册对应的 NodeVisitor 和 SpanFactory,覆盖所有核心块级 / 内联节点的默认渲染逻辑:
Java
public class CorePlugin extends AbstractMarkwonPlugin {
@NonNull
public static CorePlugin create() {
return new CorePlugin();
}
/**
* @return a set with enabled by default block types
* @since 4.4.0
*/
@NonNull
public static Set<Class<? extends Block>> enabledBlockTypes() {
return new HashSet<>(Arrays.asList(
BlockQuote.class,
Heading.class,
FencedCodeBlock.class,
HtmlBlock.class,
ThematicBreak.class,
ListBlock.class,
IndentedCodeBlock.class
));
}
// @since 4.0.0
private final List<OnTextAddedListener> onTextAddedListeners = new ArrayList<>(0);
// @since 4.5.0
private boolean hasExplicitMovementMethod;
protected CorePlugin() {
}
/**
* @since 4.5.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
public CorePlugin hasExplicitMovementMethod(boolean hasExplicitMovementMethod) {
this.hasExplicitMovementMethod = hasExplicitMovementMethod;
return this;
}
/**
* Can be useful to post-process text added. For example for auto-linking capabilities.
*
* @see OnTextAddedListener
* @since 4.0.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
public CorePlugin addOnTextAddedListener(@NonNull OnTextAddedListener onTextAddedListener) {
onTextAddedListeners.add(onTextAddedListener);
return this;
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
text(builder);
strongEmphasis(builder);
emphasis(builder);
blockQuote(builder);
code(builder);
fencedCodeBlock(builder);
indentedCodeBlock(builder);
image(builder);
bulletList(builder);
orderedList(builder);
listItem(builder);
thematicBreak(builder);
heading(builder);
softLineBreak(builder);
hardLineBreak(builder);
paragraph(builder);
link(builder);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// reuse this one for both code-blocks (indent & fenced)
final CodeBlockSpanFactory codeBlockSpanFactory = new CodeBlockSpanFactory();
builder
.setFactory(StrongEmphasis.class, new StrongEmphasisSpanFactory())
.setFactory(Emphasis.class, new EmphasisSpanFactory())
.setFactory(BlockQuote.class, new BlockQuoteSpanFactory())
.setFactory(Code.class, new CodeSpanFactory())
.setFactory(FencedCodeBlock.class, codeBlockSpanFactory)
.setFactory(IndentedCodeBlock.class, codeBlockSpanFactory)
.setFactory(ListItem.class, new ListItemSpanFactory())
.setFactory(Heading.class, new HeadingSpanFactory())
.setFactory(Link.class, new LinkSpanFactory())
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
OrderedListItemSpan.measure(textView, markdown);
// @since 4.4.0
// we do not break API compatibility, instead we introduce the `instance of` check
if (markdown instanceof Spannable) {
final Spannable spannable = (Spannable) markdown;
TextViewSpan.applyTo(spannable, textView);
}
}
@Override
public void afterSetText(@NonNull TextView textView) {
// let's ensure that there is a movement method applied
// we do it `afterSetText` so any user-defined movement method won't be
// replaced (it should be done in `beforeSetText` or manually on a TextView)
// @since 4.5.0 we additionally check if we should apply _implicit_ movement method
if (!hasExplicitMovementMethod && textView.getMovementMethod() == null) {
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
private void text(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Text.class, new MarkwonVisitor.NodeVisitor<Text>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) {
final String literal = text.getLiteral();
visitor.builder().append(literal);
// @since 4.0.0
if (!onTextAddedListeners.isEmpty()) {
// calculate the start position
final int length = visitor.length() - literal.length();
for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) {
onTextAddedListener.onTextAdded(visitor, literal, length);
}
}
}
});
}
....
}
CorePlugin 覆盖的核心节点包括:文本、粗体、斜体、引用块、行内代码、代码块、图片、列表、分割线、标题、换行、段落、链接等。
四、扩展Markwon(解决 Span 扩展性不足问题)
1、当前Markwon存在的问题
Markwon 核心库基于 TextView 的 Span 机制实现渲染,所有节点最终转换为 Span 展示。这种方式的核心痛点是UI 扩展性不足:
- Span 仅能实现静态样式(如颜色、字体),无法支持动态交互(如代码块添加复制按钮);
- 复杂布局(如嵌套表格、自定义卡片)无法通过 Span 实现;
- 流式输出时的性能优化成本高。
2、markwon-recycler 扩展库(分块渲染解决方案)
Markwon 提供 markwon-recycler 扩展库,通过 RecyclerView 实现 Markdown 的分块渲染,解决 Span 扩展性不足的问题。其核心是 MarkwonAdapter(RecyclerView.Adapter 的抽象子类,默认实现为 MarkwonAdapterImpl),设计思路是:将 AST 按块级节点拆分,每个块级节点作为 RecyclerView 的一个条目独立渲染,支持自定义每个块的布局和交互。
2.1、 MarkwonAdapter 核心代码
java
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
// 构建方法:支持默认布局/自定义Entry
public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
}
public static Builder builder(@LayoutRes int defaultEntryLayoutResId, @IdRes int defaultEntryTextViewResId) {
return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
}
// 核心方法:绑定Markdown数据
public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown);
public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document);
public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List<Node> nodes);
// 条目渲染规则抽象类
public static abstract class Entry<N extends Node, H extends Holder> {
public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
public void clear() {}
public long id(@NonNull N node) { return node.hashCode(); }
public void onViewRecycled(@NonNull H holder) {}
}
// RecyclerView.ViewHolder基类
public static class Holder extends RecyclerView.ViewHolder {
public Holder(@NonNull View itemView) { super(itemView); }
// 省略视图查找方法...
}
// 构建器接口
public interface Builder {
<N extends Node> Builder include(@NonNull Class<N> node, @NonNull Entry<? super N, ? extends Holder> entry);
Builder reducer(@NonNull MarkwonReducer reducer);
MarkwonAdapter build();
}
}
2.2、MarkwonAdapter 核心能力
- 分块渲染:通过 MarkwonReducer 将 AST 根节点(Document)拆分为多个独立的块级节点,每个块作为 RecyclerView 的一个条目单独渲染;
- 自定义渲染:为特定节点类型(如 FencedCodeBlock、TableBlock)注册自定义 Entry(渲染规则),实现差异化布局 / 交互;
- 灵活构建:支持基于默认布局(TextView)或自定义 Entry 快速构建适配器,适配不同业务场景。
2.3、Entry 抽象类(单块渲染规则)
Entry 是 MarkwonAdapter 的核心,定义单个块级节点的渲染逻辑,需实现以下核心方法:
- createHolder ():基于布局文件创建 ViewHolder,定义块级节点的 UI 布局;
- bindHolder ():将 Markdown 节点数据绑定到 ViewHolder,实现数据与 UI 的关联;
- 可选方法:clear ()(清理渲染缓存)、id ()(自定义条目 ID)、onViewRecycled ()(回收视图)。
3、基于 Markwon 的扩展方案总结
至此, 基于Markwon来封装Markdown渲染组件的整体方案就比较清晰了。
3.1、内联节点:复用 Span 渲染
内联节点(如加粗、链接、行内代码等)无需注册 Entry------ 内联节点依附于块级节点存在(如段落、列表项内的加粗文本),继续通过 Span 在 TextView 中渲染,兼顾渲染效率和基础样式需求。
3.2、块级节点:自定义 Entry 渲染
块级节点(如代码块、引用块、列表等)需为其注册自定义 Entry:
- 为每个块级节点类型(如 FencedCodeBlock)实现 Entry,自定义布局(如代码块添加 "复制" 按钮);
- 通过 MarkwonAdapter 的 include () 方法将 "节点类型 - Entry" 关联:
实例代码:
java
MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.item_default, R.id.tv_content)
// 为代码块注册自定义Entry
.include(FencedCodeBlock.class, new CodeBlockEntry())
// 为引用块注册自定义Entry
.include(BlockQuote.class, new BlockQuoteEntry())
.build();
MarkwonAdapter中的include方法:
less
@NonNull
@Override
public <N extends Node> Builder include(
@NonNull Class<N> node,
@NonNull Entry<? super N, ? extends Holder> entry) {
//noinspection unchecked
entries.append(node.hashCode(), (Entry<Node, Holder>) entry);
return this;
}
3.3、嵌套块级节点的处理
块级节点存在嵌套场景(如引用块嵌套列表、列表嵌套列表),处理方式为递归渲染子节点:在父节点 Entry 的 bindHolder () 方法中,遍历父节点的子节点,复用 MarkwonAdapter 的 Entry 和 ViewHolder 逻辑,将子节点 View 嵌入父节点布局容器:
java
// 父节点Entry的bindHolder方法示例(以引用块为例)
@Override
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull BlockQuote node) {
// 获取父节点的布局容器
LinearLayout container = holder.requireView(R.id.container_quote);
container.removeAllViews();
// 遍历引用块的子节点(如列表、段落)
Node child = node.getFirstChild();
while (child != null) {
// 获取子节点对应的ViewType
int childViewType = markwonAdapter.getNodeViewType(child.getClass());
// 创建子节点ViewHolder
MarkwonAdapter.Holder childHolder = markwonAdapter.onCreateViewHolder(container, childViewType);
if (markwonAdapter instanceof MarkwonAdapterImpl) {
MarkwonAdapterImpl adapterImpl = (MarkwonAdapterImpl) markwonAdapter;
// 获取子节点对应的Entry
MarkwonAdapter.Entry<Node, MarkwonAdapter.Holder> childEntry = adapterImpl.getEntry(childViewType);
// 绑定子节点数据
childEntry.bindHolder(markwon, childHolder, child);
// 将子节点View添加到父容器(实现嵌套)
container.addView(childHolder.itemView);
}
child = child.getNext();
}
}
上述逻辑的核心是 "复用已有 Entry",避免重复编写嵌套节点的渲染规则,保证扩展方案的一致性和可维护性。
五、总结
本文主要是围绕Android 端Markdown渲染的高性能、高定制化的需求展开。介绍了CommonMark-Java 解析框架以及Markwon 基于 Span 的插件化渲染体系,同时讨论基于markwon-recycler 的分块渲染方案。
后续可推进的优化点
Span复用
内联节点仍依赖 Span 渲染,当前每次渲染都会为相同内容 / 样式的内联节点重复创建 Span 对象,存在内存与 GC 开销。优化方向:
- 针对 Emphasis、StrongEmphasis、Code 等高频内联节点,构建 Span 缓存池,以 "节点类型 + 内容 + 主题样式" 为 key 复用 Span 实例;
- 结合 Android 的 Span 回收机制,在 RecyclerView 条目回收时释放非全局复用的 Span,平衡复用效率与内存占用。
RecyclerView Diff 优化
当前 MarkwonAdapter 默认以节点 hashCode 作为条目 ID,未实现 DiffUtil,流式输出时会全量刷新列表,性能损耗大。优化方向:
- 基于 AST 节点的 "类型 + 内容哈希 + 层级位置" 实现 DiffUtil.Callback,精准识别节点的增、删、改、移操作;
- 仅刷新变更的 RecyclerView 条目,尤其适配 AI 流式输出 Markdown 的场景,提升增量渲染的流畅性;
- 优化条目 ID 生成逻辑(避免 hashCode 冲突),结合稳定 ID 机制进一步降低刷新损耗。