TextView在android4.1和4.1.1上报ArrayIndexOutOfBoundsException的分析

很久以前做的表情输入及显示,用的系统的SpannableString,以前都好端端的没问题,最近突然报出个棘手的bug,在4.1和4.1.1的手机上显示某位用户的评论时,程序直接挂掉,

异常信息:

9-17 16:38:27.429: E/AndroidRuntime(10425): FATAL EXCEPTION: main
09-17 16:38:27.429: E/AndroidRuntime(10425): java.lang.ArrayIndexOutOfBoundsException: length=116; index=125
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.MeasuredText.addStyleRun(MeasuredText.java:168)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.MeasuredText.addStyleRun(MeasuredText.java:204)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.StaticLayout.generate(StaticLayout.java:297)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.StaticLayout.<init>(StaticLayout.java:156)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.StaticLayout.<init>(StaticLayout.java:96)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.text.StaticLayout.<init>(StaticLayout.java:75)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.TextView.makeSingleLayout(TextView.java:5942)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.TextView.makeNewLayout(TextView.java:5782)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.TextView.onMeasure(TextView.java:6139)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.measure(View.java:15264)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4918)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1390)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.measure(View.java:15264)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4918)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.measure(View.java:15264)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.ListView.setupChild(ListView.java:1893)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.ListView.makeAndAddView(ListView.java:1803)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.ListView.fillDown(ListView.java:681)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.ListView.fillFromTop(ListView.java:742)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.ListView.layoutChildren(ListView.java:1629)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.AbsListView.onLayout(AbsListView.java:2224)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1507)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1420)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1507)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1420)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1507)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1420)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at com.component.SinglePreviewContainer.onLayout(SinglePreviewContainer.java:124)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1507)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1420)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1638)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.onLayout(LinearLayout.java:1422)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.RelativeLayout.onLayout(RelativeLayout.java:948)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.View.layout(View.java:13846)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.view.ViewGroup.layout(ViewGroup.java:4466)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1649)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1507)
09-17 16:38:27.429: E/AndroidRuntime(10425): 	at android.widget.Linear

  原因:当TextView显示的表情恰好要被换行符截断的时候,会报异常(目前只在4.1和4.1.1上出现)

  

 解决方式:

重写TextView 在onMeasure()中捕获异常

public class PatchedTextView extends TextView {
	public PatchedTextView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public PatchedTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public PatchedTextView(Context context) {
		super(context);
	}
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		try{
			super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		}catch (ArrayIndexOutOfBoundsException e){
			setText(getText().toString());
			super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
		}
	}	
	@Override
	public void setGravity(int gravity){
		try{
			super.setGravity(gravity);
		}catch (ArrayIndexOutOfBoundsException e){
			setText(getText().toString());
			super.setGravity(gravity); 
		}
	}
	@Override
	public void setText(CharSequence text, BufferType type) {
		try{
			super.setText(text, type);
		}catch (ArrayIndexOutOfBoundsException e){
			setText(text.toString());
		}
	}
}

  引用牛人分析:

Here's some more details on the bug and how to work around it.  I've attempted accuracy but please call out any mistakes.

Crash Manifestation:
When Android is rendering long lines of text to the screen, it needs to figure out where to perform a line-wrap.  A crash will occur if all of the following are true:
1.  The text contains spans that are MetricAffectingSpans (ex: StyleSpan is one such subclass).  Other types of spans do not invoke the crash (ex: URLSpan).
2.  There is a word where a MetricAffectingSpans starts or stops in the middle of that word (ex: the first half of a word is bolded while the second half is not).
3.  A line-wrap is needed in the non-spanned part of the word.
4.  The code is running on Android 4.1 or 4.1.1.

What makes this bug show up on some devices and not others depends on several factors such as screen pixel dimensions, dpi, font size, the text and spans themselves, and others.

This shows up both in custom layouts and in simple dialogs (ex: dialog's built by AlertDialogBuilder).

So, what exactly is meant by a "word" in this case?  A word is a sequence of characters that can not be split across a line-wrap.  So then, where can line-wraps occurs?  It has already been pointed out that spaces allow line-wraps to occur.  However, there are other characters that also allow line-wraps.
1.  Spaces, Tabs, and Newline characters always allow line-wraps (i.e. ' ', '\t', and '\n').
2.  The characters '.' ',' ';' and ':' allow line-wraps IF there is not a digit immedidately before or after them  (as defined by Character.isDigit()).  When these 4 characters separate two words, these characters are considered part of the first of the two words.
3.  The characters '/' and '-' allow line-wraps IF there is not a digit immediately after them (as defined by Character.isDigit()).  As above, when these 2 characters separate two words, these characters are considered part of the first of the two words.
4.  Ideographs allow line-wraps if they are adjacent, except for non-starters while only wrap after the non-starter.  See Android's 4.1.1 StaticLayout.java file for details.

Here are some example strings that were put through textView.setText( Html.fromHtml(TEXT) ):
"hello <b>world</b>"         Safe
"hello.<b>world</b>"         Safe
"hello.<b>1orld</b>"         May crash
"hell7.<b>world</b>"         May crash
"hello-<b>world</b>"         Safe
"hello-<b>1orld</b>"         May crash
"<b>hello</b> world"         Safe
"<b>hello.</b>world"         Safe
"<b>hello</b>.world"         May crash
"<b>hell7.</b>world"         May crash
"<b>hello.</b>1orld"         May crash
"<b>hello-</b>world"         Safe
"<b>hello</b>-world"         May crash
"<b>hell7-</b>world"         Safe
"<b>hello-</b>1orld"         May crash

The crashes are limited to any span that is or derived from MetricAffectingSpans.  This means that the following spans are SAFE to use because they are not derived from MetricAffectingSpans:
MaskFilterSpan, RasterizerSpan, clickableSpan, URLSpan, BackgroundColorSpan, ForegroundColorSpan, StrikethroughSpan, SuggestionSpan, UnderlineSpan

There are two ways for detecting if the crash can even occur:
1.  Look for Build.VERSION to be equal to "4.1" or "4.1.1".  This check is easy, but your code might implement a work around on such device's where the crash wouldn't show up (ex: screen size meant that line-wraps didn't occur in any of the bad places).
2.  Wrap the call to setText() for the View with a try{ ... } catch( IndexOutOfBoundsException e ){ ... }.  Do this either around the call to setText or create a subclass that overrides setText() and calls super.setText().

Work-arounds if detected (from simplist to complex):
0.  If you have complete control over the text, re-write the text to fit the "characters that allow line-wraps" rules listed above.
1.  Remove all the spans from the text (ex: text.toString()).  This is easy but it removes ALL spans.
2.  Remove just the MetricAffectingSpans.  Call text.getSpans(0, text.length(), MetricAffectingSpans.class) and then text.removeSpan() for each one returned.
3.  Find all the MetricAffectingSpans and detect whether each span has a space/tab/newline before and after them.  If not, then insert a space/tab/newline before and/or after each span.
4.  Find all the MetricAffectingSpans and do the following:
        If the span is a StyleSpan for bold, replace the span with a MaskFilterSpan( 
            new BlurMaskFilter((float) 0.5, BlurMaskFilter.Blur.SOLID ) )
        If the span is a StyleSpan for italic, make the so-so replacement with ForegroundColorSpan(0xFF808080)
        If the span is something else, remove the span entirely.
5.  Find all the MetricAffectingSpans and detect whether each span allows a line-wrap immediately before and after them according to the "characters that allow line-wraps" rules listed above.  Modify the string accordingly.
6.  Other work-arounds are possible.

   

链接 https://code.google.com/p/android/issues/detail?id=35466

猜你喜欢

转载自hold-on.iteye.com/blog/1943437