来晚了,不过我是带着干货来的。
本回答将为大家解释真实的原因,我认为是比大家的各种猜测要更真实一些的。
0x00 先上结论
0.1 真实原因:
- QQ 使用 \u0014 加 1 个字符(该字符为表情序号)表示表情
- QQ 使用 \u0011 加 3 个字符表示脏话
- 当 \u0014 与 \u0011 再与别的 3 个字符组合到一起,就出现了问题,虽然输入的是表情,却被处理为脏话
- 而 \u0014\u0011 表示的表情为第 0x11 个,即第 17 个表情
而第 17 个表情,它就是天选之表情,脏话 bug 幕后元凶,知乎话题缔造者,程序猿背锅原因!
菜刀!!!
表情序号部分相关源码
com.tencent.mobileqq.text.EmotcationConstants
static{
...
String[] v0 = new String[203];
v0[0] = "/呲牙";
v0[1] = "/调皮";
v0[v9] = "/流汗";
v0[v11] = "/偷笑";
v0[4] = "/再见";
v0[5] = "/敲打";
v0[6] = "/擦汗";
v0[7] = "/猪头";
v0[8] = "/玫瑰";
v0[9] = "/流泪";
v0[10] = "/大哭";
v0[11] = "/嘘...";
v0[12] = "/酷";
v0[13] = "/抓狂";
v0[14] = "/委屈";
v0[15] = "/便便";
v0[16] = "/炸弹";
v0[17] = "/菜刀";
...
}
0.2 验证方式
截图中可以看到转义过的“菊花残”比正常的“菊花残”前面好像多了一点空白。
多出的空白就是无法显示的 \u0014
将 QQ 消息复制出来,粘帖到 idea 的双引号中,就会自动转义为 \u 形式
public void test() {
//菜刀表情
String a = "\u0014\u0011";
//菜刀表情+冒号+微笑表情
String b = "\u0014\u0011:\u0014\u0017";
//发出去之后变成的 菊花残
String c = "\u0014菊花残";
//可以看到 \u0011:\u0014\u0017 被还原为了 菊花残
}
0.3 脏话的处理逻辑是什么
为什么点击发送的时候,要将处理过的脏话还原呢?
经过推测,应该是这样的逻辑。
1. 在输入时,监听 afterTextChanged 将输入的脏话转义
2. 在发送时,将已转义过的脏话还原,发送给服务器
(可能为了保证不同版本发送消息的准确,比如转义只存在于 Android ,为了低版本或别的版本正常显示,只能发送期输入的内容)
3. 在收到消息时,再将收到脏话转义
每一个步骤都受变量或方法控制,为 true 才执行相关步骤。
虽然步骤 1 和 3 没有执行,但是步骤 2 却执行了。
按理说没有执行 1 的转义,执行 2 的还原也没有影响,但是却遇到了菜刀的 \u0011,触发了 bug。
前面说的好像很有道理,为什么要说是“推测”呢,因为控制 1 和 3 变量,在代码中的确是 true,但实际却没有执行。
一定是我阅读代码的方式不对,留待以后 QQ 更新版本验证或等大神指点。
0x01 分析过程
1.1 好奇心
知乎上给推送了相关问题,很感兴趣,然后有网友反编译了代码,但是只能说明在代码里存在脏话,并不能解释为什么?
尤其是为什么 菜刀+字符 就会显示,会什么别的表情不会?
有网友推测是写反了,如果是这样,为什么会过得了测试和代码 review?
为什么要把脏话替换为 菜刀加其他?
这显然是不能满足我的好奇心。
后文即整个分析过程,仅用于学习交流,侵删。
简单分析,有不对的地方欢迎指正。
原文地址[2538]QQ 发送菜刀加符号导致脏话的真实原因分析
1.2 LoveLanguageManager 中与脏话相关的方法
根据之前网友的反编译的结果,我们找到 classes2.dex 中的包 com.tencent.mobileqq.lovelanguage
经过一番查看,我们在 LoveLanguageManager 中找到两个可疑方法,分别用于将 EditText 中的脏话进行替换与还原
(后文中的代码