阿拉伯字母与LTR书写语系混排奔溃问题

这周印度那边的同事在钉钉群里反馈说,有主播的APP会在某个用户进直播间发了一串神秘消息后奔溃,而且这个用户知道这个方法就一直捣乱,管理员将这些用户的号封了,他有注册新的账号继续搞,扰乱直播秩序。-_-||
顺便说一下,笔者目前在一家搞直播的公司,主要是海外业务如印度、东南亚、日本等国家以及中国台湾省,国内业务也有,但营收主要还是来自以上国家和地区。

看到群里反馈的消息后,我的第一反应是~~黑人脸,纳尼?还有这种事?操作方式不对吧?或者将手机朝向东方试试?心里这么想,但问题还是得解决。他们那边提供了主播ID和用户ID,我就上Fabric上去查了一下,发现这个主播在他们提供的时间节点附近是有奔溃,日志如下:

Fatal Exception: java.lang.IndexOutOfBoundsException: measureLimit (32) is out of start (36) and limit (32) bounds
       at android.text.TextLine.handleRun + 1113(TextLine.java:1113)
       at android.text.TextLine.drawRun + 509(TextLine.java:509)
       at android.text.TextLine.draw + 280(TextLine.java:280)
       at android.text.Layout.drawText + 581(Layout.java:581)
       at android.text.Layout.draw + 333(Layout.java:333)
       at android.widget.TextView.onDraw + 8108(TextView.java:8108)
       at android.view.View.draw + 21870(View.java:21870)
       at android.view.View.updateDisplayListIfDirty + 20743(View.java:20743)
       at android.view.View.draw + 21596(View.java:21596)
       at android.view.ViewGroup.drawChild + 4558(ViewGroup.java:4558)
       at android.view.ViewGroup.dispatchDraw + 4333(ViewGroup.java:4333)
       at android.view.View.draw + 21873(View.java:21873)
       at android.view.View.updateDisplayListIfDirty + 20743(View.java:20743)
       at android.view.View.draw + 21596(View.java:21596)
       at android.view.ViewGroup.drawChild + 4558(ViewGroup.java:4558)
       at android.view.ViewGroup.dispatchDraw + 4333(ViewGroup.java:4333)
       at android.view.View.updateDisplayListIfDirty + 20729(View.java:20729)
       at android.view.View.draw + 21596(View.java:21596)
       at android.view.ViewGroup.drawChild + 4558(ViewGroup.java:4558)
       at android.support.v7.widget.RecyclerView.drawChild + 4703(RecyclerView.java:4703)
       at android.view.ViewGroup.dispatchDraw + 4333(ViewGroup.java:4333)
       at android.view.View.draw + 21873(View.java:21873)
       at android.support.v7.widget.RecyclerView.draw + 4107(RecyclerView.java:4107)
       at android.view.View.updateDisplayListIfDirty + 20743(View.java:20743)
       at android.view.View.draw + 21596(View.java:21596)
       at android.view.ViewGroup.drawChild + 4558(ViewGroup.java:4558)
       at android.view.ViewGroup.dispatchDraw + 4333(ViewGroup.java:4333)
       at android.view.View.updateDisplayListIfDirty + 20729(View.java:20729)
       at android.view.View.draw + 21596(View.java:21596)
       at android.view.ViewGroup.drawChild + 4558(ViewGroup.java:4558)
       at android.view.ViewGroup.dispatchDraw + 4333(ViewGroup.java:4333)
       at android.view.View.updateDisplayListIfDirty + 20729(View.java:20729)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ViewGroup.recreateChildDisplayList + 4542(ViewGroup.java:4542)
       at android.view.ViewGroup.dispatchGetDisplayList + 4514(ViewGroup.java:4514)
       at android.view.View.updateDisplayListIfDirty + 20698(View.java:20698)
       at android.view.ThreadedRenderer.updateViewTreeDisplayList + 725(ThreadedRenderer.java:725)
       at android.view.ThreadedRenderer.updateRootDisplayList + 731(ThreadedRenderer.java:731)
       at android.view.ThreadedRenderer.draw + 840(ThreadedRenderer.java:840)
       at android.view.ViewRootImpl.draw + 3981(ViewRootImpl.java:3981)
       at android.view.ViewRootImpl.performDraw + 3755(ViewRootImpl.java:3755)
       at android.view.ViewRootImpl.performTraversals + 3064(ViewRootImpl.java:3064)
       at android.view.ViewRootImpl.doTraversal + 1927(ViewRootImpl.java:1927)
       at android.view.ViewRootImpl$TraversalRunnable.run + 8558(ViewRootImpl.java:8558)
       at android.view.Choreographer$CallbackRecord.run + 949(Choreographer.java:949)
       at android.view.Choreographer.doCallbacks + 761(Choreographer.java:761)
       at android.view.Choreographer.doFrame + 696(Choreographer.java:696)
       at android.view.Choreographer$FrameDisplayEventReceiver.run + 935(Choreographer.java:935)
       at android.os.Handler.handleCallback + 873(Handler.java:873)
       at android.os.Handler.dispatchMessage + 99(Handler.java:99)
       at android.os.Looper.loop + 214(Looper.java:214)
       at android.app.ActivityThread.main + 7094(ActivityThread.java:7094)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run + 494(RuntimeInit.java:494)
       at com.android.internal.os.ZygoteInit.main + 975(ZygoteInit.java:975)

TextView奔溃java.lang.IndexOutOfBoundsException: measureLimit is out of start and limit bounds

妈耶!奔溃还挺多!
emmmmm……这该如何是好,大家应该可以看出来,日志里全是系统API的调用栈,根本就看不出来是应用程序的那里报了错,这就很难查了。我也顺着报错的系统源代码去查,也看不出来那里出了问题,TextView相关的系统代码太长,分析起来费时费力。
与此同时,我也让server端查了一下直播间日志,看看这个用户到底发了什么玩意,我们也试试能不能复现。一段时间之后,数据出来了,不出所料,这个用户用他的几个账号,不断得在直播间发一串“神秘”的代码:

_ヽ   \\ Λ_Λ    \( 'ㅅ' )     > ⌒ヽ    /   へ\    /  / \\    レ ノ   ヽ_つ   / /   / /|  ( (ヽ  _ヽ   \\ Λ_Λ    \( 'ㅅ' )     > ⌒ヽ    /   へ\    /  / \\    レ ノ   ヽ_つ   / /   / /|  ( (ヽ  ⊂_ヽ   \\ Λ_Λ    \( 'ㅅ' )     > ⌒ヽ    /   へ\    /  / \\    レ ノ   ヽ_つ   / /   / /|  ( (ヽ  ⊂_ヽ   \\ Λ_Λ    \( 'ㅅ' )     > ⌒ヽ    /   へ\    /  / \\    レ ノ   ヽ_つ   / /   / /|  ( (ヽ  ⊂_ヽ   \\ Λ_Λ    \( 'ㅅ' )     > ⌒ヽ    /   へ\    /

我们欣喜若狂,以为这就能复现问题了。但让人没想到的是,将这段神秘的字符串发送到直播间后,并没有出现任何异常。
我一边绝望着,一边把这个用户注册的所有子账号都关注了,看能不能找到什么共同点:

等等!我好想发现了什么,昵称你面几乎都包含一个字符:ء 这是什么鬼?管它呢,我也把昵称改成这个试试。如我所愿,改成这个字符之后,进直播间发那一串什么代码,程序果然奔溃了。-_-||

在我进行昵称编辑的时候,发现这个字符是显示在输入框最右侧的,而且再输入内容都会显示到它的左边。原来是阿拉伯字母,Right-To-Left,真相终于水落石出了。
查看APP代码后发现,聊天消息和昵称以及其他一些徽章图片等都是在同一个textview里面,通过spannable来进行排版的。阿拉伯字母的出现打乱了spannable顺序,引起了这次事故。(正常在TextView显示阿拉伯字母和那串神秘字符串,是不会出问题的,除非用了一些spannable)。

找到原因了就解决吧,我的思路是强制将阿拉伯字母也以LTR(left to right)的方式排版(或者强制以本地语言的书写方式进行排版),因为我们目前没有做中东版本,没有未阿拉伯语系的用户提供服务,所以这样还是可以接受的。(这样做是有风险的,操作需谨慎)

<stylename="ForceLocalDirection">
    <itemname="android:textDirection">locale</item>
    <itemname="android:textAlignment">gravity</item>
</style>

附:
阿拉伯字母表 

字符wiki