线上错误日志
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的代码,可以发现
/* 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时检测头部以及尾部是否含有\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中的样式统统复制一遍,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中的样式复制,这里已经是这个类中能追踪到最起始的位置,下面还要继续追踪的话需要通过查看调用栈,找到引用的地方
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中的文本不规范导致复制时报错。考虑到应用内使用时没有加入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;
}