我最近一年来都在开发ios应用,不过感受公司的app维护起来很是麻烦。ios
由于公司要为不少个企业订作app,每一个app的功能基本相同,只是界面上的一些图片和文字要换掉,功能也有一些小改动。考虑到代码维护的问题,比较好的作法就是只维护一份代码,而后用不一样的配置文件来管理各个target的内容。git
当工程里达到上百个target的时候,为工程新增文件就成了一件很是痛苦的事情。github
我必须一个一个地去勾选全部的targets,每每要花上几分钟的时间来重复无聊的操做,既浪费时间又影响心情,而Xcode竟然没有自带全选targets的功能。所以我萌生了一个想法:写一个能自动勾选全部targets的插件。数组
google一下Xcode的制做教程,找到了VVDocumenter插件做者写的一篇教程:《Xcode 4 插件制做入门》。xcode
这篇教程很适合入门,不过里面有些东西因为年代久远,已经不兼容最新的Xcode 6.1了。可是教程里不少细节都写得很详细,建议先看完这篇教程。我看了教程后加上本身的摸索,终于完成了插件的开发,所以在这里把插件的开发过程分享出来。app
本插件的源码下载地址:https://github.com/poboke/AllTargetside
1、安装插件模板函数
Alcatraz是一款开源的Xcode包管理器,源码下载地址为:https://github.com/supermarin/Alcatrazui
编译完成以后,重启Xcode,而后点击Xcode顶部菜单”Windows”中的”Package Manager”就能够打开Alcatraz包管理器面板。google
搜索关键字”Xcode Plugin”,能够找到一个”Xcode Plugin”模板,该模板能够用来建立Xcode 6+的插件。
点击左边的图标按钮就能够把模板安装到Xcode里。
新建一个Xcode工程,选择”Xcode Plugin”模板,本例子的工程名为AllTargets。
该模板的部分初始代码为:
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
|
- (id)initWithBundle:(NSBundle *)plugin
{
if
(self = [
super
init]) {
// reference to plugin's bundle, for resource access
self.bundle = plugin;
// Create menu items, initialize UI, etc.
// Sample Menu Item:
NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@
"Edit"
];
if
(menuItem) {
[[menuItem submenu] addItem:[NSMenuItem separatorItem]];
NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@
"Do Action"
action:@selector(doMenuAction) keyEquivalent:@
""
];
[actionMenuItem setTarget:self];
[[menuItem submenu] addItem:actionMenuItem];
}
}
return
self;
}
// Sample Action, for menu item:
- (void)doMenuAction
{
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@
"Hello, World"
];
[alert runModal];
}
|
初始代码会在Xcode的”Edit”菜单里加入一个名字为”Do Action”的子菜单,当你点击这个子菜单的时候,会调用doMenuAction函数弹出一个提示框,提示内容为”Hello, World”。
2、需求分析
在Xcode里按command+alt+A打开添加文件窗口:
全部的targets都位于白色矩形视图里,能够猜想该矩形视图是一个NSTableView(大小差很少为320*170),勾选的按钮是一个NSCell。
首先要得到NSTableView对象,《Xcode 4 插件制做入门》里提到可使用递归打印subviews的方法来获得某个NSView对象。
不过我发现一种更简便的方法,在本例子中比较适用。在没打开添加文件窗口以前,NSTableView是不会建立的,而视图建立设置尺寸时都会调用NSViewDidUpdateTrackingAreasNotification通知。因此咱们能够先监听该通知,再打开添加文件窗口,这样就能获得添加文件窗口里全部视图对象了,修改代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- (void)doMenuAction
{
//监听视图更新区域大小的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationListener:) name:NSViewDidUpdateTrackingAreasNotification object:nil];
}
- (void)notificationListener:(NSNotification *)notification
{
//打印出视图对象以及视图的大小
NSView *view = notification.object;
if
([view respondsToSelector:@selector(frame)]) {
NSLog(@
"view : %@, frame : %@"
, view, [NSValue valueWithRect:view.frame]);
}
}
|
编译代码后重启Xcode,打开控制台(Control+空格,输入console),并清空控制台里的log。
点击Xcode的”Do Action”子菜单开始监听消息,这时打开添加文件的窗口会看到控制台输出一堆log。
把log复制到MacVim里,搜索”NSTableView”,能够找到一条结果:
1
|
view : < NSTableView: 0x7fb206c65f40>, frame : NSRect: {{0, 0}, {321, 170}}
|
能够发现,此TableView的大小为321*170,看来正是咱们正在寻找的对象。
3、hook私有类
因为NSCell的值是由NSTableView的数据源所控制的,因此咱们必须找到NSTableView的数据源,修改一下代码打印出数据源:
1
2
3
4
5
6
7
8
|
- (void)notificationListener:(NSNotification *)notification
{
NSView *view = notification.object;
if
([view.className isEqualToString:@
"NSTableView"
]) {
NSTableView *tableView = (NSTableView *)view;
NSLog(@
"dataSource : %@"
, tableView.dataSource);
}
}
|
能够看到控制台输出了log:
1
|
dataSource : < Xcode3TargetMembershipDataSource: 0x7fadb7352830>
|
Xcode3TargetMembershipDataSource是Xcode的私有类,位于 /Applications/Xcode.app/Contents/PlugIns/Xcode3UI.ideplugin/Contents/MacOS/Xcode3UI 里。因为这个私有类没有frameworks可引用,因此只能经过NSClassFromString来Hook该私有类的函数。
在这里能够下载从Xcode 6.1 dump出来的私有类头文件:https://github.com/luisobo/Xcode-RuntimeHeaders/tree/xcode6-beta1。
打开Xcode3TargetMembershipDataSource.h,部分代码以下:
1
2
3
4
5
6
7
|
@interface Xcode3TargetMembershipDataSource : NSObject {
NSMutableArray *_wrappedTargets;
//......
}
- (void)updateTargets;
//......
|
_wrappedTargets数组颇有可能保存着targets的信息,updateTargets函数的做用应该是用来更新targets的值,因此能够试试hook updateTargets函数,代码以下:
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
38
39
40
|
//originalImp用来保存原私有类的方法
static IMP originalImp = NULL;
@implementation AllTargets
//......
- (void)doMenuAction
{
[self hookClass];
}
- (void)hookMethod
{
SEL method = @selector(updateTargets);
//获取私有类的函数
Class originalClass = NSClassFromString(@
"Xcode3TargetMembershipDataSource"
);
Method originalMethod = class_getInstanceMethod(originalClass, method);
originalImp = method_getImplementation(originalMethod);
//获取当前类的函数
Class replacedClass = self.class;
Method replacedMethod = class_getInstanceMethod(replacedClass, method);
//交换两个函数
method_exchangeImplementations(originalMethod, replacedMethod);
}
- (void)updateTargets
{
//先调用原私有类的函数
originalImp();
//查看_wrappedTargets数组里保存了什么类型的对象
NSMutableArray *wrappedTargets = [self valueForKey:@
"wrappedTargets"
];
for
(id wrappedTarget
in
wrappedTargets) {
NSLog(@
"target : %@"
, wrappedTarget);
}
}
|
能够看到控制台输出了log,因为工程只有一个target,因此只有一个对象:
1
|
target : < Xcode3TargetWrapper: 0x7f8b59264ab0>
|
在Xcode的私有类里找到Xcode3TargetWrapper.h,内容以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@interface Xcode3TargetWrapper : NSObject
{
PBXTarget *_pbxTarget;
Xcode3Project *_project;
NSString *_name;
NSImage *_image;
BOOL _selected;
}
@property(readonly) NSImage *image;
// @synthesize image=_image;
@property(readonly) NSString *name;
// @synthesize name=_name;
@property BOOL selected;
// @synthesize selected=_selected;
//......
|
能够看到,该类有三个属性:图片、名字和是否选中,咱们只要把selected属性改成YES就好了。
咱们把updateTargets函数修改成:
1
2
3
4
5
6
7
8
9
10
11
|
- (void)updateTargets
{
//先调用原私有类的函数
originalImp();
//修改wrappedTarget的属性
NSMutableArray *wrappedTargets = [self valueForKey:@
"wrappedTargets"
];
for
(id wrappedTarget
in
wrappedTargets) {
[wrappedTarget setValue:@YES forKey:@
"selected"
];
}
}
|
再次编译重启Xcode,打开添加文件窗口,能够发现全部targets都自动选中了。
4、添加菜单
考虑到有时可能要关闭这个功能,因此能够给菜单加上是否选中的状态,此外还能够给Xcode加上一个独立的Plugins菜单,大部分插件就能够放在这个菜单里,以方便管理。
建立菜单的代码以下:
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
|
- (void)addPluginsMenu
{
//增长一个"Plugins"菜单到"Window"菜单前面
NSMenu *mainMenu = [NSApp mainMenu];
NSMenuItem *pluginsMenuItem = [mainMenu itemWithTitle:@
"Plugins"
];
if
(!pluginsMenuItem) {
pluginsMenuItem = [[NSMenuItem alloc] init];
pluginsMenuItem.title = @
"Plugins"
;
pluginsMenuItem.submenu = [[NSMenu alloc] initWithTitle:pluginsMenuItem.title];
NSInteger windowIndex = [mainMenu indexOfItemWithTitle:@
"Window"
];
[mainMenu insertItem:pluginsMenuItem atIndex:windowIndex];
}
//添加"Auto Select All Targets"子菜单
NSMenuItem *subItem = [[NSMenuItem alloc] init];
subItem.title = @
"Auto Select All Targets"
;
subItem.target = self;
subItem.action = @selector(toggleMenu:);
subItem.state = NSOnState;
[pluginsMenuItem.submenu addItem:subItem];
}
- (void)toggleMenu:(NSMenuItem *)menuItem
{
//改变菜单选中状态
menuItem.state = !menuItem.state;
//从新交换函数,hook与unhook
[self hookMethod];
}
|
本插件的源码下载地址:https://github.com/poboke/AllTargets