便簽SpannableString崩潰問(wèn)題分析
1. 問(wèn)題描述
自研的便簽在加入多樣式文本后出現(xiàn)偶現(xiàn)崩潰問(wèn)題匿刮,代碼提示如下我磁,可以看到是在SpannableString對(duì)象初始化出現(xiàn)的越界錯(cuò)誤鱼辙,出錯(cuò)的代碼很快就定位到舵揭,但是發(fā)現(xiàn)并不好分析防楷,原因在于
(1)出錯(cuò)的代碼在源碼中,代碼中只是對(duì)象初始化堡牡,按道理來(lái)說(shuō)不會(huì)出現(xiàn)問(wèn)題(后面證實(shí)該想法存在問(wèn)題,初始化過(guò)程中會(huì)觸發(fā)其他的代碼)
(2)由于該問(wèn)題是偶現(xiàn)問(wèn)題晤柄,通過(guò)moneky測(cè)試發(fā)現(xiàn)的,沒(méi)有辦法找到必現(xiàn)路徑
(3)在網(wǎng)上沒(méi)有搜索出同樣的問(wèn)題芥颈,其實(shí)從這里就可以推斷是自己代碼中的問(wèn)題
java.lang.IndexOutOfBoundsException: setSpan (0 ... -1) has end before start
at android.text.SpannableStringInternal.checkRange(SpannableStringInternal.java:428)
at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:163)
at android.text.SpannableStringInternal.copySpans(SpannableStringInternal.java:68)
at android.text.SpannableStringInternal.<init>(SpannableStringInternal.java:43)
at android.text.SpannableString.<init>(SpannableString.java:30)
2.問(wèn)題分析
(1)嘗試復(fù)現(xiàn)問(wèn)題
該功能是在加入富文本樣式中后出現(xiàn)的,所以推斷是跟樣式存在聯(lián)系爬坑,而且代碼出錯(cuò)的觸發(fā)時(shí)機(jī)是在打開(kāi)頁(yè)面或者頁(yè)面重復(fù)滾動(dòng)。測(cè)試時(shí)通過(guò)多次輸入樣式文本打開(kāi)和滾動(dòng)頁(yè)面妇垢,最后發(fā)現(xiàn)當(dāng)輸入樣式文本第一個(gè)字體為帶樣式文本時(shí)巾遭,上下滾動(dòng)頁(yè)面后會(huì)出現(xiàn)崩潰闯估。
(2)源碼分析
找到必現(xiàn)路徑后灼舍,跟蹤崩潰的源碼,發(fā)現(xiàn)在SpannableStringInternal中會(huì)調(diào)用CopySpans方法涨薪,當(dāng)調(diào)用到i=6時(shí)骑素,發(fā)現(xiàn)st和en獲取都為-1,設(shè)置setSpan的參數(shù)時(shí)就會(huì)出現(xiàn)開(kāi)頭所說(shuō)的越界錯(cuò)誤刚夺。
private void copySpans(Spanned src, int start, int end, boolean ignoreNoCopySpan) {
Object[] spans = src.getSpans(start, end, Object.class);
for(int i = 0; i < spans.length; ++i) {
if (!ignoreNoCopySpan || !(spans[i] instanceof NoCopySpan)) {
int st = src.getSpanStart(spans[i]);
int en = src.getSpanEnd(spans[i]);
int fl = src.getSpanFlags(spans[i]);
if (st < start) {
st = start;
}
if (en > end) {
en = end;
}
this.setSpan(spans[i], st - start, en - start, fl, false);
}
}
}
查看getSpanStart方法代碼(SpannableStringBuilder類中)献丑,getSpanStart值與mIndexOfSpan值有關(guān),mIndexOfSpan是IdentityHashMap類型對(duì)象侠姑,IdentityHashMap是以對(duì)象作為key值保存创橄,具體使用可參考官方文檔,這里我們可以當(dāng)hashMap進(jìn)行理解莽红。
查看以下代碼妥畏,我們發(fā)現(xiàn)getSpanStart返回-1的情況只有兩種:
- mIndexOfSpan為空,由于i=6時(shí)才會(huì)觸發(fā)該條件安吁,說(shuō)明mIndexOfSpan不可能為空
- mIndextOfSpan在獲取i為spans[6]時(shí)對(duì)應(yīng)的object為null醉蚁,所以這里推斷在SpannableString初始化時(shí),在代碼的其他地方設(shè)置了該object的值為null
private IdentityHashMap<Object, Integer> mIndexOfSpan;
public int getSpanStart(Object what) {
if (this.mIndexOfSpan == null) {
return -1;
} else {
Integer i = (Integer)this.mIndexOfSpan.get(what);
return i == null ? -1 : this.resolveGap(this.mSpanStarts[i]);
}
}
在SpannableStringBuilder類中查看mIndexOfSpan的引用鬼店,發(fā)現(xiàn)在removeSpan方法中會(huì)移處指定的標(biāo)記對(duì)象网棍。
public void removeSpan(Object what, int flags) {
if (mIndexOfSpan == null) return;
Integer i = mIndexOfSpan.remove(what);
if (i != null) {
removeSpan(i.intValue(), flags);
}
}
在removeSpan方法中設(shè)置斷點(diǎn),調(diào)試跟蹤代碼妇智,調(diào)用棧如下滥玷。在SpannableString初始化時(shí)會(huì)調(diào)用onSelectionChange方法中捌锭,在onSelectionChanged方法中會(huì)判斷selStart和selEnd參數(shù)進(jìn)行span的設(shè)置,當(dāng)滿足設(shè)定條件時(shí)罗捎,會(huì)調(diào)用removeSpan方法移除設(shè)置的富文本樣式(該方法為自己調(diào)用代碼观谦,不屬于源碼)。
問(wèn)題的原因就很簡(jiǎn)單桨菜,在SpannableString調(diào)用了removeSpan方法豁状,移除相應(yīng)的對(duì)象引用,導(dǎo)致在copySpans中調(diào)用時(shí)倒得,遍歷mIndexOfSpan時(shí)獲取該值為空泻红,導(dǎo)致越界錯(cuò)誤。
但這里存在兩個(gè)疑惑霞掺,
在SpannableString初始化中為什么調(diào)用onSelectionChange
在調(diào)用CopySpans方法時(shí)谊路,首先會(huì)遍歷源對(duì)象的span對(duì)象,怎么確保出錯(cuò)的span不在onSelectionChange方法前調(diào)用菩彬,也就是onSelectionChange調(diào)用的時(shí)機(jī)
這里我們先分析第一個(gè)疑惑:從以上調(diào)用棧缠劝,可以獲取函數(shù)調(diào)用為
SpannableStringInterna.copy --> SpanSpannableStringInternal.setSpan --> SpannableStringInternal.sendSpanAdded --> ChangeWatcher.onSpanChanged --> TextView.spanChange
在設(shè)置span時(shí),會(huì)觸發(fā)onSpanAdd方法骗灶,最后會(huì)調(diào)用到TextView的onSelectionChange方法惨恭,這里的關(guān)鍵在于Span數(shù)組中包含了TextView的ChangeWatcher對(duì)象脱羡。
SpannableStringInternal類的SetSpan方法
private void setSpan(Object what, int start, int end, int flags, boolean enforceParagraph) {
this.checkRange("setSpan", start, end);
...
if (this instanceof Spannable) {
this.sendSpanAdded(what, start, end);
}
}
SpannableStringInternal類的sendSpanAdded方法
private void sendSpanAdded(Object what, int start, int end) {
SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
int n = recip.length;
for (int i = 0; i < n; i++) {
recip[i].onSpanAdded((Spannable) this, what, start, end);
}
}
ChangeWatcher的onSpanChanged
public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
TextView.this.spanChange(buf, what, s, st, e, en);
}
TextView的spanChange方法
void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
if (selChanged) {
...
if ((buf.getSpanFlags(what) & Spanned.SPAN_INTERMEDIATE) == 0) {
...
onSelectionChanged(newSelStart, newSelEnd);
}
}
}
那么這個(gè)changeWather是在何時(shí)增加到Span數(shù)組中呢锉罐,查看TextView的源碼绕娘,在setText中可以看到,在設(shè)置文本時(shí)會(huì)自動(dòng)添加TextWatcher业舍。
出錯(cuò)的span為我們自己設(shè)置的StyleSpan(富文本樣式),是在setText之后設(shè)置的span,所以也就確保出錯(cuò)的span是在onSelectionChange方法之后噩茄,為了驗(yàn)證這個(gè)問(wèn)題,斷點(diǎn)查看了span的順序绩聘,如下所示耗啦,如猜想所示帜讲,這里也就解決了第二個(gè)疑惑。
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
if (text instanceof Spannable && !mAllowTransformationLengthChange) {
if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
| (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
}
}
3.解決方案
最后整理下總過(guò)程似将,在進(jìn)行new SpannableString時(shí)會(huì)時(shí)會(huì)調(diào)用CopySpan方法在验,會(huì)遍歷對(duì)象的span堵未,當(dāng)遍歷到Span為ChangeWatcher時(shí),會(huì)觸發(fā)onSelectionChange函數(shù)渗蟹,會(huì)主動(dòng)調(diào)用removeSpan方法移除我們?cè)O(shè)定的樣式span,同時(shí)移除mIndexOfSpan中對(duì)應(yīng)的對(duì)象刨沦,導(dǎo)致在copySpan獲取下次StyleSpan對(duì)象為空膘怕,出現(xiàn)崩潰問(wèn)題。
可以看到岛心,問(wèn)題的關(guān)鍵在于onSelectionChange方法的調(diào)用,在問(wèn)題驗(yàn)證過(guò)程中徘禁,發(fā)現(xiàn)將SpannableString修改為SpanStringBuilder時(shí)不會(huì)出現(xiàn)崩潰問(wèn)題。
查看源碼發(fā)現(xiàn)SpannableStringBuilder在setSpan時(shí)會(huì)設(shè)置標(biāo)志位send為false送朱,在SpannableStringBuilder的setSpan方法中會(huì)根據(jù)該值觸發(fā)sendSpanAdded方法干旁,所以也就不會(huì)觸發(fā)onSelectionChange方法,最終的解決方案也很簡(jiǎn)單回怜,將原來(lái)調(diào)用的修改為SpannableStringBuilder即可。
public SpannableStringBuilder(CharSequence text, int start, int end) {
if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
Object[] spans = sp.getSpans(start, end, Object.class);
for (int i = 0; i < spans.length; i++) {
...
setSpan(false, spans[i], st, en, fl, false/*enforceParagraph*/);
...
}
}
}
private void setSpan(boolean send, Object what, int start, int end, int flags,
boolean enforceParagraph) {
checkRange("setSpan", start, end);
...
if (send) {
restoreInvariants();
sendSpanAdded(what, nstart, nend);
}
...
}
參考文檔: