TextView预渲染研究

优化TextView渲染
本文介绍了一种通过预渲染静态布局来优化Android中TextView渲染效率的方法,该方法通过避免不必要的监听和计算,有效提高了列表视图中大量文本的显示性能。

Android中的TextView是整个framework中最复杂的控件之一,负责Android中显示文本的大部分工作,framwork中的许多控件也直接或者间接的继承于TextView,例如Button,EditText等。其内部实现也相当复杂,单论代码行数来说,android-22中TextView有足足9509行,另外,TextView中许多操作都非常繁重,例如setText操作,需要设置SpanWatcher,或者需要重现创建一个SpannableString,还需要根据情况重新创建Text Layout,这些操作加起来之后令一次setText操作非常耗时。为了提升TextView的渲染效率,最近研究了一下预渲染的方法,接下来给大家讲解一下原理。

TextView渲染基本原理

首先来介绍下TextView的基本渲染原理,总的来说,TextView中负责渲染文字的主要是这三个类:

  • BoringLayout
    主要负责显示单行文本,并提供了isBoring方法来判断是否满足单行文本的条件。
  • DynamicLayout
    当文本为Spannable的时候,TextView就会使用它来负责文本的显示,在内部设置了SpanWatcher,当检测到span改变的时候,会进行reflow,重新计算布局。
  • StaticLayout
    当文本为非单行文本,且非Spannable的时候,就会使用StaticLayout,内部并不会监听span的变化,因此效率上会比DynamicLayout高,只需一次布局的创建即可,但其实内部也能显示SpannableString,只是不能在span变化之后重新进行布局而已。

另外,以上三个类都继承于Layout类,在此类中统一负责文本的具体绘制,在Layout.draw方法中,会对文本一行一行的进行渲染:

TextLine tl = TextLine.obtain();

// Draw the lines, one at a time.
// The baseline is the top of the following line minus the current line's descent.
for (int i = firstLine; i <= lastLine; i++) {
      ....
      Directions directions = getLineDirections(i);
      if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
         // XXX: assumes there's nothing additional to be done
          canvas.drawText(buf, start, end, x, lbaseline, paint);
      } else {
          tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
          tl.draw(canvas, x, ltop, lbaseline, lbottom);
      }
}
TextLine.recycle(tl);

可以看出来对于Spannble,或者包含emoji的文本的话,实际渲染操作是交给了TextLine去绘制,否则直接使用canvas.drawText,TextLine负责单行复杂文本的绘制,其中Spannable, Emoji之类的绘制逻辑都包含在里面,TextLine的绘制逻辑也并非十分高效,这里后续将会继续说明其应该如何优化。

TextLayoutCache

Canvas在drawText的时候,如果需要每次都计算字体的大小,边距等之类的话,就会非常耗时,导致drawText时间会拉的很长,为了提高效率,android在4.0之后引入了TextLayoutCache,使用LRU Cache缓存了字形,边距等数据,提升了drawText的速度,在4.4中,这个cache的大小是0.5M,全局使用,并且会在Activity的configurationChanged, onResume, lowMemory, updateVisibility等时机,会调用Canvas.freeTextLayoutCache来释放这部分内存。由于这部分的cache是系统底层控制的,我们无法做具体的控制。

TextView的预渲染优化

从TextView的渲染原理来看,如果只是单纯的显示文本,我们根本不需要另外设置SpanWatcher来监听span的变化,因此我们可以直接使用BoringLayout或者StaticLayout来直接显示文本内容,但是BoringLayout只能显示单行文本,因此这里最好的选择是直接用StaticLayout

我们选择了自定义View,并希望最终有这样的一个接口:

public class StaticLayoutView extends View {
    private Layout layout = null;
    public void setLayout(Layout layout) {
        this.layout = layout;
        requestLayout();
    } 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        if (layout != null) {
            layout.draw(canvas, null, null, 0);
        }
        canvas.restore();
    }
}

我们可以直接通过设置这个view的Layout来绘制文本,并在onDraw方法中直接使用这个Layout对象来绘制文本。在这里我们摒弃了setText方法,直接通过Layout来绘制文本,而这里的Layout对象,我们可以通过预先创建之后才设置进去(这里可以放到单独的一个线程中创建),这样对比起普通TextView的setText方法,我们减少了setText中的许多消耗,可以大幅度的提升效率。

StaticLayout的创建非常简单,只需要给定文本,宽度等就能直接创建。另外,为了预先填充TextLayoutCache,我们也可以在创建完StaticLayout对象之后,预先在一个dummy canvas中draw出来:


StaticLayout layout = new StaticLayout(TestSpan.getSpanString(i), textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);
layout.draw(dummyCanvas);

性能对比
接下来我们测试一下具体的性能,这里的testcase放到了Github上:StaticLayoutView

testcase的内容为,在一个ListView中,显示300个Item,每个item都是一段纯文本,里面全都是包含有大量ImageSpan的SpannableString,进行两边的对比,一边是直接使用StaticLayout,一边是使用普通的TextView,并且这300段文本不全相同,长度不同,随机生成,在StaticLayout的testcase中,StaticLayout都是预先在另外一个线程创建好之后才设置进去的,另外SpannableString也是预先生成好的。

另外,在这里为了模拟真实app繁重的后台工作,另外创建了3个线程,不停在做浮点预算以尝试抢占CPU资源。

测量性能的指标为,ListView连续向下滚动,测量其平均帧率为多少,分别测量五次,计算其平均值,最终性能测试结果如下:

这里写图片描述

这里测试的机器是MX3,左侧是直接使用StaticLayout的方案,右侧是系统的默认方案,Y轴是FPS,可以看出来,使用优化之后的方案,帧率提升了许多。

References
Improving Comment Rendering on Android
这篇文章介绍了Instagram如何优化他们的TextView渲染的效率,这也是这里优化方法的来源,Instagram也是直接使用StaticLayout并通过预先创建Layout的方法来减少了ListView滚动过程中的掉帧率,并且效果非常显著。这篇文章算是给出了这里的原理解析以及一个简单的实现

<think>嗯,用户想知道在AndroidTextView上如何渲染Markdown。首先,我得回忆一下Android原生支持的文本样式有哪些。TextView本身支持一些基本的HTML标签,比如<b>、<i>,但Markdown的话可能需要更多的处理。 然后,用户可能希望显示更复杂的Markdown元素,比如链接、列表、图片等。这时候可能需要用到第三方库,比如Markwon或者AndroidTextView结合SpannableStringBuilder。不过原生支持有限,可能需要自己解析Markdown并转换成Spannable。 接着,要考虑是否需要支持图片加载,因为Markdown中的图片需要网络或本地资源加载,这可能涉及到Glide或Picasso这样的库,但需要额外处理。另外,链接的点击事件也需要处理,可能需要设置MovementMethod。 用户可能没有提到性能问题,但如果Markdown内容很长,解析可能会影响性能,这时候需要异步处理,比如在后台线程解析,再在主线程更新UI。或者使用预处理的库来提高效率。 另外,是否需要支持复杂的表格或代码块?如果用户的需求比较简单,可能基础的库就足够了,否则可能需要更强大的解决方案,比如WebView加载渲染后的HTML,但这样会牺牲一些原生体验。 总结下来,步骤大概是:解析Markdown为Spannable,应用样式到TextView,处理图片和链接,考虑性能优化。需要比较不同的库,比如Markwon的功能是否满足,或者是否需要自定义解析。同时提醒用户注意安全,比如防止XSS攻击,如果使用WebView的话。</think>在 AndroidTextView 上渲染 Markdown 需要分步骤实现。以下是具体方法和技术路线: --- ### 一、原生基础方案 1. **内置 HTML 支持** ```kotlin textView.text = Html.fromHtml( "<b>加粗</b> <i>斜体</i> <a href='https://example.com'>链接</a>", Html.FROM_HTML_MODE_COMPACT ) textView.movementMethod = LinkMovementMethod.getInstance() ``` - ✅ 优点:无需第三方库 - ❌ 限制:仅支持部分 HTML 标签 2. **SpannableString 自定义** ```kotlin val spannable = SpannableString("特殊样式") spannable.setSpan(StyleSpan(Typeface.BOLD), 0, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannable ``` --- ### 二、第三方库解决方案(推荐) #### 方案 1:使用 Markwon 库 $$ \text{Markwon} = \text{Markdown 解析器} + \text{Android 样式渲染} $$ **实现步骤:** 1. 添加依赖 ```gradle implementation 'io.noties.markwon:core:4.6.2' ``` 2. 基础使用 ```kotlin val markwon = Markwon.create(context) markwon.setMarkdown(textView, "# 标题\n- 列表项\n**加粗**") ``` 3. 高级功能扩展 ```kotlin val markwon = Markwon.builder(context) .usePlugin(LinkifyPlugin.create()) // 链接支持 .usePlugin(ImagesPlugin.create()) // 图片支持(需额外配置) .build() ``` #### 方案 2:Jetpack Compose 方案 ```kotlin @Composable fun MarkdownText(content: String) { val markdown = remember { AnnotatedString.Builder() } // 自定义解析逻辑... Text(text = markdown.toAnnotatedString()) } ``` --- ### 三、关键问题解决 1. **图片加载方案** - 使用 Glide/Picasso 集成 ```kotlin Markwon.builder(context) .usePlugin(ImagesPlugin.create { plugin -> plugin.addMediaDecoder(GlideImagesPlugin.create(context)) }) ``` 2. **代码块高亮** ```gradle implementation 'io.noties.markwon:syntax-highlight:4.6.2' ``` ```kotlin .usePlugin(SyntaxHighlightPlugin.create(Prism4j(GrammarLocatorDef()))) ``` 3. **表格支持** ```gradle implementation 'io.noties.markwon:ext-tables:4.6.2' ``` --- ### 四、性能优化建议 1. 异步解析策略 ```kotlin lifecycleScope.launch(Dispatchers.Default) { val spanned = markwon.toMarkdown(content) withContext(Dispatchers.Main) { textView.text = spanned } } ``` 2. 缓存机制 ```kotlin val markwon = Markwon.builder(context) .usePlugin(HtmlPlugin.create { htmlPlugin -> htmlPlugin.excludeDefaults(true).addHandler(object : TagHandler { // 自定义缓存策略 }) }) ``` --- ### 五、方案对比 | 方案 | 维护成本 | 功能完整性 | 性能表现 | |---------------------|----------|------------|----------| | 原生 HTML | ★★☆☆☆ | ★★☆☆☆ | ★★★★☆ | | Markwon | ★★★★☆ | ★★★★☆ | ★★★☆☆ | | WebView 渲染 | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | --- ### 六、安全注意事项 1. 防范 XSS 攻击(当渲染 HTML 时) 2. 限制最大渲染长度(防止超长文本导致 OOM) 3. 使用 `android:textIsSelectable` 控制内容复制权限 建议优先选择 **Markwon** 方案,其在功能完整性和维护便利性之间取得较好平衡。对于复杂文档场景,可考虑结合 WebView 实现混合渲染方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值