本文將介紹創建一個Xcode4插件所需要的基本步驟以及一些常用的方法。請注意為Xcode創建插件並沒有任何的官方支持,因此本文所描述的方法 和提供的信息可能會隨Apple在Xcode上做的變化而失效。另外,由於創建插件會使用到私有API,因此Xcode插件也不可能被提交到Mac App Store上進行出售。
本文內容是基於Xcode 4.6(4H127)完成的,但是應該可以適用於任意的Xcode 4.X版本。VVPlugInDemo的工程文件我放到了github上,有需要的話您可以從這裡下載並作為參考和起始來使用。
Xcode本身作為一個IDE來說已經可以算上優秀,但是依然會有很多缺失的功能,另外在開發中針對自己的開發需求,創建一些便利的IDE插件,必 定將大為加快開發速度。由於蘋果官方並不對Xcode插件提供任何技術和文檔支持,因此對於大部分開發者來說可能難於上手。雖然沒有官方支持,但是在 Xcode中開發並使用插件是可能的,並且也是被默許的。在Xcode啟動的時候,Xcode將會尋找位於~/Library/Application Support/Developer/Shared/Xcode/Plug-ins文件夾中的後綴名為.xcplugin的bundle作為插件進行加載 (運行其中的可執行文件),這就可以令我們光明正大合法合理地將我們的代碼注入(雖然這個詞有點不好聽)Xcode,並得到運行。因此,想要創建 Xcode插件,我們需要創建Bundle工程並將編譯的bundle放到上面所說的插件目錄中去,這就是Xcode插件的原理。
需要特別說明的是,因為Xcode會在啟動時加載你的插件,這樣就相當於你的代碼有機會注入Xcode。只要你的插件加載成功,那麼它將和 Xcode共用一個進程,也就是說當你的代碼crash的時候,Xcode也會隨之crash。同樣的情況也可能在Xcode版本更新的時候,由於兼容性 問題而出現(因為插件可能使用私有API,Apple沒有義務去維護這些API的可用性)。在出現這種情況的時候,可以直接刪除插件目錄下的導致問題的 xcplugin文件即可。
我將通過制作一個簡單的demo插件來說明一般Xcode插件的編寫方法,這個插件將在Xcode的Edit菜單中加入一個叫做“What is selected”的項目,當你點擊這個菜單命令的時候,將彈出一個警告框,提示你現在在編輯器中所選中的內容。我相信這個例子能包含絕大部分在插件創建 中所必須的步驟和一些有用的方法。由於我自己也只是個半吊子開發者,水平十分有限,因此錯誤和不當之處還懇請大家輕噴多原諒,並幫助我改正。那麼開始..
創建工程,OSX,Framework & Library,選擇Bundle,點擊Next。
在Project信息頁面中,填入插件名字,在這個例子裡,就叫做DemoPlugin,Framework使用默認的Cocoa就行。另外一定記 住將Use Automatic Reference Counting前的勾去掉,由於插件只能使用GC來進行內存管理,因此不需要使用ARC。
插件工程有別於一般工程,需要進行一些特別的設置,以確保能正確編譯插件bundle。
首先,在編輯工程的Info.plist文件(直接編輯plist文件或者是修改TARGETS下對應target的Info都行),加入以下三個布爾值:
<code class="hljs ini"><span class="hljs-setting">XCGCReady = <span class="hljs-value"><span class="hljs-keyword">YES</span> </span></span> <span class="hljs-setting">XCPluginHasUI = <span class="hljs-value"><span class="hljs-keyword">NO</span> </span></span> <span class="hljs-setting">XC4Compatible = <span class="hljs-value"><span class="hljs-keyword">YES</span> </span></span> </code>
這將告訴編譯器工程已經使用了GC,沒有另外的UI並且是Xcode4適配的,否則你的插件將不會被加載。接下來,對Bundle Setting進行一些設置:
如一開始說的那樣,Xcode會在每次啟動的時候搜索插件目錄並進行加載,做如上設置的目的是每次build之後你只需要重新啟動Xcode就能看到重新編譯後的插件的效果,而避免了自己再去尋找Product然後copy&paste的步驟。
另外,還需要自己在User-Defined裡添加一個鍵值對:
至此所有配置工作完成,接下來終於可以開始實現插件了~
新建一個類,取名叫做VVPluginDemo(當然只要不重,隨便什麼名字都是可以的),繼承自NSObject(做iOS開發的童鞋請不要忘記 現在是寫Xcode插件,您需要通過OS X的Cocoa裡的Objective-C class模版,而不要用Cocoa Touch的模版..)。打開VVPluginDemo.m,加入以下代碼:
<code class="objc hljs objectivec">+(<span class="hljs-keyword">void</span>)pluginDidLoad:(<span class="hljs-built_in">NSBundle</span> *)plugin { <span class="hljs-built_in">NSLog</span>(<span class="hljs-string">@"Hello World"</span>); } </code>
Build(對於OS X 10.8的SDK可能會有提示GC已經廢棄的警告,不用管,Xcode本身是GC的,ARC的插件是無法load的),打開控制台(Control+空格 輸入console),重新啟動Xcode。應該能控制台中看到我們的插件的輸出:
太好了。有句話叫做,寫出一個Hello World,就說明你已經掌握了一半…那麼,剩下的一半內容,將對開發插件時可能面臨的問題和一些常用的手段進行介紹。
繼續我們的插件,還記得我們的目的麼?在Xcode的Edit菜單中加入一個叫做“What is selected”的項目,當你點擊這個菜單命令的時候,將彈出一個警告框,提示你現在在編輯器中所選中的內容。一般來說,我們希望插件能夠在整個 Xcode的生命周期中都存在(不要忘記其實用來寫Cocoa的Xcode本身也是一個Cocoa程序)。最好的辦法就是 在+pluginDidLoad:中初始化單例,如下:
<code class="objc hljs objectivec">+ (<span class="hljs-keyword">void</span>) pluginDidLoad: (<span class="hljs-built_in">NSBundle</span>*) plugin { [<span class="hljs-keyword">self</span> shared]; } +(<span class="hljs-keyword">id</span>) shared { <span class="hljs-keyword">static</span> <span class="hljs-built_in">dispatch_once_t</span> once; <span class="hljs-keyword">static</span> <span class="hljs-keyword">id</span> instance = <span class="hljs-literal">nil</span>; <span class="hljs-built_in">dispatch_once</span>(&once, ^{ instance = [[<span class="hljs-keyword">self</span> alloc] init]; }); <span class="hljs-keyword">return</span> instance; } </code>
這樣,以後我們在別的類中,就可以簡單地通過[VVPluginDemo shared]來訪問到插件的實例了。
在init中,加入一個程序啟動完成的事件監聽,並在程序完成啟動後,在菜單欄的Edit中添加我們所需要的菜單項,這個操作最好是在Xcode完 全啟動以後再進行,以避免一些潛在的危險和沖突。另外,由於想要在按下按鈕時顯示編輯器中顯示的內容,我們可能需要監聽 NSTextViewDidChangeSelectionNotification事件(WTF,你為什麼會知道要監聽什麼。別著急,後面會再說,先做 demo先做demo)
<code class="objc hljs objectivec">- (<span class="hljs-keyword">id</span>)init { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">self</span> = [<span class="hljs-keyword">super</span> init]) { [[<span class="hljs-built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="hljs-keyword">self</span> selector:<span class="hljs-keyword">@selector</span>(applicationDidFinishLaunching:) name:NSApplicationDidFinishLaunchingNotification object:<span class="hljs-literal">nil</span>]; } <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>; } - (<span class="hljs-keyword">void</span>) applicationDidFinishLaunching: (<span class="hljs-built_in">NSNotification</span>*) noti { [[<span class="hljs-built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="hljs-keyword">self</span> selector:<span class="hljs-keyword">@selector</span>(selectionDidChange:) name:NSTextViewDidChangeSelectionNotification object:<span class="hljs-literal">nil</span>]; NSMenuItem *editMenuItem = [[NSApp mainMenu] itemWithTitle:<span class="hljs-string">@"Edit"</span>]; <span class="hljs-keyword">if</span> (editMenuItem) { [[editMenuItem submenu] addItem:[NSMenuItem separatorItem]]; NSMenuItem *newMenuItem = [[NSMenuItem alloc] initWithTitle:<span class="hljs-string">@"What is selected"</span> action:<span class="hljs-keyword">@selector</span>(showSelected:) keyEquivalent:<span class="hljs-string">@""</span>]; [newMenuItem setTarget:<span class="hljs-keyword">self</span>]; [newMenuItem setKeyEquivalentModifierMask: NSAlternateKeyMask]; [[editMenuItem submenu] addItem:newMenuItem]; [newMenuItem release]; } } -(<span class="hljs-keyword">void</span>) selectionDidChange:(<span class="hljs-built_in">NSNotification</span> *)noti { <span class="hljs-comment">//Nothing now. Just in case of crash. </span> } -(<span class="hljs-keyword">void</span>) showSelected:(<span class="hljs-built_in">NSNotification</span> *)noti { <span class="hljs-comment">//Nothing now. Just in case of crash. </span> } </code>
現在build,重啟Xcode,如果一切順利的話,你應該能看到菜單欄上的變化了:
剩下的事情就很簡單了,在接收到TextView的ChangeSelection通知後把現在選中的文本更新一下,在點擊按鈕時顯示一個含有儲存文字的對話框就行了。Let's do it~
首先在.m文件中加上property聲明(個人習慣,喜歡用ivar也可以)。在#import和@implementation之間加上:
<code class="objc hljs objectivec"><span class="hljs-class"><span class="hljs-keyword">@interface</span> <span class="hljs-title">VVPluginDemo</span>() </span> <span class="hljs-keyword">@property</span> (<span class="hljs-keyword">nonatomic</span>,<span class="hljs-keyword">copy</span>) <span class="hljs-built_in">NSString</span> *selectedText; <span class="hljs-keyword">@end</span> </code>
得益於新的屬性自動綁定,synthesis已經不需要寫了(對此還不太了解的童鞋可以參看我的這篇博文)。然後完成- selectionDidChange:和-showSelected:如下:
<code class="objc hljs objectivec">-(<span class="hljs-keyword">void</span>) selectionDidChange:(<span class="hljs-built_in">NSNotification</span> *)noti { <span class="hljs-keyword">if</span> ([[noti object] isKindOfClass:[NSTextView class]]) { NSTextView* textView = (NSTextView *)[noti object]; <span class="hljs-built_in">NSArray</span>* selectedRanges = [textView selectedRanges]; <span class="hljs-keyword">if</span> (selectedRanges<span class="hljs-variable">.count</span>==<span class="hljs-number">0</span>) { <span class="hljs-keyword">return</span>; } <span class="hljs-built_in">NSRange</span> selectedRange = [[selectedRanges objectAtIndex:<span class="hljs-number">0</span>] rangeValue]; <span class="hljs-built_in">NSString</span>* text = textView<span class="hljs-variable">.textStorage</span><span class="hljs-variable">.string</span>; <span class="hljs-keyword">self</span><span class="hljs-variable">.selectedText</span> = [text substringWithRange:selectedRange]; } <span class="hljs-comment">//Hello, welcom to OneV's Den </span> } -(<span class="hljs-keyword">void</span>) showSelected:(<span class="hljs-built_in">NSNotification</span> *)noti { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText: <span class="hljs-keyword">self</span><span class="hljs-variable">.selectedText</span>]; [alert runModal]; } </code>
Build,重啟Xcode,隨便選中一段文本,然後點擊Edit中的What is selected。OY~完成~
至此,您應該已經掌握了基本的Xcode插件制作方法了。接下來的就是根據您的需求實踐了~但是在此之前,還有一些重要的技巧和常用方法可能您會有興趣。
由於沒有文檔指導插件開發,調試也只能用打log的方式,因此會十分艱難。掌握一些常用的技巧和方法,將會很有幫助。
一種很好的方法是監聽需要的消息,並針對消息作出反應。就像demo裡的 NSTextViewDidChangeSelectionNotification。對於熟悉iOS或者Mac開發的童鞋來說,應該在日常開發裡也接觸 過很多類型的Notification了,而因為插件開發沒有文檔,因此我們需要自己去尋找想要監聽和接收的Notification。NSNotificationCenter文檔中,關於加入Observer的方法-addObserver:selector:name:object:,當給name參數賦值nil時,將可以監聽到所有的notification:
notificationName: The name of the notification for which to register the observer; that is, only notifications with this name are delivered to the observer. If you pass nil, the notification center doesn’t use a notification’s name to decide whether to deliver it to the observer.
因此可以用它來監測所有的Notification,並從中找到自己所需要的來進行處理:
<code class="objc hljs objectivec">-(<span class="hljs-keyword">id</span>)init { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">self</span> = [<span class="hljs-keyword">super</span> init]) { [[<span class="hljs-built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="hljs-keyword">self</span> selector:<span class="hljs-keyword">@selector</span>(notificationListener:) name:<span class="hljs-literal">nil</span> object:<span class="hljs-literal">nil</span>]; } <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>; } -(<span class="hljs-keyword">void</span>)notificationListener:(<span class="hljs-built_in">NSNotification</span> *)noti { <span class="hljs-built_in">NSLog</span>(<span class="hljs-string">@" Notification: %@"</span>, [noti name]); } </code>
編譯重啟後在控制台得到的輸出:
當然如果只是打印名字的話可能幫助不大,也許你需要從noti的object或者userinfo中獲得更多的信息。按條件打印,配合控制台的搜索進行尋找會是一個不錯的方法。
用OC的動態特性可以做很多事,比如在運行時替換掉某個Xcode的方法。記住Xcode本身也是Cocoa程序,本質上和我們用Xcode所開發 的程序沒有太大區別。因此如果可以知道Xcode在進行某些操作時候的方法的話,就可以將該方法與我們自己實現的方法進行運行時調換,從而改為執行我們自 己的方法。這便是運行時的Method Swizzling(或者叫Monkey patch,管他呢),這在smalltalk類語言中是一種很有趣和方便的做法,關於這方面更詳細的,我以前寫過一篇關於OC運行時特性的文章。當時提到的method swizzling方法並沒有對交換的函數進行檢查等工作,通用性也比較差。現在針對OC已經有比較成熟的一套方法交換機制了,其中比較有名的有rentzsch的jrswizzle以及OC社區的MethodSwizzling實現。
有了方法交換的辦法,接下來需要尋找要交換的方法。Xcode所使用的所有庫都包含在Xcode.app/Contents/的 Frameworks,SharedFrameworks和OtherFrameworks三個文件夾下。其中和Xcode關系最為直接以及最為重要的是 Frameworks中的IDEKit和IDEFoundation,以及SharedFrameworks中的DVTKit和 DVTFoundation四個。其中DVT前綴表示Developer Toolkit,IDE和IDEFoundation中的類基本是DVT中類的子類。這四個framework將是我們在開發改變Xcode默認行為的 Xcode插件時最主要要打交道的。另外如果想對IB進行注入,可能還需要用到Frameworks下的 IBAutolayoutFoundation(待確定)。關於這些framework中的私有API,可以使用class-dump很簡單地將頭文件提取出來。當然,也有人為懶人們完成了這個工作,probablycorey的xcode-class-dump中有絕大部分類的頭文件。
作為Demo,我們將簡單地完成一個方法交換:在補全代碼時,我們簡單地輸出一句log。
為了交換方法,可以直接用現成的MethodSwizzle實現。MethodSwizzle可以在這裡找到。將.h和.m導入插件工程即可~
通過搜索,補全代碼的功能定義在DVKit中的DVTTextCompletionController類,其中有一個方法為- (BOOL)acceptCurrentCompletion,猜測返回的布爾值是否接受當前的補全結果。由於這些都是私有API,因此需要在我們的工程 中自己進行聲明。在新建文件中的C and C++中選Header File,為工程加入一個Header文件,並加入一下代碼:
<code class="objc hljs objectivec"><span class="hljs-class"><span class="hljs-keyword">@interface</span> <span class="hljs-title">DVTTextCompletionController</span> : <span class="hljs-title">NSObject</span> </span> - (<span class="hljs-built_in">BOOL</span>)acceptCurrentCompletion; <span class="hljs-keyword">@end</span> </code>
然後需要將DVKit.framework添加到工程中,在/Applications/Xcode.app/Contents /SharedFrameworks中找到DVTKit.framework,拷貝到任意正常能訪問到的目錄下,然後在插件工程的Build Phases中加入framework。嗯?你說找不到DVTKit.framework?親,私有框架當然找不到,點擊Add Other...然後去剛才copy出來的地方去找吧..
最後便是加入方法交換了~新建一個DVTTextCompletionController的Category,命名為PluginDemo
import之前定義的header和MethodSwizzle.h,在DVTTextCompletionController+PluginDemo.m中加入下面實現:
<code class="objc hljs objectivec">+ (<span class="hljs-keyword">void</span>)load { MethodSwizzle(<span class="hljs-keyword">self</span>, <span class="hljs-keyword">@selector</span>(acceptCurrentCompletion), <span class="hljs-keyword">@selector</span>(swizzledAcceptCurrentCompletion)); } - (<span class="hljs-built_in">BOOL</span>)swizzledAcceptCurrentCompletion { <span class="hljs-built_in">NSLog</span>(<span class="hljs-string">@"acceptCurrentCompletion is called by %@"</span>, <span class="hljs-keyword">self</span>); <span class="hljs-keyword">return</span> [<span class="hljs-keyword">self</span> swizzledAcceptCurrentCompletion]; } </code>
+load方法在每個NSObject類或子類被調用時都會被執行,可以用來在runtime配置當前類。這裡交換了 DVTTextCompletionController的acceptCurrentCompletion方法和我們自己實現的 swizzledAcceptCurrentCompletion方法。在swizzledAcceptCurrentCompletion中,先打印了 一句log,輸出相應該方法的實例。接下來似乎是調用了自己,但是實際上swizzledAcceptCurrentCompletion的方法已經和原 來的acceptCurrentCompletion交換,因此這裡實際調用的將是原來的方法。那麼這段代碼所做的就是將Xcode想調用原來的 acceptCurrentCompletion的行為,改變成了先打印一個log,之後再進行原來的acceptCurrentCompletion調 用。
編譯,重啟Xcode,打開一個工程隨便輸入點東西,讓補全出現。控制台中的輸出符合我們的預期:
太棒了,有了對私有API的注入,能做的事情大為擴展了。
另外一種常見的插件行為是修改某些界面。再一次說明,Xcode是一個標准Cocoa程序,一切都是那麼熟悉(如果你為Cocoa或者 CocoaTouch開發的話,應該是很熟悉)。拿到整個App的Window,然後依次遞歸打印subview。stackoverflow上有一個UIView的版本,稍微改變一下就可以得到一個NSView版本。新建一個NSView的Dumping Category,加入如下實現:
<code class="objc hljs objectivec">-(<span class="hljs-keyword">void</span>)dumpWithIndent:(<span class="hljs-built_in">NSString</span> *)indent { <span class="hljs-built_in">NSString</span> *class = NSStringFromClass([<span class="hljs-keyword">self</span> class]); <span class="hljs-built_in">NSString</span> *info = <span class="hljs-string">@""</span>; <span class="hljs-keyword">if</span> ([<span class="hljs-keyword">self</span> respondsToSelector:<span class="hljs-keyword">@selector</span>(title)]) { <span class="hljs-built_in">NSString</span> *title = [<span class="hljs-keyword">self</span> performSelector:<span class="hljs-keyword">@selector</span>(title)]; <span class="hljs-keyword">if</span> (title != <span class="hljs-literal">nil</span> && [title length] > <span class="hljs-number">0</span>) { info = [info stringByAppendingFormat:<span class="hljs-string">@" title=%@"</span>, title]; } } <span class="hljs-keyword">if</span> ([<span class="hljs-keyword">self</span> respondsToSelector:<span class="hljs-keyword">@selector</span>(stringValue)]) { <span class="hljs-built_in">NSString</span> *string = [<span class="hljs-keyword">self</span> performSelector:<span class="hljs-keyword">@selector</span>(stringValue)]; <span class="hljs-keyword">if</span> (string != <span class="hljs-literal">nil</span> && [string length] > <span class="hljs-number">0</span>) { info = [info stringByAppendingFormat:<span class="hljs-string">@" stringValue=%@"</span>, string]; } } <span class="hljs-built_in">NSString</span> *tooltip = [<span class="hljs-keyword">self</span> toolTip]; <span class="hljs-keyword">if</span> (tooltip != <span class="hljs-literal">nil</span> && [tooltip length] > <span class="hljs-number">0</span>) { info = [info stringByAppendingFormat:<span class="hljs-string">@" tooltip=%@"</span>, tooltip]; } <span class="hljs-built_in">NSLog</span>(<span class="hljs-string">@"%@%@%@"</span>, indent, class, info); <span class="hljs-keyword">if</span> ([[<span class="hljs-keyword">self</span> subviews] count] > <span class="hljs-number">0</span>) { <span class="hljs-built_in">NSString</span> *subIndent = [<span class="hljs-built_in">NSString</span> stringWithFormat:<span class="hljs-string">@"%@%@"</span>, indent, ([indent length]/<span class="hljs-number">2</span>)%<span class="hljs-number">2</span>==<span class="hljs-number">0</span> ? <span class="hljs-string">@"| "</span> : <span class="hljs-string">@": "</span>]; <span class="hljs-keyword">for</span> (<span class="hljs-built_in">NSView</span> *subview <span class="hljs-keyword">in</span> [<span class="hljs-keyword">self</span> subviews]) { [subview dumpWithIndent:subIndent]; } } } </code>
在合適的時候(比如點擊某個按鈕時),調用下面一句代碼,便可以打印當前Xcode的結構,非常方便。這對了解Xcode的構成和如何搭建一個如Xcode般復雜的程序很有幫助~
<code class="hljs css"><span class="hljs-attr_selector">[[[NSApp mainWindow]</span> <span class="hljs-tag">contentView</span>] <span class="hljs-tag">dumpWithIndent</span>:<span class="hljs-at_rule">@<span class="hljs-keyword">""];</span> </span></code>
在結果控制台中的輸出結果類似這樣:
根據自己需要去去相應的view吧~然後配合方法交換,基本可以做到盡情做想做的事情了。
/Applications/Xcode.app/Contents/Frameworks/IDEKit.framework/Versions /A/Resources中有不少Xcode界面用的圖片,pdf,png和tiff格式都有,想要自定義run,stop按鈕或者想要讓斷點標記從藍色 塊變成機器貓頭像什麼的…應該是可能的~
/Applications/Xcode.app/Contents/PlugIns目錄裡有很多Xcode自帶的“官方版”外掛插件,顯然通過 class-dump和注入的方法,你可以為Xcode的插件寫插件...嗯~比如改變debugger的行為或者讓plist編輯器更聰明,就是這樣 的。
希望Apple能提供為Xcode編寫插件的支持,所有東西都需要摸索雖然很有趣,但是也比較花時間。
另外,github等代碼托管網站上有不少大神們寫的插件,都開源放出。這些必須是學習插件編寫的最優秀的教材和參考:
好了,就到這裡吧。VVPlugInDemo的工程文件我放到了github上,有需要的話您可以從這裡下載並作為參考和起始來使用。謝謝您看完這麼長的文。正如一開始所說的,我自己水平十分有限,因此錯誤和不當之處還懇請大家輕噴多原諒,並幫助我改正,再次謝謝~