便簽SpannableString崩潰問(wèn)題分析

便簽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è)疑惑霞掺,

  1. SpannableString初始化中為什么調(diào)用onSelectionChange

  2. 在調(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);
        }
        ...
    }

參考文檔:

Spannable 源碼分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翔试,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子垦缅,更是在濱河造成了極大的恐慌驹碍,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幸冻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡庞溜,警方通過(guò)查閱死者的電腦和手機(jī)碑定,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)漫试,“玉大人,你說(shuō)我怎么就攤上這事驾荣∑张荩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵歧匈,是天一觀的道長(zhǎng)砰嘁。 經(jīng)常有香客問(wèn)我,道長(zhǎng)矮湘,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任宫静,我火速辦了婚禮券时,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘橘洞。我一直安慰自己,他們只是感情好炸枣,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著适肠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侯养。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,785評(píng)論 1 314
  • 那天柠傍,我揣著相機(jī)與錄音,去河邊找鬼惧笛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛患整,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播各谚,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嘲碧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了愈涩?” 一聲冷哼從身側(cè)響起加矛,我...
    開(kāi)封第一講書(shū)人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斟览,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸠窗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年稍计,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臣嚣。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硅则,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怎虫,到底是詐尸還是另有隱情,我是刑警寧澤揪垄,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站逻翁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏八回。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一溶浴、第九天 我趴在偏房一處隱蔽的房頂上張望管引。 院中可真熱鬧,春花似錦褥伴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)隅熙。三九已至稽煤,卻和暖如春酵熙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绿店。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工庐橙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人态鳖。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓恶导,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親惨寿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361