Weex Android 文字渲染优化

Weex Android 文字渲染优化

背景

在做Weex Android适配工作的时候,发现当Text没有设置高度,需要Weex根据文字内容、样式,计算出宽高的时候,在小米手机上可能会出现文字截断现象。

例如,前端期望如下图所示的渲染效果:

然而在小米手机上的渲染效果却是下面这样,默认标题那一段最后一行的文本被截断了:

原因

在Android系统中,View的渲染可以分为Measure,Layout,Draw三步,对于Measure这一步,Weex和原生Android略有不同:

  • 在Android系统中,默认渲染文字的方式是使用TextView及其子类,TextView的宽度或高度可以使用wrap_content,match_parent或指定的值。
  • 在Weex中,Weex View的宽度和高度是由CSS属性指定或者css-layout根据flex属性计算出来的,Layout的时候使用FrameLayout.LayoutParams进行布局,因此并不存在wrap_content,match_parent这两个概念。对于文字View,如果在CSS中没有指定widthheight,css-layout会要求文字节点(TextDom)根据文字内容和文字样式(如font-size,font-family,line-height等),使用android.text.Layout,计算出该节点的宽度和高度,然后使用Weex view的布局方式。

在Draw这一步的时候,无论是Weex还是原生Android,都是使用TextView.onDraw()进行绘制的。在绘制文字的时候,TextView会根据文字内容和文字样式生成一个android.text.Layout对象,并根据此对象把文字画出来。

Weex渲染Text的过程可以用下图表示:

由于DOM和View针对同一个TextDom,生成了两个android.text.Layout对象。而android.text.Layout是一个接口,在DOM层和View层上可能使用了不同的实现,即DOM和View生成的android.text.Layout可能不一样。换句话说,DOM层负责Measure,View层负责Draw,Measure与Draw的结果可能存在差异,这样就可能导致了文字截断现象。

例如,对于一段中英文混排的文字,DOM层可能把文字计算成4行,而由于换行规则(android.text.StaticLayout.nComputeLineBreaks())不同,View层可能把文字计算成3行,这样就出现了Measure和Draw的结果不一致,发生了文字截断现象。

解决方案

DOM层和View层使用同一个对象分别进行Measure和Draw,确保Measure和Draw的结果一致,即可解决此问题。这个方案需要解决两个问题:

  • 使用一种统一的方案表示多种文字样式(font-size,font-family等)。
  • 找到一种方法,可以根据文字样式和文字内容计算文字区块的高度和宽度。

对于第一个问题使用Span解决,第二个问题则使用Layout机制解决。

Span

Android中的Span类似于html的<span>标签,可以用来描述一段inline文本的样式。

Span可以大致分为如下三种类型:

  • CharacterStyle,能影响文本中的每一个Character显示效果的Span。
  • ParagraphStyle,能影响文本一段或者一行显示效果的Span。
  • UpdateAppearance,能动态修改文本中每一个Character显示效果的Span。

很多关于文字的CSS style都可以映射到某种类型的Span上。例如,

  • font-size可以映射到android.text.style.AbsoluteSizeSpan
  • font-weightfont-style都可以映射到android.text.style.StyleSpan
  • color可以映射到android.text.style.ForegroundColorSpan
  • text-decoration可以映射到android.text.style.UnderlineSpanandroid.text.style.StrikethroughSpan
  • font-family可以映射到android.text.style.TypefaceSpan
  • text-align可以映射到android.text.style.AlignmentSpan
  • line-height可以映射到android.text.style.LineHeightSpan

Weex还支持text-overflowlines(某段文字最多显示几行)两个属性,这两个属性并没有原生的Span与之对应,需要自行实现。

text-overflowlines

lines属性指定了文字区块最多可以显示几行,text-overflow属性则说明了如果文字的行数超过了lines要如何处理,这两个属性在逻辑上关联紧密,实现的时候需要一起处理。

如果只需要支持Android API 23及以上,上述两个属性已有原生实现,使用StaticLayout.Builder即可。然而在Weex中,minSdkVersion为14,因此无法使用StaticLayout.Builder

故Weex使用如下过程来支持text-overflowlines属性:

  1. 检查当前行数是否超过lines,如果是,进入2,否则结束。
  2. 找到最后一行的第一个字符和最后一个字符,如果二者不是一个字符,进入3,否则结束。
  3. 将文字分为末行和非末行。进入4。
  4. 如果text-overflow是ellipse,用\u2026(HORIZONTAL ELLIPSIS)替换末行最后一个字符,否则什么都不做。进入5。
  5. 将非末行文字和末行文字拼接成一个新的字符串并重新layout。进入1。

在Weex中以上计算过程是一个递归的过程,当前行数小于等于lines时,则停止递归。伪代码如下:

function updateLines(text, ellipse, lines)
    layout = makeLayout(text)
    if lines > 0 && lines < layout.lineCount
        //Let lastLineStart and lastLineEnd be the index of the character
        lastLineStart = layout.lineStart(lines - 1)
        lastLineEnd = layout.lineEnd(lines - 1)
        if lastLineStart < lastLineEnd
            mainText = text.slice(0, lastLineStart)
            remainder = text.slice(lastLineStart, !ellipse ? lastLineEnd : lastLineEnd - 1)
            text = main
            text += remainder
            //text is a string, and += is the string concatenation operation
            if ellipse
                text += '\u2026'
            updateLines(text, ellipse, lines)

line-height

android.text.style.LineHeightSpan并不能直接指定line-height,只能通过设置TopAscentDescentBottom几个属性,从而间接设置line-height。下图阐述了这几个参数的意义:

根据上图,line-height可以被定义为AscentDescent之间的距离。当指定的line-height大于原始的AscentDescent之间的距离时,需要扩大AscentDescent之间的距离,反之需要缩小AscentDescent之间的距离。

有如下约定:

  • basline为x轴,向下为正,向上为负。
  • leading=line-height-(descent-ascent)
  • half-leading=leading/2
  • leadinghalf-leading可能为负数。

如果leading不为0,需要根据half-leading调整AscentDescent。此外,根据StaticLayout的源代码,AscentDescent并不会作用于首行顶部和末行尾部,需要调整TopBottom以处理首行和末行。上述逻辑可以用如下伪代码表示:

halfLeading = (lineHeight-(descent-ascent))/2;
top -= halfLeading;
bottom += halfLeading;
ascent -= halfLeading;
descent += halfLeading;

Build a span

在Android中,构建String可以使用StringBuilder,构建Span的时候,则可以使用Spannable接口的三个子类:

  • SpannedString适用于文字的内容和文字的span都不变化的场景。
  • SpannableString适用于文字的内容不变,但是文字的span可能变化的场景。
  • SpannableStringBuilder适用于文字和文字的span都可能变化的场景。

在Weex中,使用的是SpannableString,每次更新文字内容,会创建一个新的Span

Layout

使用Spannable接口后,得到的仅仅是一个文本流,并不包含文字区域的高度、宽度、首行、末行这些与Measure或Layout相关的内容,因此还需要使用android.text.Layout对文字进行Measure和Layout。使用android.text.Layout时,把Spannable与文字区块的宽度做为Layout的构造函数的参数,即可完成文字的Layout过程。android.text.Layout有以下三种实现方式:

  • BoringLayout单行文字,文字方向为LTR。
  • StaticLayout根据Spannable和指定宽度计算文字行数,文字方向由文字内容决定
  • DynamicLayout除了含有StaticLayout的功能外,还包含动态更新功能。当Spannable更新的时候,Layout.getLines()也会随之变化。在内部实现上,DynamicLayout有一个Watcher,这个Watcher观察着Spannable的变化。DynamicLayout一般与SpannableStringBuilder配合使用。

此外,可以使用Layout.draw(Canvas c)来把Layout对象画在指定的Canvas上。在DOM中生成Layout对象,计算出文字的宽度和高度后,把Layout对象传递给View,View调用Layout.draw(Canvas c)即可把文字画出来,这样就保证了Layout与Draw的一致性。

线程同步

在Weex中,DOM相关的操作运行在DOM线程中,View相关的操作运行在UI线程中,二者可能同时操作同一个Layout对象,这样就存在着线程同步问题。考虑到加锁对性能的影响,Weex没有使用锁,而是AtomicReference解决这个问题。

DOM线程内有两个android.text.Layout对象,一个是TextDom的私有成员变量,一个是AtomicReference中保存的引用。之后使用如下机制保证UI线程和DOM线程不会操作同一个android.text.Layout对象,避免了加锁带来的额外开销。

  • UI线程通过AtomicReference来读取Layout对象。
  • DOM线程在计算开始的时候,生成一个新的android.text.Layout对象。在计算过程中把计算的中间结果也保存到这个对象中。DOM线程计算结束后,把计算结果更新到AtomicReference中,同时清空私有成员变量android.text.Layout

即UI线程负责Read,DOM线程负责Write;Read与Write操作的不是同一个对象,在DOM线程完成工作后,会更新Read输出的对象。

性能

  • 优化前的方案使用Layout和Text对象进行渲染,一次渲染需要生成两个Layout对象;
  • 优化后的方案使用Layout和对象进行文字渲染,一次渲染只需要生成一个Layout对象;
    因此,在优化后,可以预期性能会得到一定幅度的提升。

下面分两种场景测试文字性能。

一段长文本

使用上图所示的一段长文本做为测试文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:

次数 优化前首屏加载时间 优化后首屏加载时间
1 825 813
2 832 721
3 816 761
4 852 756
5 850 750
6 838 766
7 863 781
8 846 780
9 793 753
10 905 727
平均 842 760.8

根据以上数据,可以看到优化后,一段长文本的渲染性能得到一定的提升,上述数据的性能提升幅度为(842-760.8)/842=9.6%。

多段短文本

使用上图所示的多段短文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:

次数 优化前首屏加载时间 优化后首屏加载时间
1 987 987
2 1056 869
3 948 880
4 997 822
5 969 947
6 1036 967
7 939 900
8 869 878
9 826 949
10 931 832
平均 955.8 903.1

根据以上数据,可以看到优化后,多段短文本的渲染性能也得到了一定的提升,上述数据的性能提升幅度为(955.8-903.1)/955.8=5.5%。

结论

使用以上的优化策略,在小米手机上得到了如下的文字渲染效果,可以看到,文字截断的现象消失了。

上述优化策略的流程如下图所述:

这次改进使用Layout和Span机制,解决了Measure和Draw不一致的问题,避免了为小米手机编写额外适配逻辑的成本。

此外,首屏加载性能也得到了小幅度提升。

参考文章

  1. http://instagram-engineering.tumblr.com/post/114508858967/improving-comment-rendering-on-android
  2. http://flavienlaurent.com/blog/2014/01/31/spans/
  3. http://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font
时间: 2024-12-30 22:16:59

Weex Android 文字渲染优化的相关文章

Weex Android 动画揭秘

背景 在目前常见的交互方式中,动画扮演了一个重要的角色. 在 Weex 框架下,Weex 的动画需要屏蔽 CSS/JS 动画与 Android 动画系统的差异,并尽可能的达到60FPS. 本文阐述了在 Android 上实现高性能CSS/JS动画过程中所遇到的问题/相关数学知识及解决方案.本文使用的前端 DSL 为 Weex vue 1.0或 Weex Vue 2.0. 现状与问题 在 Weex 环境下, 一个典型的动画在前端DSL中的写法如下: animation = weex.require

浅析安卓(Android)的性能优化_Android

Android性能的优化主要分为两点 1.布局优化 2.内存优化 布局优化 首先来看一下布局优化,系统在渲染UI的时候会消耗大量的资源,所以,对布局的优化就显得尤为重要 避免Overdraw 也就是避免过度的绘制,过度的绘制会浪费更多的资源,举个例子,Android系统会默认绘制Activity的背景,这时候我们再设置一个背景,这样默认的背景就属于过度绘制了,在『开发者工具』中有一个『调试GPU过度绘制』的选项,我们打开就可以通过颜色来判断过度绘制的次数 如图: 所以说我们尽可能的增大蓝色区域,

Weex Android Border绘制

Weex Android Border绘制 在Android/iOS/H5开发中,为View/Component添加border是一个常见的需求.然而Android SDK并没有直接提供border的支持,需要开发者自行实现.本文首先阐述border问题的定义以及几种棘手的case,随后探讨目前Android平台上解决border问题的方案及其优劣,之后详细阐述Weex的border解决方案,并总结此问题. 背景 在Weex中,border实际上代表了四个属性,即border-width, bo

Android应用性能优化最佳实践.

移动开发 Android应用性能优化最佳实践 罗彧成 著 图书在版编目(CIP)数据 Android应用性能优化最佳实践 / 罗彧成著. -北京:机械工业出版社,2017.1 (移动开发) ISBN 978-7-111-55616-9 I. A- II. 罗- III. 移动终端-应用程序-程序设计 IV. TN929.53 中国版本图书馆CIP数据核字(2016)第315986号 Android应用性能优化最佳实践 出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037

浅析安卓(Android)的性能优化

Android性能的优化主要分为两点 1.布局优化 2.内存优化 布局优化 首先来看一下布局优化,系统在渲染UI的时候会消耗大量的资源,所以,对布局的优化就显得尤为重要 避免Overdraw 也就是避免过度的绘制,过度的绘制会浪费更多的资源,举个例子,Android系统会默认绘制Activity的背景,这时候我们再设置一个背景,这样默认的背景就属于过度绘制了,在『开发者工具』中有一个『调试GPU过度绘制』的选项,我们打开就可以通过颜色来判断过度绘制的次数 如图: 所以说我们尽可能的增大蓝色区域,

Android布局性能优化之按需加载View

有时应用程序中会有一些很少用到的复杂布局.在需要它们的时候再加载可以降低内存的消耗,同时也可以加快界面的渲染速度. 定义ViewStub ViewStub是一个轻量级的View,它没有高宽,也不会绘制任何东西.所以它的加载与卸载的成本很低.每个ViewStub都可以使用android:layout属性指定要加载的布局. 下面这个ViewStub用于一个半透明的ProgressBar的加载.它只有在新工作开始时才会显示. <ViewStub android:id="@+id/stub_imp

求android文字识别能成功的demo

问题描述 求android文字识别能成功的demo 关于android 文字识别 ,好像除了5个相关jar包以外,到现在都没找到个demo,解决这个问题 啊 解决方案 http://www.cnblogs.com/hangxin1940/archive/2012/01/13/2321507.htmlhttp://blog.csdn.net/xieyan0811/article/details/5931619http://www.pudn.com/downloads524/sourcecode/c

Android开发:优化ListView实践解析

 在看了一些vogella的文章之后,发现关于android listview性能优化这一段很有意思,于是实践了一下,经过优化,性能确实提升不少! 先看看优化前和优化后的比较: 优化前的log截图: 开发:优化ListView实践解析-"> 优化后的log截图: 并且,在不停滚动ListView的过程中,优化之前会出现ANR现象,在AVD上特别容易复现: 然后,优化后显得很流畅,附上对于的log截图: 下面附上相关代码分析: ListView中的每一个Item由一个ImageView 和一

Android零基础入门第13节:Android Studio配置优化,打造开发利器

原文:Android零基础入门第13节:Android Studio配置优化,打造开发利器 是不是很多同学已经有烦恼出现了?电脑配置已经很高了,但是每次运行Android程序的时候就很卡,而且每次安装运行程序都要等待很长时间,如果是在开发后期需要不停的修改代码运行看效果,这必定会影响工作效率. 有什么办法可以改善一下这些问题呢?方法是肯定会有的,接下来通过两期来从两个维度来提高效率.今天首先来优化配置我们的Android Studio开发工具,将一些使用很少但占有内存的插件屏蔽,将影响效率的地方