每次我用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以内速度延迟<=2秒(换行符将明显加速下引)
Github地址:github.com/Maizer/TextAreaView
Note:拿来研究一下还是可以的,因为在某些方面会有BUG