恢复Mac版QQ一些隐藏的小功能
零、前言
最近使用Mac版QQ时弹出了一个版本更新提示,说最新版v5.1.2
正式版已在官网发布,于是我便更新了。
不过更新完我就后悔了,因为我发现5.x
版本有些功能用不了:
1、不能选择忙碌状态:
要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。
2、不能发送本地图片,只能发送文件:
从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。
看来,我只能自己给给QQ加上这两个功能了。
一、恢复忙碌状态选项
用Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:
可以很明显地看出,菜单列表里有一个忙碌
的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。
因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。
那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是OnlineStateImagePopUpButton
,父类为NSPopUpButton
,查看NSPopUpButton
类的头文件,可以发现该类有一个itemArray
的属性,能够获取到菜单列表:
1 2 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.h1 2 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]
方法,代码如下所示:
1 2 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.h1 2 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]
方法的伪代码:
1 2 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.m1 2 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.m1 2 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
方法就能获取到按钮的数量了。
最后的示例代码如下:
1 2 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下载。
很惭愧,就做了一点微小的工作,谢谢大家。