每次我用TextView组件,我都会抱怨不停,Android的TextView的设计师一定没有ListView设计师牛逼,在我的认知里,ListView是Android中一个伟大的组件,伟大到无与伦比,而TextView则是糟糕透顶的组件,糟糕到恶心的境界.

当然,我没有资格对TextView与ListView进行评头论足,但是,无知的我,对于这两个组件只能认知到如此地步.

当对大文本进行编辑,或者你的文本大小只有短短的几百K,TextView内存都会飙升的溢出,终于莫一天我受够了,我开始思考怎样去改进TextView中的这个重大缺陷,

我们先来分析Android中TextView的代码劣势:

1.没有使用缓存技术,(如果硬要把Spans中的分页效果说成缓存技术,那就太做作了),

2.陷得太深,类与类之间联系太深

3.测量类的缺陷性,

    我们首先看一段StaticLayout中关于测量文本的generate方法代码

......      
  int paraEnd;
        for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
            paraEnd =<span style="color:#cc0000;"> TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);</span>
            if (paraEnd < 0)
                paraEnd = bufEnd;
            else
                paraEnd++;
....
<span style="color:#cc0000;">measured.setPara(source, paraStart, paraEnd, textDir, b);</span>
         ......
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd)
{
...
if (spanned == null) {
   spanEnd = paraEnd;
  int spanLen = spanEnd - spanStart;
  measured.addStyleRun(paint, spanLen, fm);
   } else {
  spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
     MetricAffectingSpan.class);
  int spanLen = spanEnd - spanStart;
    MetricAffectingSpan[] spans =
      spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
   spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
  <span style="color:#990000;">measured.addStyleRun(paint, spans, spanLen, fm);</span>
                }
....



看到每次对Text进行格式化时,都会首先寻找下一行,我们再也看一下TextUtils中的indexOf代码

public static int indexOf(CharSequence s, char ch, int start, int end) {
        Class<? extends CharSequence> c = s.getClass();

        if (s instanceof GetChars || c == StringBuffer.class ||
            c == StringBuilder.class || c == String.class) {
            final int INDEX_INCREMENT = 500;
            char[] temp = obtain(INDEX_INCREMENT);

            while (start < end) {
                int segend = start + INDEX_INCREMENT;
                if (segend > end)
                    segend = end;

                getChars(s, start, segend, temp, 0);

                int count = segend - start;
                for (int i = 0; i < count; i++) {
                    if (temp[i] == ch) {
                        recycle(temp);
                        return i + start;
                    }
                }

                start = segend;
            }

            recycle(temp);
            return -1;
        }

        for (int i = start; i < end; i++)
            if (s.charAt(i) == ch)
                return i;

        return -1;
    }



其中obtain()函数,对缓存给予的Text,进行数组缓存,也就是说,它是以500大小的char数组进行循环利用,也就是说当你的字符数为10000时,他需要进行循环20次以此类推.

乍一看的确没什么问题,代码很Android

再来看一下MeasureText中的setPara方法,

/**
     * Analyzes text for bidirectional runs.  Allocates working buffers.
     */
    void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir,
            StaticLayout.Builder builder) {
        mBuilder = builder;
        mText = text;
        mTextStart = start;

        int len = end - start;
        mLen = len;
        mPos = 0;

        if (mWidths == null || mWidths.length < len) {
<span style="color:#cc0000;">         <mWidths = ArrayUtils.newUnpaddedFloatArray(len);</span>
        }
        if (mChars == null || mChars.length < len) {
<span style="background-color: rgb(255, 255, 255);"><span style="color:#990000;">    mChars = ArrayUtils.newUnpaddedCharArray(len);</span></span>
        }
    <span style="color:#cc0000;">   TextUtils.getChars(text, start, end, mChars, 0);</span>

        if (text instanceof Spanned) {
            Spanned spanned = (Spanned) text;
            ReplacementSpan[] spans = spanned.getSpans(start, end,
                    ReplacementSpan.class);

            for (int i = 0; i < spans.length; i++) {
                int startInPara = spanned.getSpanStart(spans[i]) - start;
                int endInPara = spanned.getSpanEnd(spans[i]) - start;
                // The span interval may be larger and must be restricted to [start, end[
                if (startInPara < 0) startInPara = 0;
                if (endInPara > len) endInPara = len;
                for (int j = startInPara; j < endInPara; j++) {
                    mChars[j] = '\uFFFC'; // object replacement character
                }
            }
        }
    }

看到了吧,又一次进行了缓存数组,并且又同时缓存了字符宽度的数组,

通常当上述两个方法单独看待,没有出现缓存过剩,情况,但是,两个方法同时出现,再加上measured.addStyleRun方法会去获取字符宽度(越多越慢),就会出现很显著的原因,卡顿严重,GC不断,而当我们的文本并没有那么多的换行符,第二个方法又会导致内存飙升,从而最终导致了OOM,这就是为什么几百K,内存也会很大的原因所在.

既然知道了原因,那么怎么去优化呢?

有两种方案:

第一种>


在MeasureText中限制获取字符缓存宽度数组的长度,这样可以暂时性的解决掉测量类过量缓存的问题,但是相应的速度会有所下降,

将开始的TextUtils.indexof换行符的方法最大的检测长度(end-start),设置为你限定的量,

然后重写StaticLayout中的generate方法,删除最外部的for循环,修正第二层循环.

假设你设定的MeasureText中缓存宽度为100,(如果Text实现了Span接口,那么首先你去获取Span的结束点,检测一下起点与结束点的位置是否大于100),每次100字符长度,进行一次循环索引换行符与测量,

说的有些笼统,因为这里面有些东西没办法描述,因为TextView里面的设定很复杂,

这一种方法只是简单的解决了OOM的问题,并没有解决加载缓慢的问题,也就是说加载更慢(但是至少它不会出现几百K内存溢出的情况)

第二种>

使用ListView的缓存技术.

缓存当前屏幕的行数,依次不断.这一中是我着重实现的.

他同时包含了上述的优点之外,在速度上简直是秒速的存在,内存节约也是独树一帜.(我的手机2GRAM,2M的文本都不会溢出)

但是,它有了一个致命的缺点,下引速度缓慢,也就是说,如果未加载完全部的起点,你索引到最后字符,会很慢.

优点:
1>降低内耗,多行布局内存可降低1/1 - 1/3 之间,随文本大  - 小决定
2>单行布局,内存可降低 1/1 - 2/3
3>秒速增删改,秒速加载,与LinkedList优点重合
4>支持原始EMOJI(当然需要字体支持),如果存在外部EMOJI请重写TextLinear中drawBitmap方法
5>Cursor/LetterSpace/LineFormat/Ellipsis/Measure等实现高度自定义
缺点:
1.由于此TextAreaView为格式安全的,当未加载完Text全部起点时,下引速度缓慢,在1MB以内速度延迟&lt;=2秒(换行符将明显加速下引)

Github地址:github.com/Maizer/TextAreaView


Note:拿来研究一下还是可以的,因为在某些方面会有BUG