線上錯誤日志
2018-11-01 11:18:29.519 21987-21987/xxx.xxx.xx E/MtaSDK.CaughtExp: java.lang.RuntimeException: PARAGRAPH span must end at paragraph boundary (62 follows )
at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:171)
at android.text.SpannableStringInternal.copySpans(SpannableStringInternal.java:68)
at android.text.SpannableStringInternal.<init>(SpannableStringInternal.java:43)
at android.text.SpannedString.<init>(SpannedString.java:30)
at android.text.method.ReplacementTransformationMethod$SpannedReplacementCharSequence.subSequence(ReplacementTransformationMethod.java:180)
at android.widget.TextView.getTransformedText(TextView.java:9529)
at android.widget.TextView.onTextContextMenuItem(TextView.java:9484)
at android.widget.Editor$TextActionModeCallback.onActionItemClicked(Editor.java:4031)
at com.android.internal.policy.DecorView$ActionModeCallback2Wrapper.onActionItemClicked(DecorView.java:2393)
at com.android.internal.view.FloatingActionMode$3.onMenuItemSelected(FloatingActionMode.java:88)
at com.android.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:761)
at com.android.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:152)
at com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:904)
at com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:894)
at com.android.internal.view.FloatingActionMode$4.onMenuItemClick(FloatingActionMode.java:114)
at com.android.internal.widget.FloatingToolbar$FloatingToolbarPopup$3.onClick(FloatingToolbar.java:398)
at android.view.View.performClick(View.java:5642)
at android.view.View$PerformClick.run(View.java:22485)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6211)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:793)
復現場景的話是復制類似情況下的文字然后粘貼進應用內的編輯框蚊锹,然后再次進行復制,報錯。但是這是后期根據分析才找出來的復現路徑攻礼,那么在沒有復現場景的時候怎么分析呢
一層一層尋找報錯源頭胖齐,這次先從報錯的方法入手玻淑,查找SpannableStringInternal中有關PARAGRAPH的代碼,可以發(fā)現
/* package */ void setSpan(Object what, int start, int end, int flags) {
int nstart = start;
int nend = end;
checkRange("setSpan", start, end);
if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
if (start != 0 && start != length()) {
char c = charAt(start - 1);
if (c != '\n')
throw new RuntimeException(
"PARAGRAPH span must start at paragraph boundary" +
" (" + start + " follows " + c + ")");
}
if (end != 0 && end != length()) {
char c = charAt(end - 1);
if (c != '\n')
throw new RuntimeException(
"PARAGRAPH span must end at paragraph boundary" +
" (" + end + " follows " + c + ")");
}
}
...
從邏輯來看是當標簽為Spannable.SPAN_PARAGRAPH時檢測頭部以及尾部是否含有\(zhòng)n呀伙,如果沒有的話會報錯
再看下他在哪調用的
/**
* Copies another {@link Spanned} object's spans between [start, end] into this object.
*
* @param src Source object to copy from.
* @param start Start index in the source object.
* @param end End index in the source object.
*/
private final void copySpans(Spanned src, int start, int end) {
Object[] spans = src.getSpans(start, end, Object.class);
for (int i = 0; i < spans.length; i++) {
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;
setSpan(spans[i], st - start, en - start, fl);
}
}
從注釋來看补履,是將src中的樣式統(tǒng)統(tǒng)復制一遍,f1就是樣式標簽剿另,其中就包括上面需要檢測的Spannable.SPAN_PARAGRAPH箫锤,再次向上查看調用
/* package */ SpannableStringInternal(CharSequence source,
int start, int end) {
if (start == 0 && end == source.length())
mText = source.toString();
else
mText = source.toString().substring(start, end);
mSpans = EmptyArray.OBJECT;
mSpanData = EmptyArray.INT;
if (source instanceof Spanned) {
if (source instanceof SpannableStringInternal) {
copySpans((SpannableStringInternal) source, start, end);
} else {
copySpans((Spanned) source, start, end);
}
}
}
在實例化的時候將傳入的source中的樣式復制,這里已經是這個類中能追蹤到最起始的位置雨女,下面還要繼續(xù)追蹤的話需要通過查看調用棧谚攒,找到引用的地方
ReplacementTransformationMethod.java
private static class SpannedReplacementCharSequence
extends ReplacementCharSequence
implements Spanned
{
public SpannedReplacementCharSequence(Spanned source, char[] original,
char[] replacement) {
super(source, original, replacement);
mSpanned = source;
}
public CharSequence subSequence(int start, int end) {
return new SpannedString(this).subSequence(start, end);
}
public <T> T[] getSpans(int start, int end, Class<T> type) {
return mSpanned.getSpans(start, end, type);
}
public int getSpanStart(Object tag) {
return mSpanned.getSpanStart(tag);
}
public int getSpanEnd(Object tag) {
return mSpanned.getSpanEnd(tag);
}
public int getSpanFlags(Object tag) {
return mSpanned.getSpanFlags(tag);
}
public int nextSpanTransition(int start, int end, Class type) {
return mSpanned.nextSpanTransition(start, end, type);
}
private Spanned mSpanned;
}
subSequence,很熟悉的方法氛堕,將 CharSequence在指定部分切割
public boolean onTextContextMenuItem(int id) {
int min = 0;
int max = mText.length();
if (isFocused()) {
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
min = Math.max(0, Math.min(selStart, selEnd));
max = Math.max(0, Math.max(selStart, selEnd));
}
switch (id) {
case ID_SELECT_ALL:
selectAllText();
return true;
case ID_UNDO:
if (mEditor != null) {
mEditor.undo();
}
return true; // Returns true even if nothing was undone.
case ID_REDO:
if (mEditor != null) {
mEditor.redo();
}
return true; // Returns true even if nothing was undone.
case ID_PASTE:
paste(min, max, true /* withFormatting */);
return true;
case ID_PASTE_AS_PLAIN_TEXT:
paste(min, max, false /* withFormatting */);
return true;
case ID_CUT:
setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
deleteText_internal(min, max);
return true;
case ID_COPY:
setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
stopTextActionMode();
return true;
case ID_REPLACE:
if (mEditor != null) {
mEditor.replace();
}
return true;
case ID_SHARE:
shareSelectedText();
return true;
}
return false;
}
CharSequence getTransformedText(int start, int end) {
return removeSuggestionSpans(mTransformed.subSequence(start, end));
}
到這里流程就很明了了馏臭,在復制時將TextView中選中的部分進行帶樣式的復制,那么肯定是TextView中的文本不規(guī)范導致復制時報錯岔擂∥晃梗考慮到應用內使用時沒有加入Spannable.SPAN_PARAGRAPH這樣的標簽,只有可能是通過復制外部文本粘貼進EditText中導致問題乱灵,所以解決方法也很簡單塑崖,提前攔截復制剪切的處理邏輯,改成純文本復制痛倚。
@Override
public boolean onTextContextMenuItem(int id) {
try {
return super.onTextContextMenuItem(id);
} catch (RuntimeException e) {
if (getText() == null){
return false;
}
int min = 0;
int max = getText().length();
if (id == android.R.id.cut) {
if (isFocused()) {
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
min = Math.max(0, Math.min(selStart, selEnd));
max = Math.max(0, Math.max(selStart, selEnd));
}
ClipData cutData = ClipData.newPlainText(null, getText().toString().subSequence(min, max));
if (setPrimaryClip(cutData)) {
getText().delete(min, max);
}
return true;
} else if (id == android.R.id.copy) {
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
min = Math.max(0, Math.min(selStart, selEnd));
max = Math.max(0, Math.max(selStart, selEnd));
ClipData copyData = ClipData.newPlainText(null, getText().toString().subSequence(min, max));
if (setPrimaryClip(copyData)) {
//通過setText隱藏FloatingMenu
setText(getText());
}
return true;
}
}
return false;
}
private boolean setPrimaryClip(ClipData clip) {
ClipboardManager clipboard =
(ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard == null){
return false;
}
try {
clipboard.setPrimaryClip(clip);
} catch (Throwable t) {
return false;
}
return true;
}