抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

零、前言

最近使用Mac版QQ时弹出了一个版本更新提示,说最新版v5.1.2正式版已在官网发布,于是我便更新了。

不过更新完我就后悔了,因为我发现5.x版本有些功能用不了:

1、不能选择忙碌状态:
image
要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。

2、不能发送本地图片,只能发送文件:
image
从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。

看来,我只能自己给给QQ加上这两个功能了。

一、恢复忙碌状态选项

Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:
image
可以很明显地看出,菜单列表里有一个忙碌的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。

因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。

那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是OnlineStateImagePopUpButton,父类为NSPopUpButton,查看NSPopUpButton类的头文件,可以发现该类有一个itemArray的属性,能够获取到菜单列表:

1
2
3
4
5
6
7
8
9
@interface NSPopUpButton : NSButton

// Accessing the items
@property (readonly, copy) NSArray<NSMenuItem *> *itemArray;
@property (readonly) NSInteger numberOfItems;

//......

@end

也就是说,如果能获取到状态按钮对象的话,就能获取到菜单列表了。由图片可知,状态按钮在MQAIOSelfInfoViewController2视图控制器里。

class-dump获取QQ的头文件,找到MQAIOSelfInfoViewController2.h,部分内容如下:

MQAIOSelfInfoViewController2.h
1
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
image

然后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,已经可以看到忙碌状态菜单了:
image

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

三、恢复发送图片功能

Interface Inspector定位到聊天框的工具栏,一共有7个按钮,这些按钮都没有设置隐藏属性:
image

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

也就是说,如果要增加发送图片的按钮的话,就只能自己创建一个新的按钮,再加到工具栏上面了。

至于点击按钮会调用什么方法,可以通过逆向旧版本的QQ来获得。下面介绍一种方法,可以比较快速获取到按钮调用的方法:

首先下载4.x版本的QQ,然后打开插件工程,按command + shift + ,快捷键打开Edit scheme界面,Executable那一项选择4.x版本QQ的路径:
image
然后再按command + R运行插件工程,Xcode会自动运行QQ。

登录QQ后随便进入一个聊天窗口,接着在Xcode的debug工具栏里点击界面调试按钮:
image

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

在Xcode的右侧边栏可以看到该按钮的属性:
image
可以看到按钮调用的方法是-[MQAIOChatTootKitViewController onPicture:]

查看MQAIOChatTootKitViewController.h文件,可以看到如下方法:

MQAIOChatTootKitViewController.h
1
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.m
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
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个。那么怎么才能获取到这些按钮的数量呢?

看一下前面出现过的图片:
image
可以看到这些按钮都是加在MQEventForwardView类的实例对象里的,在MQAIOChatTootKitViewController.m文件里搜索MQEventForwardView,可以发现以下代码:

MQAIOChatTootKitViewController.m
1
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,可以看到出现了发送图片按钮:
image
点击后也确实可以发送图片了。

插件工程可以在QQPlugin下载。

很惭愧,就做了一点微小的工作,谢谢大家。

评论