前言

通过 Mac逆向工程-入门 文章我们对Mac逆向应用有了一定的了解,如果想对Mac上的应用做一些UI修改,可以通过[动态库注入]的方式修改代码就能达到自己的需求。

这次在Sourcetree应用中加入了2个功能。

  • 在Sourcetree中的review code仓库页面中的Toolbar上加入几个按钮,替代CustomAction中的快捷键(见图一)。
  • 在点击pull(拉取)按钮时,自动勾选用变基替换合并 CheckBox(见图二)。

图一:Toolbar上加入的几个按钮

图二:默认勾选变基替换合并

分析

问题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];

仓库页面开关显示对比