Xcode 插件開發

我最近一年來都在開發ios應用,不過感受公司的app維護起來很是麻煩。ios

由於公司要爲不少個企業訂作app,每一個app的功能基本相同,只是界面上的一些圖片和文字要換掉,功能也有一些小改動。考慮到代碼維護的問題,比較好的作法就是隻維護一份代碼,而後用不一樣的配置文件來管理各個target的內容。git

當工程裏達到上百個target的時候,爲工程新增文件就成了一件很是痛苦的事情。github

xcode_plugin_lots_of_targets.png

我必須一個一個地去勾選全部的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_plugin_alcatraz.png

點擊左邊的圖標按鈕就能夠把模板安裝到Xcode裏。

新建一個Xcode工程,選擇」Xcode Plugin」模板,本例子的工程名爲AllTargets。

xcode_plugin_template.png

該模板的部分初始代碼爲:

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打開添加文件窗口:

xcode_plugin_add_to_targets.png

全部的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都自動選中了。

xcode_plugin_auto_select_all_targets.png

4、添加菜單

考慮到有時可能要關閉這個功能,因此能夠給菜單加上是否選中的狀態,此外還能夠給Xcode加上一個獨立的Plugins菜單,大部分插件就能夠放在這個菜單裏,以方便管理。

xcode_plugin_plugins_menu.png

建立菜單的代碼以下:

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

相關文章
相關標籤/搜索