上一篇 - Macaca 获取 Android 应用的性能

在测试 Android 设备时,经常会遇到输入中文的场景,切换键盘等操作繁琐易出问题 issue

说说编码

说到中文,最常见的字符集就是 GB2312,为了兼容一些繁体字就需要扩展版的 GBK,Big5(大五)。为了容纳全世界所有语言文字的编码,Unicode 协会开发了 Unicode 项目,总所周知 Unicode 是兼容 ASCII 的,而且市面上的操作系统 (桌面设备、手持便携设备) 都是支持 Unicode 字符集的,这里当然包括 Android。

Android 键盘允许输入的字符在 ASCII 集范围内,所以只要找到可以转换的字符和转换方式就能够,将字符最终表达为 ASCII 传给操作系统就可以实现中文输入,前提是不要产生乱码。

public static void main(String[] agrs) {
  System.out.println("-------- result --------");
  SortedMap availableCharsets = Charset.availableCharsets();
  System.out.println(availableCharsets);
  Charset defaultCharset = Charset.defaultCharset();
  System.out.println(defaultCharset);

  Charset UTF8 = Charset.forName("UTF-8");
  Charset ASCII  = Charset.forName("US-ASCII");

  if (Charset.isSupported("UTF-7")) {
    System.out.println("UTF-7 is supported.");
    Charset UTF7 = Charset.forName("UTF-7");
    String text = "中";
    byte[] encoded = text.getBytes(UTF8);
    System.out.println(encoded.length);

    System.out.println(encoded[0]);
    System.out.println(encoded[1]);
    System.out.println(encoded[2]);

    String str1 = new String(encoded, ASCII);
    System.out.println(str1);
  } else {
    System.out.println("UTF-7 is not supported.");
  }
  System.out.println("\n------------------------");
}

有兴趣可以自己试一下,查看下当前环境支持的编码,默认编码等,下载此仓库 java-charset-sample

UTF 编码

UTF-8 和 UTF-16 等 UTF 标准定义了 Unicode 数值和字符的映射关系

ASCII 码一共规定了 128 个字符的编码,如大写的字母 A 是 65(二进制 01000001),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0。所以我们寻找到 7 位的字符表达就可以保证无乱码的方式与 ASCII 进行转换。

UTF-7

与其他 UTF 系列编码不同,满足我们的需要,UTF-7 转成 ASCII 刚好不会产生乱码,虽然 UTF-7 并不被 Unicode 标准正式认可。

UIAutomator 输入端:

Charset UTF7 = Charset.forName("UTF-7");
Charset ASCII = Charset.forName("US-ASCII");

byte[] encoded = text.getBytes(UTF7);
String str = new String(encoded, ASCII);

boolean result = element.setText(str);

此时我们的字符中文已经被转成 &Ti1lhw- 这样的形式,通过实验,UTF-7 或 UTF-7 变种都以 &+ 作为开始,- 作为结束,天然的隔断,使得我们可以轻松的断字,处理起来方便很多,当一个字符完整技术后,触发字符提交即实现了中文输入。

为什么不用 UTF8?

UTF-8 没有字节序和 BOM 问题,而且 UTF-8 在互联网世界里使用也是最广泛的,但是 UTF-8 是一种变长的编码方式。它可以使用 1 ~ 4 个字节表示一个符号,根据不同的符号而变化字节长度。

Unicode 符号范围    | UTF-8 编码方式
(十六进制)          |(二进制)
--------------------+------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

如我们测试 “中文” 的 “中” 字,Unicode 是 4E2D,属于三个字节表示范围,即格式是 “1110xxxx 10xxxxxx 10xxxxxx”。然后,从 “中” 的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补 0。

当然,想利用 UTF 转换,也还是有办法的,比如我们可以利用 UTF-32 定长的特性,顺便说下,UTF-16 曾经是可以当定长编码使用,但是后期膨胀较快,Unicode 收录的字符很快就超过了 65536,所以如果还想用定长编码就只能采取 UTF-32,虽然浪费了空间和输入时间 (我们的场景里可以忽略这部分消耗),但是定长处理起来就没有了断字的困难。

另一种实现

我们可以将字逐一编码来实现,但是仍然会遇到上面出现的应该怎么隔断的问题,所以我们可以人工隔断。如下实现,将字符转成有 & 间隔的 16 进制 Unicode 字符串,避免了乱码。在 Android 输入端丢弃掉 & 结束前的按键响应,直到 & 将整个字符提交,实现中文的输入。

与 UTF-7 实现相比,键盘输入动作长度相等,耗时基本等价。

public static String string2Unicode(String string) {
    StringBuffer unicode = new StringBuffer();

    for (int i = 0; i < string.length(); i++) {
        char c = string.charAt(i);
        unicode.append(Integer.toHexString(c) + "&");
    }
    return unicode.toString();
}

boolean result = element.setText(string2Unicode(text));

StringBuffer string = new StringBuffer();
String str = "";

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    int c = getUnicodeChar(keyCode, event);

    if (c == 0) {
      return super.onKeyDown(keyCode, event);
    }

    if (c == '&') {
      int data = Integer.parseInt(string.toString(), 16);
      str = "" + (char) data;
      InputConnection ic = getCurrentInputConnection();
      ic.commitText(str, 1);
      return true;
    }

    string.append((char) c);
    return true;
}

Android 输入法服务

Macaca 已经将如上的一种实现封装成了模块,集成到 Android 驱动中,我们直接启动输入法服务就可以了,用户无感知。

$ adb shell ime enable android.unicode.ime/.Utf7ImeService
$ adb shell ime set android.unicode.ime/.Utf7ImeService

欢迎讨论,互相学习。

微博: http://weibo.com/xudafeng
Github: https://github.com/xudafeng

下一篇 - 原来程序员都是这么聊天的


↙↙↙阅读原文可查看相关链接,并与作者交流