前言
通过 Mac逆向工程-入门 文章我们对Mac逆向应用有了一定的了解,如果想对Mac上的应用做一些UI修改,可以通过[动态库注入]的方式修改代码就能达到自己的需求。
这次在Sourcetree应用中加入了2个功能。
- 在Sourcetree中的review code仓库页面中的Toolbar上加入几个按钮,替代CustomAction中的快捷键(见图一)。
- 在点击pull(拉取)按钮时,自动勾选用变基替换合并 CheckBox(见图二)。
分析
问题1:在Sourcetree中的review code仓库页面中的加入几个按钮???
既然是修改UI元素,我们首先想到的是借助Interface Inspector工具界面分析。
首先我们进入到Sourcetree的review code仓库页面,通过Interface Inspector查看UI元素。
我们从图中可以看到有以下信息我们值得去参考和使用:
STGitWindowController 是这个页面的class。
STGitWindowController的class上的window是STDimmableWindow。
STDimmableWindow上有个NSToolbar。
NSToolbar上存放的是我们的点击按钮。
item之间的间距是通过NSToolbarSpaceItem组件分割的。
点击NSToolbar上的item,可以查看到item的icon size占据的大小(图中看不出)。
问题2:在点击pull(拉取)按钮时,自动勾选用变基替换合并???
既然是修改代码,我们首先想到的是通过Hopper 这个反编译工具去修改某个值,现在默认是没有勾选的,修改一个bool值改为YES,不就能自动勾选了吗?
分析:
1、首先得找到点击pull(拉取)事件。
如何获取???
我们可以通过:
1> 动态跟踪(lldb),根据我们点击的过程是追踪到执行的事件。
2> 通过页面的class 在反编译Hopper工具分析出来的方法去<猜>。
我首先想用到的是以猜的方式去找这个点击的事件,既然是pull,那我直接在STGitWindowController class 中搜索pull,经过搜索果然有这个方法,查看之。
查看源码并分析,点击pull按钮后,弹出的页面是STGitPullsheetController,搜索STGitPullsheetController 的rebase关键字。我为什么会搜索rebase 关键字,是因为勾选用变基替换合并的变基英文单词是rebase。
通过图片中我们知道rebase的set方法默认是0x0,这个rebase可以得出是存放的bool值,我们把get方法的值改为0x1,这样不就OK了吗?
尝试修改并实践。。。。最后得出结论,果然是修改这个值。
(如何修改请看 Mac逆向工程-入门篇 )。
项目搭建(在入门篇已讲过)
创建一个dylib的Xcode LVCustomSourceTree 项目,选择macOS Library,点击创建,Type选择Dynamic。我们这里选择的是一个动态库的模板MonkeyAppMac,
这个模板的作用是帮我们的应用做了签名,具体的看 Mac逆向工程-入门篇 。
通过<问题1>的分析之后,我们需要勾住STGitPullsheetController 这个class,然后在windowDidLoad执行完之后去重新绘制我们的Toolbar。
这里我们借助了第三方Aspect去hook住STGitPullsheetController 的windowDidLoad方法,代码如下:
NSError *error;
[objc_getClass("STGitWindowController") aspect_hookSelector:@selector(windowDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"arguments->%@", aspectInfo.arguments);
NSLog(@"instance->%@\n", aspectInfo.instance);
} error:&error];
hook住之后,我们需要绘制我们需要的UI。
- (void)lv_addMyCustomToolBar:(NSWindowController *)window {
self.lvcurrentWindowController = window;
NSWindow *dimmableWindow = window.window;
NSToolbar *toolBar = dimmableWindow.toolbar;
for (NSToolbarItem *item in toolBar.visibleItems) {
[self.lvtoolBarItems addObject:item];
[self.lvitemIdentifiers addObject:item.itemIdentifier];
}
// NSToolbarFlexibleSpaceItem // 伸缩空间
// NSToolbarSpaceItem // 间距
[self.lvitemIdentifiers insertObject:@"NSToolbarSpaceItem" atIndex:10]; // 加入间隔
// 加入NSToolbarItem标识符
[self.lvitemIdentifiers insertObject:@"lvopenWorkSpaceItemIdentifier" atIndex:11];
[self.lvitemIdentifiers insertObject:@"lvnoUpdateItemIdentifier" atIndex:12];
[self.lvitemIdentifiers insertObject:@"lvinstallItemIdentifier" atIndex:13];
[self.lvitemIdentifiers insertObject:@"lvrebaseUpstreamItemIdentifier" atIndex:14];
[self.lvitemIdentifiers insertObject:@"lvrebaseContinueItemIdentifier" atIndex:15];
[self.lvitemIdentifiers insertObject:@"lvremotesItemIdentifier" atIndex:16];
[self.lvitemIdentifiers insertObject:@"lvtagsItemIdentifier" atIndex:17];
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"LVToolbarIdentifier"]; // 新建一个Toolbar 替换掉原来的NSToolbar
self.lvtoolBar = toolbar;
[self.lvtoolBar setDelegate:[LVCustomSourceTree lvsharedSourceTree]]; // 使用单列对象
[dimmableWindow setToolbar:self.lvtoolBar]; // 替换掉原来的
}
这里介绍下在Mac上 NSToolbar绘制item是通过delegate去绘制的。
代码解释:
1、由于我们保留原来的item,所以我们先去获取到原来的item,并用lvtoolBarItems数组保存,由于每一个item必须是要有个标识符,所以我们用lvitemIdentifiers来存储item的标识符。
2、存储完之前的item,加入我们自定义的item标识符。
3、设置代理,这里我们使用的代理对象是通过单例去设置的。(保证唯一性)
4、在NSToolbar的代码去绘制自定义的item。
#pragma NSToolbarDelegate
// 所有的item 标识
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
return self.lvitemIdentifiers;
}
// 实际显示的item 标识
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
return self.lvitemIdentifiers;
}
// 根据item 标识 返回每个具体的NSToolbarItem对象实例
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag {
NSToolbarItem *toolBarItem = [self lv_oldToolBarItem:itemIdentifier];
if (toolBarItem) {
return toolBarItem;
} else {
return [self lv_customToolbarItem:itemIdentifier];
}
}
通过上面的代码并运行发现我们的程序确实已经修改了,但点击事件没有执行。
如何执行自定义action的事件????
改代码,我们第一个想到的是Hoper去分析代码。
既然是执行自定义的action,直接搜索customaction。
如下图:
发现SourceTreeAppDelegate performCustomAction方法跟我们执行得方法很像。
继续搜索这个performCustomAction ,发现STRepoWindowController performCustomAction STRepoWindowController这个class用到了,点击这个方法并分析,看到这个STRepoWindowController class确实用到了SourceTreeAppDelegate performCustomAction:方法,但我们的当前页面class是STGitWindowController。
细想?????
这个STRepoWindowController class的方法可以在 STGitWindowController中使用,第一个想到的就是STRepoWindowController是STGitWindowController的基类,这样才能去执行在子类调用父类的方法。
通过代码查看:
NSWindowController *window = (NSWindowController *)aspectInfo.instance;
NSLog(@"classs:%@",[window superclass]);
验证了我们的推测。
那我们这就好做了,我们直接通过代码去执行performCustomAction:不就OK了吗?
但这个方法是有个参数的,它是如何执行多个cusomAction呢? 我们查看这个方法具体是怎么操作的。
我们分析、发现这个参数使用了tag,不尽让我们想起了原来sourcetree 自定义事件是通过tag去执行对应的操作的。那我们通过对点击的执行事件的对象传入对应的tag不就都OK了吗?
既然自定义的action是从菜单(menu)里面的项去点击的,直接新建一个NSMenuItem,然后对应上自己的tag就好了。
- (void)lv_performSelectorCustomMethod:(NSMenuItem *)customActionItem {
SEL selector = NSSelectorFromString(@"performCustomAction:");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.lvcurrentWindowController performSelector:selector withObject:customActionItem];
#pragma clang diagnostic pop
}
运行程序,发现按钮上的图标没有显示出来,难道通过动态库的注入的方式只能注入代码不能注入资源文件?
在我们自己的工程中,我们的framework是通过bundle的方式显示资源的,我们其实也可以。但这样有个不方便的地方就是安装的人电脑上得有这个资源文件,这很麻烦。我们也知道在xxx.app的包下的Resource存放的是资源文件,我们直接把资源文件拖进就好了。
运行我们的程序,果然如我们所想的那样。
V 2.6.3.3 记(在设置页面 加入是否 显示Diff 信息)
发现V2.6.3 这个版本用起来非常的卡,最卡的是一进入仓库预览信息页面,这个页面默认执行的操作比较多,那我们能不能去除一些不必要的操作来减少内存的消耗呢?
通过查看、分析,在进入仓库页面时,肉眼可以看到会执行这三个操作:
- 显示提交历史记录信息
- 根据最后一次选中的历史信息条目显示具体的提交信息(左下角左侧)
- 显示具体条目信息时,会在右侧请求加载显示具体的比对信息(右下角右侧)。
通过Interface Inspector 界面分析这个页面是STLogViewController。
然后通过Hopper反编译STLogViewController 这个类相关的方法,搜索diff关键字,我们发现refreshDiffView这个方法跟我们比对信息执行后的显示很像,我们hook住这个方法就能得到验证。我们现在做的就是把这个方法不要去执行操作,减少内存的消耗,达到不要那么卡的效果。
我们还有个需求就是在设置页面加入一个开关来控制这个Diff信息显示。
首先我们得知道这个设置页面的class,然后在这个class的view上加入我们自定义的view。
通过Interface Inspector 界面分析这个耗时的页面是STPreferencesWindowController。
设置页面加入如下代码:
[objc_getClass("STPreferencesWindowController") aspect_hookSelector:@selector(windowDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSWindowController *window = (NSWindowController *)aspectInfo.instance;
NSButton *oldSTPreAllowConfigFilesButton = nil;
for (NSView *view in [window.window.contentView subviews]) { // 取出当前的View
if ([view isKindOfClass:[NSButton class]]) {
oldSTPreAllowConfigFilesButton = (NSButton *)view;
break;
}
}
if (oldSTPreAllowConfigFilesButton) {
NSButton *diffButton = [[NSButton alloc] initWithFrame:CGRectMake(CGRectGetMaxX(oldSTPreAllowConfigFilesButton.frame) + 10, oldSTPreAllowConfigFilesButton.frame.origin.y, 105, oldSTPreAllowConfigFilesButton.frame.size.height)];
[diffButton setButtonType:NSButtonTypeSwitch];
[diffButton setTarget:window];
[diffButton setAction:@selector(lvCheckBoxClicked:)];
[diffButton setState:[LVUserDefaults instance].lvShowDiffView ? NSControlStateValueOn : NSControlStateValueOff];
[diffButton setTitle:@"Show DiffView"];
[window.window.contentView addSubview:diffButton];
}
} error:&error];
控制Diff是否显示代码如下:
[objc_getClass("STLogViewController") aspect_hookSelector:@selector(refreshDiffView) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo) {
NSViewController *viewController = [aspectInfo instance];
NSView *paranetDiffView = [[viewController.view subviews] lastObject];
NSView *paranetDiffView2 = [paranetDiffView.subviews lastObject];
NSView *paranetDiffView3 = [[paranetDiffView2 subviews] lastObject];
for (NSView *view in [paranetDiffView3 subviews]) {
if ([view isKindOfClass:NSClassFromString(@"STSplitView")]) {
if (view.subviews.count > 1) {
NSView *diffView = [view.subviews lastObject];
diffView.hidden = ![LVUserDefaults instance].lvShowDiffView;
}
break;
}
}
if ([LVUserDefaults instance].lvShowDiffView) { // 执行原方法
NSInvocation *invoke = [aspectInfo originalInvocation];
[invoke invoke];
}
} error:&error];