工作中自己负责IM功能的开发,平时对微信进行了不少研究学习。 这篇文章主要关注微信iOS客户端界面实现中的“聊天消息界面”实现。
写这篇文章的目的: - 分享微信的聊天界面实现方式。 - 展示逆向主要流程。
PS: 最初是为了解决项目中的一个小问题才逆向的微信。
设备:iPhone5 iOS 8.4 越狱
usbmuxd
➜ python-client python tcprelay.py -t 22:2222 Forwarding local port 2222 to remote port 22 ......
ssh
ssh root@localhost -p 2222
找到可执行文件:
everettjfs-iPhone:~ root# ps aux | grep /App mobile 38363 4.4 8.5 776400 88748 ?? Ss 8:55PM 0:52.96 /var/mobile/Containers/Bundle/Application/25FB096A-8122-49B5-9304-5FDB9080D9B0/WeChat.app/WeChat
沙盒路径:
everettjfs-iPhone:~ root# cycript -p WeChat cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] @[#"file:///var/mobile/Containers/Data/Application/F36BD1C1-1C39-4C83-AD4B-6D9F2B976330/Documents/"]
砸壳:
everettjfs-iPhone:~ root# clutch -i everettjfs-iPhone:~ root# clutch -b com.tencent.xin Finished dumping com.tencent.xin to /var/tmp/clutch/5F6CA026-C176-4FB0-9569-90F2DD251385
导出头文件:
这里不用class-dump-z 是因为class-dump-z会无法识别UIKit的很多类。
[everettjf@e w ]$ class-dump -s -S -H WeChat -o headers
打开微信,进入和某个人的会话(也就是这篇文章要研究的“聊天消息界面”)
everettjfs-iPhone:~ root# cycript -p WeChat cy# [[[UIWindow keyWindow] rootViewController] _printHierarchy].toString() <MMTabBarController 0x18265240>, state: appeared, view: <UILayoutContainerView 0x18265ac0> | <MMUINavigationController 0x1800f230>, state: appeared, view: <UILayoutContainerView 0x180cd0e0> | | <NewMainFrameViewController 0x179a2400>, state: disappeared, view: <MMUIHookView 0x1827f980> not in the window | | <BaseMsgContentViewController 0x179b3800>, state: appeared, view: <UIView 0x16e36c30> | <MMUINavigationController 0x181a40a0>, state: disappeared, view: <UILayoutContainerView 0x181a4400> not in the window | | <ContactsViewController 0x17162800>, state: disappeared, view: (view not loaded) | <MMUINavigationController 0x181adb10>, state: disappeared, view: <UILayoutContainerView 0x181ade00> not in the window | | <FindFriendEntryViewController 0x179aec00>, state: disappeared, view: (view not loaded) | <MMUINavigationController 0x18003e00>, state: disappeared, view: <UILayoutContainerView 0x18008cc0> not in the window | | <MoreViewController 0x179ad400>, state: disappeared, view: (view not loaded)
微信主界面是个MMTabBarController,有四个TabBarItem,分别对应一个MMUINavigationController。对应的RootViewController如下:
此次我们关注的“聊天消息界面”,就是BaseMsgContentViewController (state:appeared)。
界面中各种类型的消息都发送一下,这里先发送:文本、图片、位置、语音。 使用Reveal观察,如下图:
从这两张图可以看到:
整个消息列表本质上是个 MMTableView (这个我们自己实现一般也是这么做)。从class-dump出的头文件中可知道,MMTableView是UITableView的子类。
@interface MMTableView : UITableView <MMDelegateCenterExt>
TableView的Cell只有一种类型, MultiSelectTableViewCell。 这里开始看到时,很让我好奇。为什么没有采用传统的一个消息一个Cell的方式呢
@interface MultiSelectTableViewCell : UITableViewCell
Cell都是MultiSelectTableViewCell,而区分不同消息的是contentView的内容。
此外,消息之间的时间,也是MultiSelectTableViewCell,只是contentView是关于时间的Label。
弄明白消息UI的基本结构,下一步就是找到如何创建这些MessageNodeView。 这里很容易有个疑问,所有消息都是MultiSelectTableViewCell,那如何实现的Cell重用呢? 继续探索。
在class-dump出的头文件中找到 BaseMsgContentViewController类。可以找到 BaseMsgContentViewController.h文件,这个头文件有614行,可见这个类的复杂。(估计微信开发早期并没有考虑到后期的大量需求加入,于是成了今天的Mess View Controller)
这里细化此次行动的目的:想知道聊天中的每一条消息是如何创建、显示的。
观察类的实现,发现一些相关变量和方法:
// 字面上看,应该就是存储MessageNode的数组 NSMutableArray *m_arrMessageNodeData; // 应该是存储所有支持的MessageNode Class类型 struct vector<Class, std::__1::allocator<Class>> m_messageNodeClass; // 这就是主要是TableView MMTableView *m_tableView; // 预创建消息,有意思,一会儿仔细研究研究 - (void)preCreateMessageContentNode:(id)arg1; - (void)preCreateMessageSplitNode:(id)arg1; - (void)preCreateMessageTimeNode:(id)arg1; // 初始化Class - (void)initMessageNodeClass; - (id)newMessageNodeViewForMessageWrap:(id)arg1 contact:(id)arg2 chatContact:(id)arg3; // 获取node数目 - (unsigned int)getMsgNodeCount; // 获取指定索引的node - (id)getNodeDataByIndex:(unsigned int)arg1; // 获取消息node数组 - (id)GetMessageNodeDataArray; // 添加 - (void)addMessageNode:(id)arg1 layout:(BOOL)arg2 addMoreMsg:(BOOL)arg3; - (void)addReceiveMessageNode:(id)arg1; - (id)addSplitNode:(id)arg1 addMoreMsg:(BOOL)arg2; - (void)addTimeNode:(id)arg1 layout:(BOOL)arg2 addMoreMsg:(BOOL)arg3; // 移除 - (void)removeAllObjectsFromMessageNodeDatas; - (void)removeObjectsFromMessageNodeDatas:(id)arg1; // 更新 - (void)updateMessageNodeImageLoadingPercent:(unsigned long)arg1 percent:(unsigned long)arg2; - (void)updateMessageNodeStatus:(id)arg1; - (void)updateMessageNodeViewForOrientation:(id)arg1; // 一些NodeView的事件 - (void)tagLink:(id)arg1 messageWrap:(id)arg2; - (void)tapAppNodeView:(id)arg1; - (void)tapFriendCard_NodeView:(id)arg1 WithContact:(id)arg2 WithMsg:(id)arg3; - (void)tapImage_NodeView:(id)arg1; - (void)tapLocation_NodeView:(id)arg1; - (void)tapPushContact_NodeView:(id)arg1; - (void)tapPushMail_NodeView:(id)arg1 withPushMailWrap:(id)arg2; - (void)tapReader_NodeView:(id)arg1; - (void)tapStatus_NodeView:(id)arg1; - (void)tapText_NodeView:(id)arg1; - (void)tapVideoStatus_NodeView:(id)arg1;
cycript 打印出来
cy# v = #0x15067600 #"<BaseMsgContentViewController: 0x15067600>" cy# v->m_arrMessageNodeData @[#"<CMessageNodeData: 0x15b95260>",#"<CMessageNodeData: 0x15adf260>",#"<CMessageNodeData: 0x15a4abb0>",#"<CMessageNodeData: 0x1580f190>",#"<CMessageNodeData: 0x15a49930>",#"<CMessageNodeData: 0x1589b8a0>",#"<CMessageNodeData: 0x15a41410>",#"<CMessageNodeData: 0x158783e0>",#"<CMessageNodeData: 0x15a4a3b0>",#"<CMessageNodeData: 0x15aa14f0>",#"<CMessageNodeData: 0x1475ce50>",#"<CMessageNodeData: 0x15bf9960>",#"<CMessageNodeData: 0x15b53f40>",#"<CMessageNodeData: 0x147ad9f0>",#"<CMessageNodeData: 0x15b6d240>",#"<CMessageNodeData: 0x15ba04b0>",#"<CMessageNodeData: 0x15b90050>",#"<CMessageNodeData: 0x15be7ba0>",#"<CMessageNodeData: 0x15b84eb0>"] cy# v->m_arrMessageNodeData.count 19
前提,与对方的聊天消息已经有很多条 ,首次打开与对方的聊天消息界面,可以看到微信默认只加载19条消息。
CMessageNodeData是什么?
@interface CMessageNodeData : NSObject { int m_eMsgNodeType; CMessageWrap *m_msgWrap; UIView *m_view; unsigned long m_uCreateTime; }
@interface CMessageWrap : MMObject <IAppMsgPathMgr, ISysNewXmlMsgExtendOperation, IMsgExtendOperation, NSCopying> { BOOL m_bIsSplit; BOOL m_bNew; unsigned long m_uiMesLocalID; long long m_n64MesSvrID; NSString *m_nsFromUsr; NSString *m_nsToUsr; unsigned long m_uiMessageType; NSString *m_nsContent; unsigned long m_uiStatus; unsigned long m_uiImgStatus; //.............省略大量字段.............
CMessageWrap自然就是对消息数据的封装。
CMessageNodeData有个UIView *m_view的变量,看看是什么:
y# d = v->m_arrMessageNodeData @[#"<CMessageNodeData: 0x15b95260>",#"<CMessageNodeData: 0x15adf260>",#"<CMessageNodeData: 0x15a4abb0>",#"<CMessageNodeData: 0x1580f190>",#" .....省略.... cy# var x = []; for(var i = 0; i < d.count;i++) x.push([d objectAtIndex:i].m_view); x [#"<UIView: 0x1592f9c0; frame = (0 10; 320 28); layer = <CALayer: 0x147e6cc0>>",#"<TextMessageNodeView: 0x1582cca0; frame = (251 0; 60 59); layer = <CALayer: 0x158b1350>>",#"<TextMessageNodeView: 0x15a58d60; frame = (251 0; 60 59); layer = <CALayer: 0x15a3cdf0>>",#"<TextMessageNodeView: 0x15a3ba10; frame = (251 0; 60 59); layer = <CALayer: 0x15ab9ab0>>",#"<TextMessageNodeView: 0x15a31610; frame = (251 0; 60 59); layer = <CALayer: 0x15a31760>>",#"<TextMessageNodeView: 0x15a57dc0; frame = (251 0; 60 59); layer = <CALayer: 0x15a547b0 .....省略....... odeView: 0x159ee260; frame = (186 0; 125 59); layer = <CALayer: 0x159ee3b0>>",#"<ImageMessageNodeView: 0x15b91cd0; frame = (179.5 0; 131.5 150); layer = <CALayer: 0x15b6e8e0>>",#"<UIView: 0x15b675d0; frame = (0 10; 320 28); layer = <CALayer: 0x15be5fa0>>",#"<LocationMessageNodeView: 0x15bdaee0; frame = (50 0; 261 139); layer = <CALayer: 0x15b7d290>>",#"<TextMessageNodeView: 0x15bbf400; frame = (144 0; 167 59); layer = <CALayer: 0x15be6770>>"]
可见,m_view就是MultiSelectTableViewCell的contentView下的那个UIView。
为验证,我随便发送了几百条各种消息,再输出所有的m_view。
cy# d.count 419 cy# var x = []; for(var i = 0; i < d.count;i++) x.push([d objectAtIndex:i].m_view); x [#"<UIView: 0x15c91540; frame = (0 10; 320 28); layer = <CALayer: 0x16096bb0>>",#"<AppUrlMessageNodeView: 0x160977e0; frame = (0 0; 327 149); layer = <CALayer: 0x16098f50>>",#"<UIView: 0x16098d70; frame = (0 10; 320 28); layer = <CALayer: 0x16099bd0>>",#"<ImageMessageNodeView: 0x1609b000; frame = (179.5 0; 131.5 150); layer = <CALayer: 0x1609a840>>",#"<ImageMessageNodeView: 0x160a3d80; frame = ( // ........省略.................. cy# x.length 419
好吧,果然419个m_view都不是nil。
我想到的原因如下:
这里能看出BaseMsgContentViewController的实现文件是 BaseMsgContentViewController.mm,也就是Objective C++写的。(结合微信内很多变量的命名方式,微信早期的开发者应该有MFC的开发经验)
m_messageNodeClass与下面的方法有关: - (void)initMessageNodeClass;
使用Hopper反编译WeChat的二进制文件:
再反汇编为c代码:
void -[BaseMsgContentViewController initMessageNodeClass](void * self, void * _cmd) { r7 = &arg_C; sp = sp - 0xb4; r11 = self; r6 = *objc_ivar_offset_BaseMsgContentViewController_m_messageNodeClass; r5 = *(r11 + r6); r0 = *(r11 + 0xa4); if (r0 != r5) { do { *(r11 + 0xa4) = r0 - 0x4; r0 = *(r0 + 0xfffffffffffffffc); [r0 release]; r0 = *(r11 + 0xa4); } while (r0 != r5); r6 = *objc_ivar_offset_BaseMsgContentViewController_m_messageNodeClass; } r8 = @selector(class); r0 = [MultiColumnReaderMessageNodeView class]; r7 = r7; r0 = [r0 retain]; r1 = r6 + 0x4; arg_B0 = r0; r2 = r6 + r11; r3 = *(r11 + r1); if (r3 < *(r2 + 0x8)) { arg_B0 = 0x0; *r3 = r0; *(r11 + r1) = *(r11 + r1) + 0x4; } else { void std::__1::vector<objc_class* __strong, std::__1::allocator<objc_class* __strong> >::__push_back_slow_path<objc_class* __strong>(); [arg_B0 release]; } r4 = *objc_ivar_offset_BaseMsgContentViewController_m_messageNodeClass; r0 = [ImageTextReaderMessageNodeView class]; //.....省略......
就是把所有支持的MessageNode的Class都push_back到这个vector中。
手动整理伪代码如下:
std::vector<Class> m_messageNodeClass; m_messageNodeClass.push_back([MultiColumnReaderMessageNodeView class]); m_messageNodeClass.push_back([ImageTextReaderMessageNodeView class]); m_messageNodeClass.push_back([HeadImgReaderMessageNodeaView class]); m_messageNodeClass.push_back([MessageSysNodeView class]); m_messageNodeClass.push_back([AttributedReaderMessageNodeaView class]); m_messageNodeClass.push_back([ReaderNewMessageNodeView class]); m_messageNodeClass.push_back([MultiReaderMessageNodeView class]); m_messageNodeClass.push_back([MailMessageNodeView class]); m_messageNodeClass.push_back([MassSendMessageNodeView class]); m_messageNodeClass.push_back([ImageMessageNodeView class]); m_messageNodeClass.push_back([VoiceMessageNodeView class]); m_messageNodeClass.push_back([ShortVideoMessageNodeView class]); m_messageNodeClass.push_back([VideoMessageNodeView class]); m_messageNodeClass.push_back([ShareCardMessageNodeView class]); m_messageNodeClass.push_back([EmoticonMessageNodeView class]); m_messageNodeClass.push_back([GameMessageNodeView class]); m_messageNodeClass.push_back([VoipContentNodeView class]); m_messageNodeClass.push_back([AppTextMessageNodeView class]); m_messageNodeClass.push_back([AppImageMessageNodeView class]); m_messageNodeClass.push_back([AppEmoticonMessageNodeView class]); m_messageNodeClass.push_back([AppFileMessageNodeView class]); m_messageNodeClass.push_back([AppUrlMessageNodeView class]); m_messageNodeClass.push_back([AppShakeMessageNodeView class]); m_messageNodeClass.push_back([VoiceReminderConfirmNodeView class]); m_messageNodeClass.push_back([VoiceReminderRemindNodeView class]); m_messageNodeClass.push_back([AppProductMessageNodeView class]); m_messageNodeClass.push_back([AppEmoticonSharedMessageNodeView class]); m_messageNodeClass.push_back([AppWCProductMessageNodeView class]); m_messageNodeClass.push_back([AppWCCardMessageNodeView class]); m_messageNodeClass.push_back([AppTVMessageNodeView class]); m_messageNodeClass.push_back([AppTrackRoomMessageNodeView class]); m_messageNodeClass.push_back([AppRecordMessageNodeView class]); m_messageNodeClass.push_back([AppNoteMessageNodeView class]); m_messageNodeClass.push_back([AppHardWareRankMessageNode class]); m_messageNodeClass.push_back([AppHardWareLikeNotifyMessageNode class]); m_messageNodeClass.push_back([MultiTalkMessageNodeView class]); m_messageNodeClass.push_back([WCPayTransferMessageNodeView class]); m_messageNodeClass.push_back([WCPayTransferAcceptedMessageNodeView class]); m_messageNodeClass.push_back([WCPayTransferRejectedMessageNodeView class]); m_messageNodeClass.push_back([WCPayMessageBaseNodeView class]); m_messageNodeClass.push_back([WCPayC2CMessageNodeView class]); m_messageNodeClass.push_back([WCPayC2CFestivalMsgNodeView class]); m_messageNodeClass.push_back([AppDefaultMessageNodeView class]); m_messageNodeClass.push_back([TextMessageNodeView class]);
可见,微信真实个巨大的工程,支持的消息类型这么多(我使用的微信版本:6.3.19)。
随便看个消息,例如: MessageSysNodeView 继承自 BaseMessageNodeView 然后 MMUIView
- (void)preCreateMessageContentNode:(id)arg1; - (void)preCreateMessageSplitNode:(id)arg1; - (void)preCreateMessageTimeNode:(id)arg1;
由这三个preCreateMessage开头的方法,可猜测到 MultiSelectTableViewCell的contentView的第一层子View 存在三类:
Hopper反汇编找到对应代码:
为循序渐进,先研究下TimeNode的preCreate:
由于内部有取arg2.m_view的代码,能基本猜到 arg2是 CMessageNodeData类型。(后面可以用lldb证实)
关键代码行及伪代码大概如下:
void -[BaseMsgContentViewController preCreateMessageTimeNode:](void * self, void * _cmd, void * arg2) { messageNodeData = arg2 if(messageNodeData.m_view == nil){ // 就是填充m_view // 从MMThemeManager获取时间Node的高度 r5 = [[MMThemeManager sharedThemeManager] retain]; [[r5 getValueOfProperty:@"message_node_timeNode_height" inRuleSet:@"#message_node_view"] retain]; UIView *timeRoot = [][UIView alloc]initWithFrame:....]; r11 = [[MMUILabel alloc] init]; // 这是label各种属性 r10 = [[UIImageView alloc] init]; // 设置ImageView各种属性 }
最终就是构成这个:
知道了TimeNode如何preCreate的,那ContentNode就类似了,只是代码更多。
void -[BaseMsgContentViewController preCreateMessageContentNode:](void * self, void * _cmd, void * arg2) { messageNodeData = arg2 if(messageNodeData.m_view == nil){ // 仍然是填充m_view // 判断是否自己发的消息 r5 = [[r11 m_msgWrap] retain]; arg_14 = [CMessageWrap isSenderFromMsgWrap:r5]; 如果是对方消息 r0 = [r8 newMessageNodeViewForMessageWrap:r4 contact:r5 chatContact:STK-1]; 如果是我发送的消息 r0 = [r8 newMessageNodeViewForMessageWrap:r6 contact:0x0 chatContact:STK-1]; 设置m_view //计算frame //GameNode特殊处理 //语音特殊处理 r2 = [VoiceMessageNodeView class]; }
PS:这个方法之前的版本很长,现在的版本进行了优化。新增了newMessageNodeViewForMessageWrap方法。
void * -[BaseMsgContentViewController newMessageNodeViewForMessageWrap:contact:chatContact:](void * self, void * _cmd, void * arg2, void * arg3, void * arg4) { // 这里循环判断vector中的每个类,交给每个类判断是否是自己的类型 r0 = r5->m_messageNodeClass; for(Class in r0){ // 先判断能否创建 r4 = *(r0 + r11 * 0x4); if (([r4 canCreateMessageNodeViewInstanceWithMessageWrap: r2] & 0xff) != 0x0) goto loc_1609718; // 创建 r0 = [r4 alloc]; r4 = arg_8; r6 = arg_4; var_0 = r6; r5 = [r0 initWithMessageWrap:arg_C Contact:r4 ChatContact:STK-1]; } }
看下canCreateMessageNodeViewInstanceWithMessageWrap方法,
char +[BaseMessageNodeView canCreateMessageNodeViewInstanceWithMessageWrap:](void * self, void * _cmd, void * arg2) { return 0x0; }
先看所有NodeView的基类 BaseMessageNodeView,默认返回0x0,也就是NO。(32位下BOOL是char,这里也就是返回个BOOL类型)
再随便找个子NodeView类,例如:MessageSysNodeView
char +[MessageSysNodeView canCreateMessageNodeViewInstanceWithMessageWrap:](void * self, void * _cmd, void * arg2) { r4 = [arg2 retain]; r5 = @selector(m_uiMessageType); if ([r4 m_uiMessageType] == 0x2710) { r5 = 0x1; } else { r0 = [r4 m_uiMessageType]; r5 = 0x0; asm{ it eq }; if (r0 == 0x2712) { r5 = 0x1; } } [r4 release]; r0 = r5; return r0; }
可见。如果 m_uiMessageType(CMessageNodeData的CMessageWrap的成员)是 0x2710或0x2712,则认为是此消息类型。
再看ImageMessageNodeView:
char +[ImageMessageNodeView canCreateMessageNodeViewInstanceWithMessageWrap:](void * self, void * _cmd, void * arg2) { r4 = [arg2 retain]; r5 = @selector(m_uiMessageType); if (([r4 m_uiMessageType] == 0x3) || ([r4 m_uiMessageType] == 0xd)) { r5 = 0x1; } else { r0 = [r4 m_uiMessageType]; r5 = 0x0; asm{ it eq }; if (r0 == 0x27) { r5 = 0x1; } } [r4 release]; r0 = r5; return r0; }
可知 0x3、0xd、0x27 都是图像。
还有很多消息,不一一列出了。
最后再看下 TextMessageNodeView:
char +[TextMessageNodeView canCreateMessageNodeViewInstanceWithMessageWrap:](void * self, void * _cmd, void * arg2) { return 0x1; }
直接返回的YES。可见,如果所有消息都不是的话,则按照文本消息来处理。TextMessageNodeView也正好是最后一个push_back到 m_messageNodeClass中去的。
先看BaseMessageNodeView:
void * -[BaseMessageNodeView initWithMessageWrap:Contact:ChatContact:](void * self, void * _cmd, void * arg2, void * arg3, void * arg4) { 省略……
再看看TextMessageNodeView 的initWithMessageWrap:Contact:ChatContact。 代码或多或少,没有什么关键代码。
就是根据CMessageWrap配置各种View的属性。
下面想办法找到preCreate调用源。
usbmuxd
➜ python-client python tcprelay.py -t 22:2222 1234:1234 Forwarding local port 2222 to remote port 22 Forwarding local port 1234 to remote port 1234 ......
ssh
ssh root@localhost -p 2222
debugserver
everettjfs-iPhone:~ root# debugserver *:1234 -a WeChat debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-320.2.89 for armv7. Attaching to process WeChat... Listening to port 1234 for a connection from *... Waiting for debugger instructions for process 0.
lldb
[everettjf@e ~ ]$ lldb (lldb) process connect connect://localhost:1234 Process 67776 stopped * thread #1: tid = 0x214590, 0x31ef4474 libsystem_kernel.dylib`mach_msg_trap + 20, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x31ef4474 libsystem_kernel.dylib`mach_msg_trap + 20 libsystem_kernel.dylib`mach_msg_trap: -> 0x31ef4474 <+20>: pop {r4, r5, r6, r8} 0x31ef4478 <+24>: bx lr libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x31ef447c <+0>: mov r12, sp 0x31ef4480 <+4>: push {r4, r5, r6, r8}
找到偏移地址
(lldb) image list -o -f [ 0] 0x000e7000 /private/var/mobile/Containers/Bundle/Application/25FB096A-8122-49B5-9304-5FDB9080D9B0/WeChat.app/WeChat(0x00000000000eb000) [ 1] 0x031c7000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x00000000031c7000)
看到image list -o -f后面的偏移地址:0x000e7000
先看下聊天消息界面时默认加载的历史消息。
hopper中找到BaseMsgContentViewController::preCreateMessageContentNode: 的文件偏移地址:0x0160a444
计算出真实偏移地址(我比较喜欢拿ipython当计算器):
In [1]: hex(0x000e7000+0x0160a444) Out[1]: '0x16f1444'
下断点:
(lldb) br s -a 0x16f1444 Breakpoint 1: where = WeChat`___lldb_unnamed_function80337$$WeChat, address = 0x016f1444
然后点击一个会话,进入消息界面。此时会命中断点。
这里既然命中断点了,顺带看一下 preCreateMessageContentNode 的参数类型:
(lldb) po $r0 <BaseMsgContentViewController: 0x17127c00> (lldb) po (char*)$r1 "preCreateMessageContentNode:" (lldb) po $r2 <CMessageNodeData: 0x1789bfb0>
回归正题,bt命令查看调用栈:
(lldb) bt * thread #1: tid = 0x214590, 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat frame #1: 0x016f2516 WeChat`___lldb_unnamed_function80343$$WeChat + 990 frame #2: 0x016f2bfe WeChat`___lldb_unnamed_function80345$$WeChat + 590 frame #3: 0x016f397e WeChat`___lldb_unnamed_function80355$$WeChat + 690 frame #4: 0x01708ac0 WeChat`___lldb_unnamed_function80565$$WeChat + 1416 frame #5: 0x26c54b8e UIKit`-[UIViewController loadViewIfRequired] + 602 frame #6: 0x26c548fc UIKit`-[UIViewController view] + 24 省略
可见这几个方法都是在主线程调用。frame#0就是preCreateMessageContentNode方法。frame #1就是调用preCreateMessageContentNode的方法。我们找下frame#1的方法。 从内存地址 0x016f2516 减去 偏移地址0x000e7000 就得到文件偏移地址:
In [4]: hex(0x016f2516-0x000e7000) Out[4]: '0x160b516'
hopper 中找到这个方法:
找到方法:
就是这个方法:
void -[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:](void * self, void * _cmd, void * arg2, char arg3, char arg4) {
下断点到这个方法的首地址 0x16f2138 = 0x000e7000 + 0x0160b138:(先清掉之前的断点)
In [6]: hex(0x000e7000 + 0x0160b138) Out[6]: '0x16f2138'
(lldb) br l Current breakpoints: 1: address = WeChat[0x0160a444], locations = 1, resolved = 1, hit count = 1 1.1: where = WeChat`___lldb_unnamed_function80337$$WeChat, address = 0x016f1444, resolved, hit count = 1 (lldb) br delete 1 1 breakpoints deleted; 0 breakpoint locations disabled. (lldb) br s -a 0x16f2138 Breakpoint 3: where = WeChat`___lldb_unnamed_function80343$$WeChat, address = 0x016f2138
看下参数:
(lldb) po $r0 <BaseMsgContentViewController: 0x17127c00> (lldb) po (char*)$r1 "addMessageNode:layout:addMoreMsg:" (lldb) po $r2 {m_uiMesLocalID=384, m_ui64MesSvrID=7606243121581773106, m_nsFromUsr=wxi*h12~19, m_nsToUsr=wxi*t21~19, m_uiStatus=2, type=1, msgSource="(null)"} (lldb) po [$r2 class] CMessageWrap (lldb) p $r3 (unsigned int) $13 = 0 (lldb) p $r4 (unsigned int) $14 = 40
也就是 BaseMsgContentViewController addMessageNode:layout:addMoreMsg 方法的第一个参数是 CMessageWrap,layout是0 , addMoreMsg 是40 。
同样的步骤,看下调用栈中的剩余几个方法,汇总到一起就是:
void -[BaseMsgContentViewController preCreateMessageContentNode:](void * self, void * _cmd, void * arg2) { void -[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:](void * self, void * _cmd, void * arg2, char arg3, char arg4) { void -[BaseMsgContentViewController initHistroyMessageNodeData](void * self, void * _cmd) { void -[BaseMsgContentViewController initData](void * self, void * _cmd) { void -[BaseMsgContentViewController viewDidLoad](void * self, void * _cmd) {
利用hopper的反汇编看下这几个方法,我们又找到了,initView等一系列init开头的函数。比如:initTableView 中初始化tableView,并调用了reloadData。(initData在先,initView在后)
仔细看
void -[BaseMsgContentViewController initHistroyMessageNodeData](void * self, void * _cmd) { ... arg_1C = r8; r0 = [r5 GetMessageArray]; r7 = r7;
找到 [r5 GetMessageArray] 这句的汇编代码行 0x0160bb20。
断点到这行,然后输出$r0。
(lldb) br s -a 0x163db20 (这里我换了机器,重新启动了Weixin,内存偏移变为0x00032000,因此hex(0x0160BB20 + 0x00032000)=0x163db20) (lldb) po $r0 <WeixinContentLogicController: 0x1582ad20> (lldb) po (char*)$r1 "GetMessageArray" (lldb) n 省略 (lldb) po $r0 <__NSArrayM 0x1584bf30>( {m_uiMesLocalID=382, m_ui64MesSvrID=4946812604026242266, m_nsFromUsr=wxi*h12~19, m_nsToUsr=wxi*t21~19, m_uiStatus=2, type=1, msgSource="(null)"} , {m_uiMesLocalID=383, m_ui64MesSvrID=145730894416135475, m_nsFromUsr=wxi*h12~19, m_nsToUsr=wxi*t21~19, m_uiStatus=2, type=1, msgSource="(null)"} , 省略 ) (lldb) po [[$r0 firstObject]class] CMessageWrap
单步执行后,也可以看返回值$r0,也就是所有消息CMessageWrap。
可知是WeixinContentLogicController类, 看下这个类:
@interface WeixinContentLogicController : BaseMsgContentLogicController
hopper看下WeixinContentLogicController的GetMessageArray方法,发现找不到。那就是在父类BaseMsgContentLogicController中。
- BaseMsgContentLogicController GetMessageArray
内部又调用了 WeixinContentLogicController GetMsg:FromID:Limit:LeftCount:LeftUnreadCount:
- WeixinContentLogicController GetMsg:FromID:Limit:LeftCount:LeftUnreadCount: r0 = [MMServiceCenter defaultCenter]; arg_30 = 0xffffffff; r0 = [r0 retain]; arg_24 = r0; arg_30 = 0x2; r2 = [CMessageMgr class]; arg_30 = 0x3; r0 = [arg_24 getService:r2]; arg_30 = 0xffffffff; arg_28 = [r0 retain]; [arg_24 release]; arg_30 = 0x4; asm{ stm.w sp, {r3, r5, r6} }; r0 = [arg_28 GetMsgByCreateTime:arg_20 FromID:arg_1C FromCreateTime:STK1 Limit:STK0 LeftCount:STK-1];
大概就是 从 MMServiceCenter 获取到CMessageMgr,然后调用 CMessageMgr的GetMsgByCreateTime:arg_20 FromID:arg_1C FromCreateTime:STK1 Limit:STK0 LeftCount:STK-1 方法。
有两个方法:
- (id)GetMsgByCreateTime:(id)arg1 FromID:(unsigned long)arg2 FromCreateTime:(unsigned long)arg3 Limit:(unsigned long)arg4 LeftCount:(unsigned int *)arg5; - (id)GetMsgByCreateTime:(id)arg1 FromID:(unsigned long)arg2 FromCreateTime:(unsigned long)arg3 Limit:(unsigned long)arg4 LeftCount:(unsigned int *)arg5 FromSequence:(unsigned long)arg6;
第一个会调用第二个带FromSequence的方法,hopper看下第二个方法:
r0 = *objc_ivar_offset_CMessageMgr_m_oMsgDB; r2 = *(r7 + 0x14); r0 = *(r6 + r0); arg_C = r2; arg_4 = r5 + 0x5; r5 = *(r7 + 0x10); arg_8 = r5; var_0 = r8; r0 = [r0 GetMsgByCreateTime:r10 FromID:arg_24 FromCreateTime:STK2 Limit:STK1 LeftCount:STK0 FromSequence:STK-1]; r7 = r7; r4 = [r0 retain]; r1 = @selector(HandleMsgList:MsgList:); [r6 HandleMsgList:r2 MsgList:STK3];
objc_ivar_offset_CMessageMgr_m_oMsgDB 就是 CMessageDB *m_oMsgDB; 也就是调用了 CMessageDB的GetMsgByCreateTime:r10
PS: > 在hopper中能看到不少日志信息,而且写明了当前实现文件的文件名。 后缀是.mm,当然不止这一个,微信好多类都是Objective C++实现的。包括消息主界面的 BaseMsgContentViewController.mm,以及下面CMessageMgr中的很多类。(再次猜测,微信的初期开发人员不少做Windows下C++开发客户端的哈。C开头的类……)
这个CMessageMgr也是Objective C++开发 。不过hopper能看出 GetMsgByCreateTime: 内部调用了
int -[CMessageDB GetMsg:Where:order:Limit:](int arg0) {
内部又调用:
r11 = *objc_ivar_offset_CMessageDB_m_oMMDB; r0 = *(r6 + r11); r3 = *0x26b20d8; asm{ stmeq.w sp, {r4, r10} }; arg_8 = r5; r5 = r8; r0 = [r0 GetMessagesByChatName:r5 onProperty:r3 where:STK1 order:STK0 limit:STK-1];
调用了成员CMMDB的 GetMessagesByChatName方法。
@interface CMessageDB : NSObject { CMMDB *m_oMMDB; }
CMMDB的 GetMessagesByChatName方法内部如下:
res = [arg0 GetMessageTable:r11]; r0 = [res getObjectsWhere:r10 onProperties:r4 orderBy:STK0 limit:STK-1];
也就是对 CMMDB::GetMessageTable 的返回值调用了getObjectsWhere方法。
void * -[CMMDB GetMessageTable:](void * self, void * _cmd, void * arg2) { r4 = [[CMMDB messageTableName:arg2] retain]; r5 = [[self m_db] retain]; r3 = [DBMessage class]; r6 = [[r5 getTable:r4 withClass:r3] retain]; r0 = loc_215a20c(r6, @selector(getTable:withClass:));
调用了 m_db( WCDataBase *m_db;) 的 getTable:withClass方法。再看进入 就是返回 WCDataBaseTable类型。
看看CMMDB的头文件
@interface CMMDB : NSObject <WCDataBaseEventDelegate> { NSRecursiveLock *m_lockMMDB; NSMutableSet *m_setMessageCreatedTable; NSMutableSet *m_setMessageExtCreatedTable; OpLogDB *m_oplogWcdb; WCDataBase *m_db; WCDataBaseTable *m_tableContact; WCDataBaseTable *m_tableContactExt; WCDataBaseTable *m_tableContactMeta; WCDataBaseTable *m_tableQQContact; WCDataBaseTable *m_tableSendMsg; WCDataBaseTable *m_tableUploadVoice; WCDataBaseTable *m_tableDownloadVoice; WCDataBaseTable *m_tableRevokeMsg; WCDataBaseTable *m_tableEmoticon; WCDataBaseTable *m_tableEmoticonUpload; WCDataBaseTable *m_tableEmoticonDownload; WCDataBaseTable *m_tableEmoticonPackage; WCDataBaseTable *m_tableBottle; WCDataBaseTable *m_tableBottleContact; WCDataBaseTable *m_tableMassSendContact; }
就是根据要获取的表类型(这里是DBMessage class)获取到对应的WCDataBaseTable实例,用来操作某个表。
PS: WCDataBase 就是对sqlite的封装了。
@interface WCDataBase : NSObject <WCDBCorruptReportInterface, WCDBHandlesPoolProtocol> { WCDBHandlesPool *m_handlesPool; struct sqlite3 *m_dbHandle; NSData *m_dbEncryptKey; BOOL m_isMemoryOnly; NSString *m_nsDBPath; NSString *m_nsDBFilePath; NSString *m_nsDBName; NSRecursiveLock *m_oLock; unsigned int m_databaseID; unsigned int m_initTime; id <WCDataBaseEventDelegate> m_eventDelegate; WCDBCorruptReport *m_corruptReport; }
进一步跟踪调用,会到:
int -[WCDataBase getObjectsOfClass:fromTable:onProperties:where:orderBy:limit:getError:](? arg0) {
这里就是对sqlite的本地查询了。
就到这里吧,知道了大体流程。但貌似有个问题,这一溜下来都是在主线程干的事情,不过看来足够快了。
PS:微信的本地sqlite数据库设计及Objective C++的封装有时间可以学习下。
上面找到了首次打开聊天界面时加载历史聊天消息的调用栈。
我还想知道,在会话界面时,新消息到来时的调用栈。那就进入聊天界面后,再下载断点,然后用另一个手机给这个账号发消息(自己发也行啊),然后看调用栈。
首先进入聊天消息页面,然后再次下断点到 preCreateMessageContentNode方法。
(lldb) br s -a 0x16f1444 Breakpoint 4: where = WeChat`___lldb_unnamed_function80337$$WeChat, address = 0x016f1444 (lldb) c error: Process is running. Use 'process interrupt' to pause execution. Process 67776 stopped * thread #1: tid = 0x214590, 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1 frame #0: 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat WeChat`___lldb_unnamed_function80337$$WeChat: -> 0x16f1444 <+0>: push {r4, r5, r6, r7, lr} 0x16f1446 <+2>: add r7, sp, #0xc 0x16f1448 <+4>: push.w {r8, r10, r11} 0x16f144c <+8>: sub.w r4, sp, #0x20 (lldb) bt * thread #1: tid = 0x214590, 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1 * frame #0: 0x016f1444 WeChat`___lldb_unnamed_function80337$$WeChat frame #1: 0x016f2516 WeChat`___lldb_unnamed_function80343$$WeChat + 990 frame #2: 0x018df210 WeChat`___lldb_unnamed_function87462$$WeChat + 472 frame #3: 0x018df44e WeChat`___lldb_unnamed_function87463$$WeChat + 398 frame #4: 0x01f6e3a2 WeChat`___lldb_unnamed_function115325$$WeChat + 1242 frame #5: 0x2433e5ce Foundation`__NSThreadPerformPerform + 386 frame #6: 0x235c5fae CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14
采用上面的方法可获得调用栈:
void -[BaseMsgContentViewController preCreateMessageContentNode:](void * self, void * _cmd, void * arg2) { void -[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:](void * self, void * _cmd, void * arg2, char arg3, char arg4) { void -[BaseMsgContentLogicController DidAddMsg:](void * self, void * _cmd, void * arg2) { void -[BaseMsgContentLogicController OnAddMsg:MsgWrap:](void * self, void * _cmd, void * arg2, void * arg3) { void -[CMessageMgr MainThreadNotifyToExt:](void * self, void * _cmd, void * arg2) { __NSThreadPerformPerform __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
根据调用栈,大体得知 CMessageMgr MainThreadNotifyToExt 分发出消息。又到了CMessageMgr类。
上一个方法是__NSThreadPerformPerform,可知是从其他线程使用perform过来的。(perform到主线程会加入到主线程的RunLoop中)
看看MainThreadNotifyToExt的参数。断点到第一行代码:
lldb查看:
(lldb) po $r0 po<CMessageMgr: 0x147afbe0> (lldb) po $r0 <CMessageMgr: 0x147afbe0> (lldb) po (char*)$r1 "MainThreadNotifyToExt:" (lldb) po $r2 { 1 = 1; 2 = "wxid_pamzqdzakikt21"; 3 = "{m_uiMesLocalID=394, m_ui64MesSvrID=8508546064571928607, m_nsFromUsr=wxi*t21~19, m_nsToUsr=wxi*h12~19, m_uiStatus=3, type=1, msgSource=/"/"} "; } (lldb) po [$r2 class] __NSDictionaryM (lldb) po [[$r2 objectForKey:@"3"] class] CMessageWrap (lldb) po [[$r2 objectForKey:@"2"] class] __NSCFString (lldb) po [[$r2 objectForKey:@"1"] class] __NSCFString
可知参数是个NSDictionary,key分别为字符串1 2 3,分别是 NSString NSString 以及CMessageWrap。
可知CMessageWrap是后台线程准备好的。
hopper能能看到大体流程:
center = [MMServiceCenter defaultCenter] service = getService:[MMExtensionCenter class] IMsgExt ext = service getExtension:[IMsgExt class] 然后使用IMsgExt分发消息。
IMsgExt协议如下:
@protocol IMsgExt @optional - (void)OnAddMsg:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnAddMsgForSpecialSession:(NSString *)arg1 MsgList:(NSArray *)arg2; - (void)OnAddMsgListForSession:(NSDictionary *)arg1 NotifyUsrName:(NSSet *)arg2; - (void)OnBeginDownloadAppData:(CMessageWrap *)arg1; - (void)OnBeginDownloadImage:(CMessageWrap *)arg1; - (void)OnBeginDownloadVideo:(CMessageWrap *)arg1; - (void)OnChangeMsg:(NSString *)arg1 OpCode:(unsigned long)arg2; - (void)OnDelMsg:(NSString *)arg1; - (void)OnDelMsg:(NSString *)arg1 DelAll:(BOOL)arg2; - (void)OnDelMsg:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnGetNewXmlMsg:(NSString *)arg1 Type:(NSString *)arg2 MsgWrap:(CMessageWrap *)arg3; - (void)OnModMsg:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgDownloadAppAttachExpiredFail:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgDownloadThumbFail:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgDownloadThumbOK:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgDownloadVideoExpiredFail:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgNotAddDBNotify:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnMsgNotAddDBSession:(NSString *)arg1 MsgList:(NSArray *)arg2; - (void)OnPreAddMsg:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; - (void)OnReceiveSight:(CMessageWrap *)arg1; - (void)OnRevokeMsg:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2 ResultCode:(unsigned long)arg3 ResultMsg:(NSString *)arg4 EducationMsg:(NSString *)arg5; - (void)OnSendSight:(NSString *)arg1; - (void)OnShowPush:(CMessageWrap *)arg1; - (void)OnUnReadCountChange:(NSString *)arg1; - (void)OnUpdateVideoStatus:(NSString *)arg1 MsgWrap:(CMessageWrap *)arg2; @end
具体细节就不继续分析了。大体知道了与UI相关的流程。
微信这种把消息的view 预创建到实体中,且不销毁。不销毁的意思是:退出界面会话时不会销毁;不断的下拉消息会不断的创建。一眼感觉不太考虑,看看微信内存占用情况。
首先,把微信进程结束后,重新打开。
在这种状态下看看内存:
RSIZE=52M
然后,进入聊天界面:
RSIZE=56M
然后,使劲发消息(图片、文字各种消息),400多条,全部下拉下来。
RSIZE=81M
这样看来,由于在一个会话中打开很多消息的概率较少,且内存占用还是可接受的。占用81M感觉还是比较少的。看来这种方案还是比较靠谱的。
这种方案也有效率上的优势,就是不需要
微信会对ViewController进行缓存,也就是对同一个用户的消息打开两次,ViewController的地址是相同的。
应该会有个缓存策略,有空研究研究。
要支持IM界面的多种类型消息展示,首先想到的肯定是使用多种Cell。例如:TextCell, ImageCell 等。经典的QQ,其实就是这种方式。可以用Reveal看看。
如果采用QQ这种使用Cell的方案,有个UI上的细节问题要注意。见这篇文章。
根据微信上面的消息界面实现,我实现了一个很简单的类似机制的界面Demo https://github.com/everettjf/WeChatLikeMessageDemo 。
实现过程中发现这种机制有个好处,就是在preCreate消息时,可以提前(在heightForRowAtIndexPath之前)知道cell的高度,也就很方便的解决了Cell动态高度这个问题。
逆向可以让我们了解一个App的实现方法(尤其是优秀未开源的App哈),学习这些优秀的App可以辅助正向开发。
推荐《iOS应用逆向工程》这本书,以及 http://iosre.com 论坛。