欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android TextView预渲染研究

程序员文章站 2024-03-06 10:36:31
android中的textview是整个framework中最复杂的控件之一,负责android中显示文本的大部分工作,framwork中的许多控件也直接或者间接的继承于t...

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连续向下滚动,测量其平均帧率为多少,分别测量五次,计算其平均值,最终性能测试结果如下:

Android TextView预渲染研究

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

references

improving comment rendering on android

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

以上就是对android textview预渲染 的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!