南京音乐推荐联合社

脑洞之旅:Xcode Log汉字解码

转转App技术团队 2019-05-23 16:06:53

口水惹得祸

        经常看server端的同学通过命令行使用grep、sed 、awk查看线上日志,灵活的查询方式,酷炫的输出展示馋的客户端同学流口水。虽然我们大部分时间是使用断点调试,但NSLog大家用的也不少,随着代码规模的不断扩大,Xcode控制台中输出的信息也越来越多。激增的日志信息,往往让使用者很难找到属于自己的那部分。Xcode虽然也提供了一些日志搜索能力(下图),但是相较于grep+sed+awk的强大组合,还是有点太小儿科了。

要是能够用grep+sed+awk查询Xcode输出的日志就爽了,我们想。


拿什么查看你,我的日志

  • Xcode的控制台不支持对日志进行命令行搜索,直接放弃了。

  • 可以通过Devices(Xcode -> Window ->Devices)查看手机应用输出的日志,而且无论手机是否连接Xcode都可以查看。听起来都超预期了,但现实是残酷的,Devices的搜索能力和Xcode控制台如出一辙,放弃。

      找已有轮子的希望破灭后,我们思考,如果通过Terminal实时读取Xcode生成的日志文件,也能实现最初的目的。但是Xcode在连接真机调试时,log文件是存放在iPhone中的。只有使用模拟器进行调试时,日志才放在本地的~/Library/Logs/CoreSimulator/APP唯一标识(如:31C8CDFD-70AD-4E9E-BCFB-56AA2132D75A)/system.log:路径下。或通过Simulator ->Debug -> Open System Log直接查看它。

应用在真机中生成的log文件,有什么办法通过mac电脑进行实时读取吗?一番折腾发现,网上的方案多是通过在iPhone真机中搭建一个小型的HTTP Server,然后在mac端通过浏览器进行访问。这种方式一是繁琐,二是不能满足我们的需求,因为浏览器也不支持linux命令搜索呀。

        当然神器也不是没有,程序的世界,就是不缺轮子。国外有个软件叫iOS Console号称The most awesome iOS console log viewer,这个软件确实牛B,能够实时读取iPhone真机中的log,但是还是不够牛,因为它提供的搜索能力还是太弱。如果你安装了Xcode,这个软件完全就是多余的。

        iOS Console虽然没能满足我们最初的设想:mac端查看+支持linux命令搜索。但是,它实时读取真机log的能力,开阔了我们的视野。这说明,除了Apple自己之外,第三方也是有能力实时读取iPhoen真机log文件的。

        读取日志哪家强,deviceconsole来帮忙。deviceconsole是git上一个有6年历史的悠久项目,虽然很小(仅300余行代码),但是,很好,很强大。因为它的核心功能就是通过mac的Terminal实时读取iPhone的日志信息。Terminal、日志信息、实时,这不正是我们一直追求的梦幻组合吗?!是的,就是这么easy。区区300行C代码,目标实现了。怀着激动的心情,我们在命令行中执行了deviceconsole程序,果然不负众望,而且还有超预期的颜色主题功能。

        但是,等等。好像有哪里不对。postName = "\134U7ea2\134U4e94..." 是什么东西,\M-e\M^P\M-/\M-e...又是什么东西,输出的信息怎么有这么多奇奇怪怪的符号。我们的汉字勒?这些奇怪的东西不会是汉字吧?恩,原来,故事刚刚开始。?

神秘的134

        经过比对发现,deviceconsole输出的那些奇怪字符,果然是表示汉字的。说实话,看到这些乱码,我们首先想到的是放弃。大不了再找别的解决方案。但是回顾一路走来的历程,真的还有别的方案吗?也许解决这些乱码就是最好的方案。

仔细查看deviceconsole的输出发现,乱码主要分为两类。一是以\134开头的字符串,另一类是\M轮番出现的字符串。\M看着毫无头绪,\134U7ea2看着还有点眼熟。那就先从解决134开始。

U7ea2是汉字的unicode编码,这个可以理解。但是神秘的数字134是什么东西?为什么每一个unicode编码前都有一个134?有啥特殊含义?百思不得姐。为了弄清134的含义,我们将相同的日志在Xcode的控制台和Devices(deviceconsole的输出和Devices的一模一样)中分别输出如下所示:

xcode控制台的输出

Devices中的输出

        对比发现,\134U5317\134U4eac 等价于 \U5317\U4eac经查ASCII码表,原来134代表的就是\,Xcode的控制台显示log时应该是对\134这种转义字符的转义做了处理,而Devices和deviceconsole就没那么友好。

\U5317咋就转不成汉字呢?

        解决了神秘的134,我们就开始准备将字符串转汉字了。算法很快完成构思,将类似\134U5317\134U4eac这样的字符串,使用替换的方式干掉其中多余的134,然后再将U替换为u,最后输出。程序如下:

一番折腾之后,重新运行deviceconsole,结果然并卵。熟悉的汉字并没有如期展示出来,输出的内容依然是\u5317这样的字符串。

看来,还是得从长计议。

仔细阅读deviceconsole源码发现,它之所以能够实时读取iPhone真机中的log,是通过Apple提供的一个私有库MobileDevice.framework使mac与iPhone建立socket通信,然后将其中的define AMSVC_SYSLOG_RELAY          CFSTR("com.apple.syslog_relay")类型信息过滤出来。而mac端接收iPhone的数据,主要靠下面这个回调方法实现:

一切的源头,就是这个方法中的字节流data


通过监听上面的socket回调方法,我们截取到这样一段字节码:

<004a756c 20323720 31383a31 383a3136 2047616f 6d696e67 64656950 686f6e65 3750206c 6f636174 696f6e64 5b323933 345d203c 4e6f7469 63653e3a 207b226d 7367223a 22616461 70746572 20646574 61696c73 222c2022 61646170 65724465 73637269 7074696f 6e223a22 75736220 686f7374 222c2022 62617474 65727943 68617267 65725479 7065223a 226b4368 61726765 72547970 65557362 227d0a>

此段字节码以00开头,总长度为159(上面是16进制表示,每两个数字代表一个字节Byte)。

经过deviceconsole的处理后,输出在Terminal中显示为:

Jul 27 18:18:16 GaomingdeiPhone7P locationd[2934] <Notice>: {"msg":"adapter details", "adaperDescription":"usb host", "batteryChargerType":"kChargerTypeUsb"}

字符串总长度为158,因为剔除了字节码中的第一个字节00

输出英文就很正常,中文就出现乱码,我们不禁思考,中文编码和英文编码,到底有什么差别?这篇文章写的非常详实浅显《刨根究底字符编码

简而言之,ASCII使用一个字节,256种字符就能满足所有英文的输出需求。但中文用256种字符显然是不够的。所以,就产生了Unicode和UTF-8两种编码形式以满足类似中文这样的语言输出需求。其中,Unicode是使用定长的字节表示字符(2个字节,如u5317表示或4个字节),UTF-8使用的是不定长字节表示字符(具体可见《刨根究底字符编码之十二——UTF-8究竟是怎么编码的》)。

至此,总结一下。ASCII码用1个字节表示字符。Unicode码用2个或4个。UTF-8用不定长字节。那么上面的字节流是什么编码格式呢?159个字节转换成了158个字符,显然。这是ASCII编码格式。

那么,也就可以理解为什么对上面的字节流使用Unicode或者UTF-8进行解码得到的总是更乱的乱码了。因为这个字节流本质是一个ASCII码流,只能使用ASCII的方式去解码,如果用Unicode去解码,解码程序会把字节流中的两个字节当成一个字符,输出的结果,绝对不是我们想要的。同理,如果用UTF-8进行解码,解码程序会把字节流按照UTF-8编码规则进行识别,结果也是不可控的。

所以,回到我们最开始的疑问,\u5317虽然长的很像一个Unicode码,但它本质上是一个字符串,确切地说是由多个ASCII码组成的字符串。而如何把这个\u5317 转成我们想要的中文。可行的方案,至少应该包含以下流程:

[图]Unicode字符串转16进制Unicode再转UTF-8编码

核心代码如下:

不厌其烦的\M

解决了字符串型Unicode码转中文的问题,接下来我们要啃更加诡异的字符了。观察日志,我们发现总有中文被转成不断重复\M的天书。\M是什么特殊的编码格式标识吗?思考最终还是回归字节流本身。通过上面的分析,我们知道socket接口返回的字节流是按照ASCII编码规则编码的。这些神秘的\M 应该也是汉字的编码按照ASCII进行解码后的输出。那么,通过将这些神秘的\M再转换成二进制字节流,然后和汉字的正常编码格式进行对比,也许我们就能找出其中的规律。说干就干。

我们截取一段这样的\M字符串:

[>>>>[IMAPILog]     ][\M-e\M-“\M^^\M-i\M^G\M^O\M-h\M^N\M-7\M-e\M^O\M^V\M-h\M^A\M^T\M-g\M-3\M-;\M-d\M-:\M-:\M-f\M^H\M^P\M-e\M^J\M^_ ][maxTimestamp:1501255281826] \M-e\M^E\M-10\M-d\M-8\M-* <-

这段乱码的正确含义是:>>>>[ZZIM] 增量获取联系人成功 maxTimestamp:..... //其中.....是省略部分

通过监听socket接口,得知它的字节流如下:

<3e3e3e3e 5b494d41 50494c6f 675d2020 2020205c 4d2d655c 4d2d225c 4d5e5e5c 4d2d695c 4d5e475c 4d5e4f5c 4d2d685c 4d5e4e5c 4d2d375c 4d2d655c 4d5e4f5c 4d5e565c 4d2d685c 4d5e415c 4d5e545c 4d2d675c 4d2d335c 4d2d3b5c 4d2d645c 4d2d3a5c 4d2d3a5c 4d2d665c 4d5e485c 4d5e505c 4d2d655c 4d5e4a5c 4d5e5f20 6d617854 696d6573 74616d70 3a313530 31323535 32383138 3236205c 4d2d655c 4d5e455c 4d2d3130 5c4d2d64 5c4d2d38 5c4d2d2a 203c2d0a>

分析:socket字节流中,单字节字符都能正常解析,如下所示:3e 解析为:>5b 解析为:[

但是,汉字在上面字节流中的编码特别奇怪,经分析, "增量获取联系人成功" 这9个汉字对应的字节流如下所示:

5c4d 2d65   -> e55c4d 2d22   -> a25c4d 5e5e   -> 9e

5c4d 2d69   -> e95c4d 5e47   -> 875c4d 5e4f   -> 8f

5c4d 2d68   -> e85c4d 5e4e   -> 8e5c4d 2d37   -> b7

5c4d 2d65   -> e5    5c4d 5e4f    -> 8f5c4d 5e56   -> 96

5c4d 2d68   -> e85c4d 5e41   -> 815c4d 5e54   -> 94

5c4d 2d67   -> e75c4d 2d33   -> b35c4d 2d3b   -> bb

5c4d 2d64   -> e45c4d 2d3a   -> ba5c4d 2d3a   -> ba

5c4d 2d66   -> e65c4d 5e48   -> 885c4d 5e50   -> 90

5c4d 2d65   -> e55c4d 5e4a   -> 8a5c4d 5e5f    -> 9f

通过上面字节流可以看出,全部都是12个字节表示一个汉字,其中5c4d应该为转义字节(猜测),去除5e4d后,相当于每6个字节表示一个汉字。

       用这个汉字进行单字分析:     (1)字对应的Unicode码为589e     (2)字Unicode码转为UTF-8编码后的16进制表示为e5a29e   在线查询     (3)字在上述字节流中的表示为:

        观察上面的字节流发现,字节流的尾数是52e字的UTF-8 16进制表示法的尾数恰好匹配!但是,字节流的整体是通过什么算法和UTF-8的16进制表示匹配的?

结合以上所有9个汉字的字节流,我们发现,有以下的转换规律:

疑问:上表中,socket字节流经过怎样的算法才能够得到目标16进制呢?

socket返回的字节流(16进制)  目标16进制

5e4                                      ->      80101 1110 0100                  -> 10 005e5    ->      90101 1110 0101                  ->  10 012d2    ->       a0010 1101 0010                  ->   10 102d3                                      ->       b0010 1101 0011                  ->    10 112d6                                      ->        e0010 1101 0110                  ->     11 10

经过一番脑洞之后,我们(其实就是@露哥)发现了这样的算法:

第一个(5e4->8)
 0101
& 1110
= 0100 -> 0100
       + 0100
       = 1000
第二个(5e5->9)
 0101
& 1110
= 0100 -> 0100
       + 0101
       = 1001

第三个(2d2->a)
 0010
& 1101
= 0000 -> 1000
       + 0010
       = 1010
第四个(2d3->b)
 0010
& 1101
= 0000 -> 1000
       + 0011
       = 1011
第五个(2d6->e)
 0010
& 1110
= 0010 => 0010
       + 0110
       = 1100

这个算法总结下来就是取5c4d这两个个转义字节后面的两个字节A、B,然后用A的高字节AH与A的低字节AL进行与运算,再用与的结果与B的高字节BH进行加法运算。

Apple为什么会有这么变态的转换算法(猜想)举例,的UTF-8 16进制编码是e5a29e,而苹果系统内的UTF-8编码时char *字节流。char的取值范围是-128~127 即 -80~7f,可以看出,其中的9e是无法存储在char中的。所以,苹果为了解决这种问题,将9e拆分到两个字节中进行存储即5e5e,所以,就有了上面那个变态的算法。

OK,算法有了,剩下的就是撸代码。核心代码如下:

最后

终于,iOS客户端的日志也能愉快地在命令行中使用grep、sed、awk啦。。

Copyright © 南京音乐推荐联合社@2017