我最近一年來都在開發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