 
      
    
    
      
        恢复Mac版QQ一些隐藏的小功能
      
      
    
   
  
  零、前言
最近使用Mac版QQ时弹出了一个版本更新提示,说最新版v5.1.2正式版已在官网发布,于是我便更新了。
不过更新完我就后悔了,因为我发现5.x版本有些功能用不了:
1、不能选择忙碌状态:

要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。
2、不能发送本地图片,只能发送文件:

从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。
看来,我只能自己给给QQ加上这两个功能了。
一、恢复忙碌状态选项
用Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:

可以很明显地看出,菜单列表里有一个忙碌的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。
因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。
那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是OnlineStateImagePopUpButton,父类为NSPopUpButton,查看NSPopUpButton类的头文件,可以发现该类有一个itemArray的属性,能够获取到菜单列表:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | @interface NSPopUpButton : NSButton
 
 @property (readonly, copy) NSArray<NSMenuItem *> *itemArray;
 @property (readonly) NSInteger numberOfItems;
 
 
 
 @end
 
 | 
也就是说,如果能获取到状态按钮对象的话,就能获取到菜单列表了。由图片可知,状态按钮在MQAIOSelfInfoViewController2视图控制器里。
用class-dump获取QQ的头文件,找到MQAIOSelfInfoViewController2.h,部分内容如下:
MQAIOSelfInfoViewController2.h| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | @interface MQAIOSelfInfoViewController2 : NSViewController <NSMenuDelegate>{
 unsigned long long _status;
 NSButton *_avatarButton;
 NSPopUpButton *_statusPopUpButton;
 }
 
 
 
 @end
 
 | 
 
可以看到该视图控制器有一个_statusPopUpButton属性,看名字基本可以肯定是状态按钮了。
那么我们可以hook该视图控制器的viewDidLoad方法,这时状态按钮已经创建完毕,通过遍历状态按钮的菜单列表,将每个菜单的hidden属性设为NO。
二、创建插件工程
按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为QQPlugin:

然后hook-[MQAIOSelfInfoViewController2 viewDidLoad]方法,代码如下所示:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | @implementation NSObject (MQAIOSelfInfoViewController2BusyStatus)
 + (void)busyStatus_MQAIOSelfInfoViewController2
 {
 [self jr_swizzleMethod:@selector(viewDidLoad)
 withMethod:@selector(busyStatus_viewDidLoad)
 error:NULL];
 }
 
 - (void)busyStatus_viewDidLoad
 {
 [self busyStatus_viewDidLoad];
 
 NSPopUpButton *popUpButton = [self valueForKey:@"_statusPopUpButton"];
 for (NSMenuItem *menuItem in popUpButton.itemArray) {
 menuItem.hidden = NO;
 }
 }
 
 @end
 
 | 
编译后重新运行QQ,已经可以看到忙碌状态菜单了:

选择忙碌状态后,登录另一个QQ查看我当前的状态,确实变成忙碌了:

三、恢复发送图片功能
用Interface Inspector定位到聊天框的工具栏,一共有7个按钮,这些按钮都没有设置隐藏属性:

可以看到聊天界面的工具栏也是7个按钮:

也就是说,如果要增加发送图片的按钮的话,就只能自己创建一个新的按钮,再加到工具栏上面了。
至于点击按钮会调用什么方法,可以通过逆向旧版本的QQ来获得。下面介绍一种方法,可以比较快速获取到按钮调用的方法:
首先下载4.x版本的QQ,然后打开插件工程,按command + shift + ,快捷键打开Edit scheme界面,Executable那一项选择4.x版本QQ的路径:

然后再按command + R运行插件工程,Xcode会自动运行QQ。
登录QQ后随便进入一个聊天窗口,接着在Xcode的debug工具栏里点击界面调试按钮:

当视图层次界面加载完毕之后,在层次界面里选中QQ聊天框工具栏的发送图片按钮:

在Xcode的右侧边栏可以看到该按钮的属性:

可以看到按钮调用的方法是-[MQAIOChatTootKitViewController onPicture:]。
查看MQAIOChatTootKitViewController.h文件,可以看到如下方法:
MQAIOChatTootKitViewController.h| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | @interface MQAIOChatTootKitViewController : NSViewController
 
 
 @property(readonly) MQFaceButton *faceBtn;
 @property(readonly) TXHoverButton *grabBtn;
 @property(readonly) TXHoverButton *shakeBtn;
 @property(readonly) TXHoverButton *pictureBtn;
 @property(readonly) TXHoverButton *historyBtn;
 @property(readonly) TXHoverButton *disruptBtn;
 @property(readonly) MQSwitchButton *switchBtn;
 
 - (void)onFaceButtonClick;
 - (void)onGrabScreen:(id)arg1;
 - (void)onShakeWindow:(id)arg1;
 - (void)onPicture:(id)arg1;
 - (void)onBlock:(id)arg1;
 - (void)onMsgRecord:(id)arg1;
 - (void)onClickSwitchButton:(id)arg1;
 
 
 
 @end
 
 | 
 
可以猜想pictureBtn就是发送图片的按钮。
在5.x版QQ的头文件里也可以看到这些方法,把5.x版本QQ的可执行文件拖到Hopper里分析,然后查看-[MQAIOChatTootKitViewController pictureBtn]方法的伪代码:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | void * -[MQAIOChatTootKitViewController pictureBtn](void * self, void * _cmd) {rbx = self;
 rax = rbx->_pictureBtn;
 if (rax == 0x0) {
 r12 = *_OBJC_IVAR_$_MQAIOChatTootKitViewController._pictureBtn;
 r13 = *objc_msgSend;
 rax = [rbx buttonOfClass:[TXHoverButton class]];
 rax = [rax retain];
 *(rbx + r12) = rax;
 [rbx setConstraintsForButton:rax];
 [*(rbx + r12) setTarget:rbx];
 [*(rbx + r12) setAction:@selector(onPicture:)];
 [*(rbx + r12) setToolTip:[[NSBundle mainBundle] localizedStringForKey:@"Send pictures" value:@"" table:0x0]];
 [*(rbx + r12) setNormalImage:[NSImage imageNamed:@"toolbar_pictures_normal"]];
 [*(rbx + r12) setHoverImage:[NSImage imageNamed:@"toolbar_pictures_hover"]];
 [*(rbx + r12) setAlternateImage:[NSImage imageNamed:@"toolbar_pictures_down"]];
 rax = *(rbx + r12);
 }
 return rax;
 }
 
 | 
可以看到该方法用懒加载的方式创建了一个按钮,也就是说代码还是保留的,只是没使用而已。那么我们可以先看其它按钮是怎么创建的,就看历史记录按钮好了。
按照《Hopper Disassembler批量导出反编译的伪代码》里的方法,反编译出MQAIOChatTootKitViewController类所有方法的伪代码。
打开~/ClassDecompiles/QQ/MQAIOChatTootKitViewController.m文件,搜索historyBtn,可以很快发现以下代码:
MQAIOChatTootKitViewController.m| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | void -[MQAIOChatTootKitViewController setupUI:](void * self, void * _cmd, int arg2) {
 
 
 loc_100086744:
 if (LODWORD(r13) != 0x80) goto loc_100086dad;
 rax = (r12)(r15, @selector(fileBtn));
 r14 = var_A0;
 (r12)(r14, @selector(addSubview:), rax);
 rax = (r12)(r15, @selector(historyBtn));
 (r12)(r14, @selector(addSubview:), rax);
 rsi = r15->_fileBtn;
 rax = _NSDictionaryOfVariableBindings(@"_fileBtn, _historyBtn", rsi);
 var_A8 = rax;
 rax = (r12)(NSLayoutConstraint, @selector(constraintsWithVisualFormat:options:metrics:views:), @"V:|-(top)-[_fileBtn]", 0x0, var_98, rax);
 (r12)(r14, @selector(addConstraints:), rax);
 rbx = (r12)(r15, @selector(historyBtn));
 rax = (r12)(r15, @selector(fileBtn));
 rax = (r12)(r15, @selector(topAlignView:andView:), rbx, rax);
 (r12)(r14, @selector(addConstraint:), rax);
 rdi = NSLayoutConstraint;
 rdx = @"H:|-(leading)-[_fileBtn]-(gap1)-[_historyBtn]-(>=gap3)-|";
 LODWORD(rcx) = 0x0;
 rsi = @selector(constraintsWithVisualFormat:options:metrics:views:);
 r8 = var_98;
 
 
 
 }
 
 | 
 
可以看到这些按钮是用自动布局的方法写的,因此可以考虑hook-[MQAIOChatTootKitViewController setupUI:]方法,在这些按钮添加完毕后,把发送图片的按钮加到最后面。
要先知道前面按钮的数量,才能计算出最后一个按钮的位置。考虑到不同的聊天框可能会出现不同数量的按钮,所以不能写死7个。那么怎么才能获取到这些按钮的数量呢?
看一下前面出现过的图片:

可以看到这些按钮都是加在MQEventForwardView类的实例对象里的,在MQAIOChatTootKitViewController.m文件里搜索MQEventForwardView,可以发现以下代码:
MQAIOChatTootKitViewController.m| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | void * -[MQAIOChatTootKitViewController init](void * self, void * _cmd) {var_28 = self;
 var_20 = *0x1011d6e70;
 r14 = @selector(init);
 rbx = [[var_28 super] init];
 if (rbx != 0x0) {
 r15 = *objc_msgSend;
 r14 = [[[MQEventForwardView alloc] init] autorelease];
 [r14 setTranslatesAutoresizingMaskIntoConstraints:0x0];
 [r14 setDelegate:rbx];
 [rbx setView:r14];
 MQRegisterNotificationInMainThread(@"kMQGroupEventNotification+weqe", rbx, @selector(handleGroupEventNotification:));
 MQRegisterNotificationInMainThread(@"kMQDiscussEventNotification+dfsdf", rbx, @selector(handleDiscussEventNotification:));
 MQRegisterNotificationInMainThread(@"kMQContactIMInfoEvtNotification", rbx, @selector(handleIMInfoEventNotification:));
 }
 rax = rbx;
 return rax;
 }
 
 | 
 
也就是说,当试图控制器初始化的时候,就将MQEventForwardView实例对象赋值给了view属性。那么使用self.view.subviews.count方法就能获取到按钮的数量了。
最后的示例代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | @implementation NSObject (MQAIOChatTootKitViewControllerSendPicture)
 + (void)sendPicture_MQAIOChatTootKitViewController
 {
 [self jr_swizzleMethod:@selector(setupUI:)
 withMethod:@selector(sendPicture_setupUI:)
 error:NULL];
 }
 
 - (void)sendPicture_setupUI:(int)arg1
 {
 [self sendPicture_setupUI:arg1];
 
 MQAIOChatTootKitViewController *vc = (MQAIOChatTootKitViewController *)self;
 
 NSInteger buttonCount = vc.view.subviews.count;
 
 NSButton *pictureBtn = (NSButton *)vc.pictureBtn;
 [vc.view addSubview:pictureBtn];
 
 NSDictionary *metrics = @{
 @"left" : @(20 + buttonCount * 40),
 @"top"  : @(10),
 };
 NSDictionary *views = NSDictionaryOfVariableBindings(pictureBtn);
 
 [vc.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[pictureBtn]"
 options:0
 metrics:metrics
 views:views]];
 [vc.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[pictureBtn]"
 options:0
 metrics:metrics
 views:views]];
 }
 
 @end
 
 | 
编译后重启QQ,可以看到出现了发送图片按钮:

点击后也确实可以发送图片了。
插件工程可以在QQPlugin下载。
很惭愧,就做了一点微小的工作,谢谢大家。