实战:把微信语音转换成文字的全自动化解决方案(第二弹)

前言

《从微信中提取语音文件,并转换成文字的全自动化解决方案》里,我曾使用讯飞iOS SDK完成了将微信语音转化为文字的功能。在帖子的7楼@everettjf 说,微信本身就提供了长按语音转文字的功能,而我在前期研究微信时并没有发现这个功能。经过一番调研,发现只有当系统语言为中文时,微信才会开放这个功能,而我一直使用的是英文系统,所以没看到过这个功能。今天,我们就以这个功能为突破口,摒弃讯飞SDK,采用微信自带的方案,将语音全自动转换为文字。

开启英文版微信的语音转文字功能

这个过程我就不复述了,核心代码是:

%hook VoiceMessageNodeView
- (BOOL)canShowVoiceTransMenu
{
	%orig;
	return YES;
}
%end

大家编译一个tweak安装一下,就可以在英文版微信里开启这个功能了:point_down:

定位“Convert to Text”这个UIMenuItem的action

照例,我们先用Cycript,定位这个UIMenuItem。停留在上图的界面中,然后用choose命令做一次小小的hack:

cy# choose(UIMenuItem)
[#"<UIMenuItem: 0x15621200>",#"<UIMenuItem: 0x1562d020>",#"<UIMenuItem: 0x15680080>",#"<UIMenuItem: 0x1577f9b0>",#"<UIMenuItem: 0x157afe90>"]
cy# [#0x15621200 title]
@"Favorite"
cy# [#0x1562d020 title]
@"Turn Off Speaker"
cy# [#0x15680080 title]
@"More..."
cy# [#0x1577f9b0 title]
@"Convert to Text"

好了,找到了“Convert to Text”这个UIMenuItem,我们看看它的action是什么:

cy# [#0x1577f9b0 action]
@selector(onVoiceTrans:)

好了,onVoiceTrans:就是我们的答案了。咱们grep一遍WeChat的头文件,看看哪个类实现了这个方法:

FunMaker-MBP:~ snakeninny$ grep -r onVoiceTrans: /Users/snakeninny/Code/RE/WeChat
/Users/snakeninny/Code/RE/WeChat/VoiceMessageNodeView.h:- (void)onVoiceTrans:(id)arg1;
Binary file /Users/snakeninny/Code/RE/WeChat/WeChat_arm64.decrypted matches
Binary file /Users/snakeninny/Code/RE/WeChat/WeChat_armv7.decrypted matches

看起来是VoiceMessageNodeView这个类。从类名上猜测,这个类应该是语音信息view的类,在后面我们会验证这个猜测。

###查看[VoiceMessageNodeView onVoiceTrans:]的实现细节
把decrypt之后的WeChat可执行文件拖到Hopper里,待分析完毕之后,看看它的反编译C代码:


可以看到很明显的2条分支,1条简单调用了showVoiceTransView,另1条的操作相对复杂。我们先从简单的入手,看看showVoiceTransView都干了些什么。

查看[VoiceMessageNodeView showVoiceTransView]的实现细节

从不够完整的函数实现截图来看,showVoiceTransView做的基本都只是UI层面的操作;它们具体是什么呢?等会再揭晓,我们先看另1条相对复杂的分支。

查看else分支的实现细节

从截图里诸如getStringForCurLanguage:defaultTo:initWithTitle:andImageName:andContent:andCancelText:m_voiceTransIntroshow等关键词可以大概猜测,这段代码也是show了一个view出来,貌似也是UI层面的操作。如果if和else都是纯UI操作,那么语音转换的核心代码在哪里呢?是时候上LLDB,往更深一层次的代码去挖掘了。

用LLDB分析[VoiceMessageNodeView onVoiceTrans:]

用LLDB附加WeChat,然后在[VoiceMessageNodeView onVoiceTrans:]上下断点并触发:

(lldb) br s -a 0x00064000+0x0183e950
Breakpoint 1: where = WeChat`___lldb_unnamed_function88615$$WeChat, address = 0x018a2950
Process 3098 stopped
* thread #1: tid = 0x3a3d5, 0x018a2950 WeChat`___lldb_unnamed_function88615$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x018a2950 WeChat`___lldb_unnamed_function88615$$WeChat
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2950 <+0>: push   {r4, r5, r6, r7, lr}
    0x18a2952 <+2>: add    r7, sp, #0xc
    0x18a2954 <+4>: push.w {r8, r10, r11}
    0x18a2958 <+8>: sub    sp, #0x1c
(lldb)  

我们用ni单步跟一下这个函数,看看每一个objc_msgSend都是在干嘛:

* thread #1: tid = 0x3a3d5, 0x018a2974 WeChat`___lldb_unnamed_function88615$$WeChat + 36, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2974 WeChat`___lldb_unnamed_function88615$$WeChat + 36
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2974 <+36>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2978 <+40>: mov    r7, r7
    0x18a297a <+42>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a297e <+46>: mov    r5, r0
(lldb) p (char *)$r1
(char *) $0 = 0x0237235d "getMainSettingExt"
(lldb) po $r0
SettingUtil
* thread #1: tid = 0x3a3d5, 0x018a2998 WeChat`___lldb_unnamed_function88615$$WeChat + 72, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2998 WeChat`___lldb_unnamed_function88615$$WeChat + 72
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2998 <+72>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a299c <+76>: mov    r7, r7
    0x18a299e <+78>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a29a2 <+82>: mov    r6, r0
(lldb) p (char *)$r1
(char *) $2 = 0x0238a4f8 "theadSafeGetObject:"
(lldb) po $r0
<CSettingExt: 0x15722990>

(lldb) po $r2
SETTINGEXT_VOICE_TRANS_TIP_TIMES
* thread #1: tid = 0x3a3d5, 0x018a29b8 WeChat`___lldb_unnamed_function88615$$WeChat + 104, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a29b8 WeChat`___lldb_unnamed_function88615$$WeChat + 104
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a29b8 <+104>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a29bc <+108>: cmp    r0, #0x0
    0x18a29be <+110>: ble    0x18a29d0                 ; <+128>
    0x18a29c0 <+112>: movw   r0, #0xae4
(lldb) p (char *)$r1
(char *) $7 = 0x33c13528 "integerValue"
(lldb) po $r0
<nil>
* thread #1: tid = 0x3a3d5, 0x018a2a04 WeChat`___lldb_unnamed_function88615$$WeChat + 180, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2a04 WeChat`___lldb_unnamed_function88615$$WeChat + 180
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2a04 <+180>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2a08 <+184>: str    r0, [sp, #0x10]
    0x18a2a0a <+186>: movw   r0, #0xca5e
    0x18a2a0e <+190>: movt   r0, #0x10c
(lldb) p (char *)$r1
(char *) $9 = 0x33c09603 "alloc"
(lldb) po $r0
MMTipsViewController
* thread #1: tid = 0x3a3d5, 0x018a2a26 WeChat`___lldb_unnamed_function88615$$WeChat + 214, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2a26 WeChat`___lldb_unnamed_function88615$$WeChat + 214
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2a26 <+214>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2a2a <+218>: mov    r7, r7
    0x18a2a2c <+220>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2a30 <+224>: mov    r5, r0
(lldb) p (char *)$r1
(char *) $11 = 0x33c0c435 "defaultCenter"
(lldb) po $r0
MMServiceCenter
* thread #1: tid = 0x3a3d5, 0x018a2a50 WeChat`___lldb_unnamed_function88615$$WeChat + 256, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2a50 WeChat`___lldb_unnamed_function88615$$WeChat + 256
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2a50 <+256>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2a54 <+260>: mov    r2, r0
    0x18a2a56 <+262>: movw   r0, #0xca22
    0x18a2a5a <+266>: movt   r0, #0x10c
(lldb) p (char *)$r1
(char *) $13 = 0x33c0951e "class"
(lldb) po $r0
MMLanguageMgr
* thread #1: tid = 0x3a3d5, 0x018a2a68 WeChat`___lldb_unnamed_function88615$$WeChat + 280, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2a68 WeChat`___lldb_unnamed_function88615$$WeChat + 280
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2a68 <+280>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2a6c <+284>: mov    r7, r7
    0x18a2a6e <+286>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2a72 <+290>: str    r0, [sp, #0xc]
(lldb) p (char *)$r1
(char *) $15 = 0x0236945e "getService:"
(lldb) po $r0
<MMServiceCenter: 0x156dd840>

(lldb) po $r2
MMLanguageMgr
* thread #1: tid = 0x3a3d5, 0x018a2a8e WeChat`___lldb_unnamed_function88615$$WeChat + 318, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2a8e WeChat`___lldb_unnamed_function88615$$WeChat + 318
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2a8e <+318>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2a92 <+322>: mov    r7, r7
    0x18a2a94 <+324>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2a98 <+328>: str    r0, [sp, #0x8]
(lldb) p (char *)$r1
(char *) $18 = 0x0236a7db "getStringForCurLanguage:defaultTo:"
(lldb) po $r0
<MMLanguageMgr: 0x1573a430>

(lldb) po $r2
Voice_Trans_Tips_Title

(lldb) po $r3
Voice_Trans_Tips_Title
* thread #1: tid = 0x3a3d5, 0x018a2aa8 WeChat`___lldb_unnamed_function88615$$WeChat + 344, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2aa8 WeChat`___lldb_unnamed_function88615$$WeChat + 344
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2aa8 <+344>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2aac <+348>: mov    r7, r7
    0x18a2aae <+350>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2ab2 <+354>: mov    r6, r0
(lldb) p (char *)$r1
(char *) $22 = 0x33c0c435 "defaultCenter"
(lldb) po $R0
MMServiceCenter
* thread #1: tid = 0x3a3d5, 0x018a2ac2 WeChat`___lldb_unnamed_function88615$$WeChat + 370, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2ac2 WeChat`___lldb_unnamed_function88615$$WeChat + 370
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2ac2 <+370>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2ac6 <+374>: mov    r2, r0
    0x18a2ac8 <+376>: mov    r0, r6
    0x18a2aca <+378>: mov    r1, r10
(lldb) p (char *)$r1
(char *) $24 = 0x33c0951e "class"
(lldb) po $R0
MMLanguageMgr
* thread #1: tid = 0x3a3d5, 0x018a2ace WeChat`___lldb_unnamed_function88615$$WeChat + 382, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2ace WeChat`___lldb_unnamed_function88615$$WeChat + 382
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2ace <+382>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2ad2 <+386>: mov    r7, r7
    0x18a2ad4 <+388>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2ad8 <+392>: movw   r2, #0x6b2c
(lldb) p (char *)$r1
(char *) $26 = 0x0236945e "getService:"
(lldb) po $r0
<MMServiceCenter: 0x156dd840>

(lldb) po $r2
MMLanguageMgr
* thread #1: tid = 0x3a3d5, 0x018a2ae8 WeChat`___lldb_unnamed_function88615$$WeChat + 408, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2ae8 WeChat`___lldb_unnamed_function88615$$WeChat + 408
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2ae8 <+408>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2aec <+412>: mov    r7, r7
    0x18a2aee <+414>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2af2 <+418>: mov    r10, r0
(lldb) po $r0
<MMLanguageMgr: 0x1573a430>

(lldb) p (char *)$r1
(char *) $30 = 0x0236a7db "getStringForCurLanguage:defaultTo:"
(lldb) po $r2
Voice_Trans_Tips_Content

(lldb) po $r3
Voice_Trans_Tips_Content
* thread #1: tid = 0x3a3d5, 0x018a2b10 WeChat`___lldb_unnamed_function88615$$WeChat + 448, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2b10 WeChat`___lldb_unnamed_function88615$$WeChat + 448
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2b10 <+448>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2b14 <+452>: ldr.w  r1, [r4, r8]
    0x18a2b18 <+456>: str.w  r0, [r4, r8]
    0x18a2b1c <+460>: mov    r0, r1
(lldb) p (char *)$r1
(char *) $33 = 0x023b452b "initWithTitle:andImageName:andContent:andCancelText:"
(lldb) po $r0
<MMTipsViewController: 0x16544950>

(lldb) po $r2
Audio to Text

(lldb) po $r3
<nil>

(lldb) x/10 $sp
0x27d99c88: 0x160e8500 0x00000000 0x160d0a90 0x1573a430
0x27d99c98: 0x16544950 0x156dd840 0x00000000 0x157367a0
0x27d99ca8: 0x0000010f 0x1563d790
(lldb) po 0x160e8500
This feature only supported for Mandarin Chinese, and the result is for reference only.

(lldb) po 0x00000000
<nil>
* thread #1: tid = 0x3a3d5, 0x018a2b5a WeChat`___lldb_unnamed_function88615$$WeChat + 522, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2b5a WeChat`___lldb_unnamed_function88615$$WeChat + 522
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2b5a <+522>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2b5e <+526>: movw   r0, #0xcc36
    0x18a2b62 <+530>: movt   r0, #0x10c
    0x18a2b66 <+534>: add    r0, pc
(lldb) p (char *)$r1
(char *) $40 = 0x0236cc3c "setM_delegate:"
(lldb) po $r0
<MMTipsViewController: 0x16544950>

(lldb) po $r2
<VoiceMessageNodeView: 0x157087c0; frame = (183 0; 128 59); layer = <CALayer: 0x15708950>>
* thread #1: tid = 0x3a3d5, 0x018a2b6e WeChat`___lldb_unnamed_function88615$$WeChat + 542, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2b6e WeChat`___lldb_unnamed_function88615$$WeChat + 542
WeChat`___lldb_unnamed_function88615$$WeChat:
->  0x18a2b6e <+542>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2b72 <+546>: mov    r0, r6
    0x18a2b74 <+548>: add    sp, #0x1c
    0x18a2b76 <+550>: pop.w  {r8, r10, r11}
(lldb) p (char *)$r1
(char *) $43 = 0x33c0f712 "show"
(lldb) po $r0
<MMTipsViewController: 0x16544950>

到此告一段落。可以看到,这个分支其实是走了else的这一段,就是偏复杂的操作。值得注意的是0x18a2b10处的objc_msgSend,生成了一个关键句“This feature only supported for Mandarin Chinese, and the result is for reference only.”,我们c一下即可看到:


好了,出现了一个WeChat自定义的弹框。我们用Cycript看看,点击“OK”后会触发什么样的操作。

找到点击弹框上“OK”按钮触发的操作

一般来说,弹框都不会出现在keyWindow上,需要到其他的window里找寻它的踪迹;因此,我们依次检查各个window:

cy# [[UIApp windows][1] recursiveDescription].toString()
`<SvrErrorTipWindow: 0x1601d640; baseClass = UIWindow; frame = (0 0; 320 45); hidden = YES; gestureRecognizers = <NSArray: 0x1601d180>; layer = <UIWindowLayer: 0x1601d500>>
   | <UIImageView: 0x1601bc50; frame = (12.5 2.5; 40 40); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1601b8d0>>
   | <UIButton: 0x1601a5a0; frame = (295 12.5; 20 20); opaque = NO; layer = <CALayer: 0x1601a4f0>>
   |    | <UIImageView: 0x160194b0; frame = (0 0; 20 20); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x16019480>>
   | <RichTextView: 0x160189f0; baseClass = UILabel; frame = (65 5; 0 0); opaque = NO; layer = <CALayer: 0x16018890>>`
cy# [[UIApp windows][2] recursiveDescription].toString()
"<UITextEffectsWindow: 0x156acc50; frame = (0 0; 320 480); hidden = YES; opaque = NO; gestureRecognizers = <NSArray: 0x16455e60>; layer = <UIWindowLayer: 0x1623d730>>"
cy# [[UIApp windows][3] recursiveDescription].toString()
"<UITextEffectsWindow: 0x162e66e0; frame = (0 0; 320 480); hidden = YES; gestureRecognizers = <NSArray: 0x15615570>; layer = <UIWindowLayer: 0x1604ba00>>"
cy# [[UIApp windows][4] recursiveDescription].toString()
`<MMUIWindow: 0x162cd5d0; baseClass = UIWindow; frame = (0 0; 320 480); gestureRecognizers = <NSArray: 0x1572f4c0>; layer = <UIWindowLayer: 0x1645eb70>>
   | <UIView: 0x1602d620; frame = (0 0; 320 480); autoresize = W+H; layer = <CALayer: 0x1602d680>>
   |    | <UIButton: 0x16021780; frame = (0 0; 320 480); opaque = NO; autoresize = W+H; layer = <CALayer: 0x156dd060>>
   |    |    | <UIView: 0x15705000; frame = (20 144; 280 192); layer = <CALayer: 0x156e3fc0>>
   |    |    |    | <UIImageView: 0x165457f0; frame = (0 0; 280 192); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x156e3f40>>
   |    |    |    | <MMUILabel: 0x165456d0; baseClass = UILabel; frame = (15 22; 250 22); text = 'Audio to Text'; userInteractionEnabled = NO; layer = <CALayer: 0x156d7050>>
   |    |    |    | <MMUILabel: 0x16545350; baseClass = UILabel; frame = (16.5 59; 247 62); text = 'This feature only support...'; userInteractionEnabled = NO; layer = <CALayer: 0x156e3090>>
   |    |    |    | <FixTitleColorButton: 0x1604ae00; baseClass = UIButton; frame = (0 142; 280 50); opaque = NO; layer = <CALayer: 0x156e3480>>
   |    |    |    |    | <UIButtonLabel: 0x162220c0; frame = (127 14; 26 22); text = 'OK'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x156de430>>
   |    |    |    |    | <UIView: 0x16035ce0; frame = (0 0; 280 0.5); autoresize = W; layer = <CALayer: 0x156da050>>`
cy# [#0x1604ae00 allTargets]
[NSSet setWithArray:@[#"<MMTipsViewController: 0x16544950>"]]]
cy# [#0x1604ae00 allControlEvents]
64
cy# [#0x1604ae00 actionsForTarget:#0x16544950 forControlEvent:64]
@["onClickBtn:"]

在第5个window里(注意,[UIApp windows][0]就是keyWindow,因此这里是第5个window),我们找到了OK按钮,且它的响应函数是[MMTipsViewController onClickBtn:];去看看它的实现细节。

查看[MMTipsViewController onClickBtn:]的实现细节

[MMTipsViewController onClickBtn:]下个断点,动态跟跟看:

(lldb) br s -a 0x01cd8120+0x00064000
Breakpoint 2: where = WeChat`___lldb_unnamed_function108280$$WeChat, address = 0x01d3c120
Process 3098 stopped
* thread #1: tid = 0x3a3d5, 0x01d3c120 WeChat`___lldb_unnamed_function108280$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x01d3c120 WeChat`___lldb_unnamed_function108280$$WeChat
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c120 <+0>: push   {r4, r5, r6, r7, lr}
    0x1d3c122 <+2>: add    r7, sp, #0xc
    0x1d3c124 <+4>: push.w {r8, r10, r11}
    0x1d3c128 <+8>: mov    r4, r0
(lldb) ni
* thread #1: tid = 0x3a3d5, 0x01d3c140 WeChat`___lldb_unnamed_function108280$$WeChat + 32, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x01d3c140 WeChat`___lldb_unnamed_function108280$$WeChat + 32
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c140 <+32>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x1d3c144 <+36>: tst.w  r0, #0xff
    0x1d3c148 <+40>: bne    0x1d3c15c                 ; <+60>
    0x1d3c14a <+42>: movw   r0, #0x40ee
(lldb) p (char *)$r1
(char *) $45 = 0x023e6510 "bIsForbidCancelBtn"
(lldb) po $r0
<MMTipsViewController: 0x16544950>
* thread #1: tid = 0x3a3d5, 0x01d3c158 WeChat`___lldb_unnamed_function108280$$WeChat + 56, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x01d3c158 WeChat`___lldb_unnamed_function108280$$WeChat + 56
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c158 <+56>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x1d3c15c <+60>: movw   r0, #0xf490
    0x1d3c160 <+64>: movt   r0, #0xc7
    0x1d3c164 <+68>: add    r0, pc
(lldb) p (char *)$r1
(char *) $47 = 0x023e643d "hideTips"
(lldb) po $r0
<MMTipsViewController: 0x16544950>
* thread #1: tid = 0x3a3d5, 0x01d3c1a4 WeChat`___lldb_unnamed_function108280$$WeChat + 132, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x01d3c1a4 WeChat`___lldb_unnamed_function108280$$WeChat + 132
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c1a4 <+132>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x1d3c1a8 <+136>: mov    r4, r0
    0x1d3c1aa <+138>: mov    r0, r6
    0x1d3c1ac <+140>: blx    0x20eb5c0                 ; symbol stub for: objc_release
(lldb) p (char *)$r1
(char *) $49 = 0x33c098a1 "respondsToSelector:"
(lldb) p (char *)$r2
(char *) $51 = 0x023b4abf "onClickTipsBtn:"
* thread #1: tid = 0x3a3d5, 0x01d3c1c2 WeChat`___lldb_unnamed_function108280$$WeChat + 162, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x01d3c1c2 WeChat`___lldb_unnamed_function108280$$WeChat + 162
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c1c2 <+162>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x1d3c1c6 <+166>: mov    r0, r4
    0x1d3c1c8 <+168>: blx    0x20eb5c0                 ; symbol stub for: objc_release
    0x1d3c1cc <+172>: mov    r0, r5
(lldb) p (char *)$r1
(char *) $52 = 0x023b4abf "onClickTipsBtn:"
(lldb) po $r0
<VoiceMessageNodeView: 0x157087c0; frame = (183 0; 128 59); layer = <CALayer: 0x15708950>>

(lldb) po $r2
1

注意,这里调用了[VoiceMessageNodeView onClickTipsBtn:],我们把动态调试暂停在这里,去看看[VoiceMessageNodeView onClickTipsBtn:]的实现细节。

查看[VoiceMessageNodeView onClickTipsBtn:]的实现细节

这段代码很容易还原,它的核心操作是:

[[%c(SettingUtil) getMainSettingExt] theadSafeSetObject:@"1" forKey:@"SETTINGEXT_VOICE_TRANS_TIP_TIMES"];
AccountStorageMgr *manager = [[%c(MMServiceCenter) defaultCenter] getService:[%c(AccountStorageMgr) class]];
[manager SaveSettingExt];

从函数及参数名来看,这段代码的功能貌似就是作为语音转换的开关。我们继续ni,看看还有没有其他玄机。

继续查看[MMTipsViewController onClickBtn:]的实现细节

* thread #1: tid = 0x3a3d5, 0x01d3c1e8 WeChat`___lldb_unnamed_function108280$$WeChat + 200, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x01d3c1e8 WeChat`___lldb_unnamed_function108280$$WeChat + 200
WeChat`___lldb_unnamed_function108280$$WeChat:
->  0x1d3c1e8 <+200>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x1d3c1ec <+204>: mov    r4, r0
    0x1d3c1ee <+206>: mov    r0, r6
    0x1d3c1f0 <+208>: blx    0x20eb5c0                 ; symbol stub for: objc_release
(lldb) p (char *)$r1
(char *) $55 = 0x33c098a1 "respondsToSelector:"
(lldb) p (char *)$r2
(char *) $56 = 0x023b4acf "onClickTipsBtn:Index:"
(lldb) po $r0
<VoiceMessageNodeView: 0x157087c0; frame = (183 0; 128 59); layer = <CALayer: 0x15708950>>

因为[VoiceMessageNodeView respondsToSelector:@selector(onClickTipsBtn:Index:)]的返回值为NO,所以这一大段分析,得出的核心代码,就是上面提取出的这段,且它的功能,就是开启微信语音转换功能的开关。这段代码怎么使用呢?我们只需要在微信第一次启动时调用这段代码,就可以开启微信的语音转换功能了。
开启了语音转换之后,我们取得了阶段性胜利。但是下一个问题来了,哪段代码是负责实际转换操作的呢?这才是我们的重中之重。

找到语音转换操作的核心代码

刚才的[VoiceMessageNodeView onClickTipsBtn:]里,除了开启语音转换功能的代码外,还有一个低调的showVoiceTransView,它的调用者是一个VoiceMessageNodeView对象,我猜语音转换操作的核心代码就藏着这里。我们看看它的反编译代码:


还挺复杂。咱们动态跟跟看:

(lldb) br s -a 0x0183eb84+0x64000
Breakpoint 3: where = WeChat`___lldb_unnamed_function88616$$WeChat, address = 0x018a2b84
Process 3098 stopped
* thread #1: tid = 0x3a3d5, 0x018a2b84 WeChat`___lldb_unnamed_function88616$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x018a2b84 WeChat`___lldb_unnamed_function88616$$WeChat
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2b84 <+0>: push   {r4, r5, r6, r7, lr}
    0x18a2b86 <+2>: add    r7, sp, #0xc
    0x18a2b88 <+4>: str    r8, [sp, #-4]!
    0x18a2b8c <+8>: vpush  {d8}
(lldb)  
* thread #1: tid = 0x3a3d5, 0x018a2bde WeChat`___lldb_unnamed_function88616$$WeChat + 90, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2bde WeChat`___lldb_unnamed_function88616$$WeChat + 90
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2bde <+90>:  blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2be2 <+94>:  mov    r7, r7
    0x18a2be4 <+96>:  blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2be8 <+100>: mov    r8, r0
(lldb) p (char *)$r1
(char *) $59 = 0x0236deb3 "getAppViewControllerManager"
(lldb) ni
Process 3098 stopped
* thread #1: tid = 0x3a3d5, 0x018a2be2 WeChat`___lldb_unnamed_function88616$$WeChat + 94, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2be2 WeChat`___lldb_unnamed_function88616$$WeChat + 94
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2be2 <+94>:  mov    r7, r7
    0x18a2be4 <+96>:  blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2be8 <+100>: mov    r8, r0
    0x18a2bea <+102>: movw   r0, #0xe05a
(lldb) po $r0
<CAppViewControllerManager: 0x1601dcb0>
* thread #1: tid = 0x3a3d5, 0x018a2bf8 WeChat`___lldb_unnamed_function88616$$WeChat + 116, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2bf8 WeChat`___lldb_unnamed_function88616$$WeChat + 116
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2bf8 <+116>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2bfc <+120>: mov    r7, r7
    0x18a2bfe <+122>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2c02 <+126>: mov    r6, r0
(lldb) p (char *)$r1
(char *) $61 = 0x023743eb "getMainWindow"
(lldb) po $r0
<CAppViewControllerManager: 0x1601dcb0>

(lldb) ni
Process 3098 stopped
* thread #1: tid = 0x3a3d5, 0x018a2bfc WeChat`___lldb_unnamed_function88616$$WeChat + 120, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2bfc WeChat`___lldb_unnamed_function88616$$WeChat + 120
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2bfc <+120>: mov    r7, r7
    0x18a2bfe <+122>: blx    0x20eb610                 ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x18a2c02 <+126>: mov    r6, r0
    0x18a2c04 <+128>: cbz    r5, 0x18a2c30             ; <+172>
(lldb) po $r0
<iConsoleWindow: 0x156b8ae0; baseClass = UIWindow; frame = (0 0; 320 480); autoresize = W+H; gestureRecognizers = <NSArray: 0x156b9290>; layer = <UIWindowLayer: 0x156b8df0>>
* thread #1: tid = 0x3a3d5, 0x018a2d0e WeChat`___lldb_unnamed_function88616$$WeChat + 394, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2d0e WeChat`___lldb_unnamed_function88616$$WeChat + 394
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2d0e <+394>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2d12 <+398>: movw   r0, #0x930e
    0x18a2d16 <+402>: add.w  r9, sp, #0x20
    0x18a2d1a <+406>: movt   r0, #0x10e
(lldb) po $r2
{m_uiMesLocalID=2, m_ui64MesSvrID=6982651110964038564, m_nsFromUsr=wxi*f12~19, m_nsToUsr=we*in~6, m_uiStatus=2, type=34, msgSource="(null)"} 

(lldb) po $r0
<VoiceTransFloatPreview: 0x16543cd0; baseClass = UIWindow; frame = (0 0; 320 480); hidden = YES; gestureRecognizers = <NSArray: 0x16053830>; layer = <UIWindowLayer: 0x15624d10>>

(lldb) p (char *)$r1
(char *) $77 = 0x02421864 "setVoiceMsg:"
* thread #1: tid = 0x3a3d5, 0x018a2d40 WeChat`___lldb_unnamed_function88616$$WeChat + 444, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x018a2d40 WeChat`___lldb_unnamed_function88616$$WeChat + 444
WeChat`___lldb_unnamed_function88616$$WeChat:
->  0x18a2d40 <+444>: blx    0x20eb570                 ; symbol stub for: objc_msgSend
    0x18a2d44 <+448>: add    sp, #0x30
    0x18a2d46 <+450>: vpop   {d8}
    0x18a2d4a <+454>: ldr    r8, [sp], #4
(lldb) p (char *)$r1
(char *) $79 = 0x02421881 "showWithAnimate:"

我省略了一些明显是UI层的操作,留下了上面这些objc_msgSend。我们回顾一下微信语音转文字时的UI效果:当我们点击“Convert to Text”之后,新的界面上出现“converting…”的字样,等待几秒钟,转换好的文字就会出现在界面上。这说明,微信可能是先把显示文字的UI给画出来,以“converting…”提示用户等待,同时开一个另一个线程去转换语音,待语音转换完毕后再把文字给显示在UI上。结合我们的猜测,上面的一系列objc_msgSend中,最可疑的无疑是[VoiceTransFloatPreview setVoiceMsg:(CMessageWrap *)][VoiceTransFloatPreview showWithAnimate:]。我们分别看一下它们的实现细节。

查看[VoiceTransFloatPreview setVoiceMsg:]的实现细节

看起来只是一个普通的setter,没有什么特别的地方,继续下一目标。

查看[VoiceTransFloatPreview showWithAnimate:]的实现细节

在一系列的函数中,[r4 onStartGet]引起了我的注意,它是除IdleTimerUtil外,唯一没有出现UI字眼的函数;我们看看它的实现。

查看[VoiceTransFloatPreview onStartGet]的实现细节

看到VoiceTransHelper的字眼,我知道我们的分析快要接近尾声了。打开VoiceTransHelper.h,诸如

- (id)initWithVoiceMsg:(id)arg1 VoiceID:(id)arg2;
- (void)startVoiceTrans;
- (void)stopVoiceTrans;
- (void)HandleGetVoiceTransResp:(id)arg1 Event:(unsigned long)arg2;

等可疑函数尽收眼底,等待检阅。对于这个类的调研,就留作练习,交给正在阅读此帖的你来完成吧~!

总结

微信的语音识别技术想必不会比讯飞强大,仅论语音识别配置的精细度来说,讯飞就要专业很多。但是,如果我们的需求仅仅是简单的语音识别,没有太多定制化的需求,那么打包讯飞SDK之后的dylib会比采用微信原生语音转换的dylib大5M左右。跟我在这里提到的思路一脉相承——我们的dylib存活在别人的进程里,相当于是我们去其他人家做客,不给别人添麻烦是基本的教养和礼貌,能够节省5+M内存,是一种尊重他人劳动成果的体现,更是我们自我要求精益求精的缩影。工程师的素养体现在一点一滴中,见微知著,才能成就大业;祝愿大家都能持续进步,再攀高峰:wink:

5 个赞

赞,抽时间学习下!

:thumbsup: 貌似研究微信能缓解工作压力,哈

可以将很多天的语音聊天记录以文字形式导出到Excel或word中么

你的问题相当于,我做了满汉全席,你来一句:能不能放葱花?

可以做满汉全席,更重要的是,怎么做满汉全席,都告诉你了。你说你能放葱花吗?