原做者:Derek Selandergit
譯者:@yohunlgithub
譯者注:原文使用的是xcode6.3.2,我翻譯的時候,使用的是xcode7.2.1,通過驗證,文章中說說的依然是有效的。在文中你能夠學習到一系列的技能,很是值得一看。正則表達式
蘋果的"一個足以應付全部"策略使得它的產品愈來愈像一個難如下嚥的藥丸。儘管蘋果已經將一些工做流帶給了iOS/OS X的開發者,咱們仍然但願經過插件來使得Xcode更加順手!vim
雖然蘋果並無提供任何的官方文檔來指導咱們如何建立一個xcode插件,可是開發者社區作了大量的工做開發了一些很是有用的工具,經過這些工具,能夠用來幫助開發者。xcode
從 自動完成圖片名的插件,到 清除緩存的插件 再到 使Xcode變成一個vim編輯器的插件,Xcode的插件社區已經拓展了咱們的思惟,咱們可讓Xcode變得更加智能。緩存
在 這個不算短的三部分教程中,你將建立一個Xcode的插件來娛樂你的同事,其特點莫過於顯示的內容並非他所指望看到的!儘管這個插件是很輕量級的,你仍 然能夠學習到不少知識,例如,經過調試Xcode,怎樣找出你感興趣的元素而且修改它,怎樣將系統的功能函數替換爲你本身的函數(經過swizzle技 術)!sass
你將會使用 x86彙編知識,代碼定技能以及LLDB調試技能來查閱未公開的私有framework,而且要探索這些私有framework中的私有API,還要使用 method swizzleing來進行代碼的注入。正由於有這麼多內容,因此本教程的講解速度會很快。在繼續以前,請務必肯定你已經掌握了相關的 iOS/OS X的開發。markdown
使用Swift來開發插件,仍是一個比較複雜的主題,而且Swift的調試工具依然比Objective-C要弱不少。就目前而言,這意味着插件開發的最佳選擇(本教程!)是Objective-C.架構
開始
爲了慶祝 惡做劇你的同事日,你的Xcode插件將會Rayroll你的受害者。等等… 什麼是Rayrolling?它是一個免費和無版權的Rayrolling瑞克搖擺-就是你看到的內容並非你指望的內容,有點掛羊頭賣狗肉的意思。當你完成了這個系列,你的插件將會更改Xcode顯示的內容:
用Ray's的頭像來替換Xcode的某些警示框(例如, Build成功或者失敗的Xcode提示框)
替換Xcode的標題內容爲Ray的熱門歌曲的一句歌詞,Never Gonna Live You Up
替換全部的Xcode中的搜索文檔內容爲一個視頻 Rayroll'd video
在教程的第一部分,咱們將聚焦於尋找到負責展現"Build成功"警示框的那個類,而且將其圖片改成Ray的頭像這張圖片
安裝插件管理插件 Alcatraz
在開始以前,須要先安裝Alcatraz,它是Xcode插件管理工具。
典型的安裝Alcatraz的方式是經過命令行
1
|
curl -fsSL https:
//raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh
|
當這條命令結束後,重啓Xcode。你可能會看到一個提示 Alcatraz bundle的警示框;點擊 Load Bundle 繼續,以便xcode可以加載Alcatraz插件,這樣這個Alcatraz插件才能起做用
注意:若是你一不當心點擊了"skip bundle",你能夠經過命令行輸入如下命令來從新顯示它!
1
|
defaults
delete
com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode-7.2.1
|
以上的Xcode 7.2.1是你機子上的Xcode版本號,若是你的不是7.2.1,改成你的對應的版本號就能夠了。
你 將會在Xcode的Window菜單下看到一個新的菜單項:Package Manager。建立一個xcode插件,須要你經過設置Build Settings來運行另外一個新的Xcode的實例來加載才能夠,這是一個枯燥和乏味的過程(若是想知道這個枯燥的過程,能夠參考個人文章Xcode7 插件製做入門),幸虧,已經有人替你完成了這件事情了,有人開發了一個Xcode的工程模板,可讓你很方便的建立一個插件工程。
打開Alcatraz(Window->Package Manager)。在Alcatraz的搜索框中輸入Xcode Plugin。務必確保你選中了搜索框中的All和Templates兩個屬性。一旦你搜索到,單擊其左邊的Install來安裝它!!
若是你搜索不到,也不要緊,你能夠前往 https://github.com/kattrali/Xcode-Plugin-Template本身下載下來的方式來加載它,具體安裝方式能夠見工程的說明。
一 旦Alcatraz下載完了Xcode Plugin插件,你就能夠建立一個插件工程了(File->New -> Project…),選擇這個新的OS X ->Xcode Plugin ->Xcode Plugin模板,而後點擊下一步。
給工程取名字Rayrolling,組織的標識符爲com。raywenderlich(這一步很是重要),選擇Objective-C做爲代碼語言。。保存工程到任何一個你想放置的目錄中。
Hello World插件模板
編譯,而後運行這個Rayroll工程,你將會看到一個新的Xcode實例出現。這個Xcode實例在Edit菜單欄下多了一個菜單項Do Action:
選擇這個菜單項,將會出現一個模態的彈出框:
從 Xcode5開始,插件都只能運行在特定版本的Xcode中。這也就意味着當新的Xcode更新安裝後,全部的第三方插件都將失效,除非你添加了該版本 Xcode的UUID。若是部分模板沒有起做用,你也沒看到一個新的菜單項,可能的緣由之一就是由於沒有對應版本的UUID,你須要添加對應該版本 Xcode的支持。
爲了添加UUID,首先是在命令行中運行如下命令
1
|
defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID
|
這條命令會輸出當前版本xcode的UUID。打開Rayroll工程的Info.plist文件。導航到DVTPlugInCompatibilityUUID,添加它
注意:經過本教程,你會運行和修改已經安裝了的插件。這將會改變Xcode的默認行爲,固然,這也可能會致使Xcode crash!!若是你想禁止某個插件,你能夠手動的經過終端去刪除它.
1
2
|
cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/
rm -r Rayrolling.xcplugin/
|
而後重啓一下Xcode
找到咱們要修改的xcode的某項特性
最直接最有效的獲得幕布後發生什麼的方式是 經過註冊一個NSNotification observer 來監聽全部的Xcode事件。經過Xcode和監聽這些消息通知,你將會深刻到一些內部類的內部。
打開Rayrollling.m,在類中添加以下的屬性
1
|
@property (nonatomic,strong) NSMutableSet *notificationSet;
|
這個NSMutableSet用來存儲全部的Xcode的控制檯打印出來的NSNotification的名字
下一步,在initWithBundle:中,if (self = [super init]) {以後,添加以下代碼
1
2
3
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil];
self.notificationSet = [NSMutableSet
new
];
|
給name參數傳遞nil指示,偶們須要監聽xcode的全部NSNotification。
如今,實現handleNotification:方法:
1
2
3
4
5
6
|
- (void)handleNotification:(NSNotification *)notification {
if
(![self.notificationSet containsObject:notification.name]) {
NSLog(@
"%@, %@"
, notification.name, [notification.object class]);
[self.notificationSet addObject:notification.name];
}
}
|
handleNotification:檢查獲取到的通知名稱是否是在notificationSet中,若是不是,則在控制檯打印出它的通知名和通知多對應的類。而後添加到notificationSet中。經過這種方式,你只會在控制檯看到每一種類型的通知一次
下一步,找到添加menu item的聲明,將其替換成下面的代碼
1
2
|
NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@
"Reset Logger"
action:@selector(doMenuAction) keyEquivalent:@
""
];
|
這段代碼只是簡單的更改了NSMenuItem的標題,以便讓你知道,當你點擊它的時候,它將會重置存放NSNotification 的set對象。
最後,替換doMenuAction的實現代碼爲下面的
1
2
3
|
- (void)doMenuAction {
[self.notificationSet removeAllObjects];
}
|
這個菜單項將會重置全部存放在notificationSet屬性中的通知。這樣作的目的是讓你在控制檯中很容易的觀察到你感興趣的通知,而不至於被控制檯的重複消息所刷屏。讓你更加專一。
再 一次編譯運行,請確認你分清了哪一個是你工程的Xcode(也就是父Xcode),哪一個是你debug出來的一個Xcode實例(子Xcode),爲何要 分清楚呢?由於咱們的每一次改變,當從新debug的時候,debug出來的Xcode中已經起做用了,而父Xcode,只有等到你重啓後,才能看到效果 的。
在子Xcode中,隨便點擊點擊按鈕,打開一些窗口,瀏覽瀏覽程序,你會在父xcode的控制檯中看到消息的觸發。
查找和監測編譯的提示框
如今,你已經學會了基本的查看由Xcode自己引發的通知(NSNotification),如今你須要明確的找出來顯示編譯狀態的提示框所關聯的類是哪個。
運行Xcode插件,在子Xcode中,打開任何一個工程,確保打開了Xcode設置中的bezel notifications,Succeeds和Fails中的bezel notifications都要打開。固然了,請再次肯定你操做的是子Xcode實例!
經過你在Xcode的edit菜單中建立的 菜單項Reset Logger來重置notificationSet,而後運行你的子code(上面讓你在子Xcode中打開了任何一個工程,如今你在子Xcode中運行這個打開的工程)
當 子Xcode的工程編譯結果出來後(或者成功,或者失敗都不要緊),關注父Xcode中控制檯輸出的信息。粗略的瀏覽一遍,看是否有能引發你關注的通知。 你可以發現一些值得你進一步關注的notifications麼?下面的這些可能能給你一些幫助(原文中,如下的列表是隱藏的,你能夠點擊展現它,做者鼓 勵你們本身先找找看,若是找不到,再打開下面的這個提示,因爲我使用的markdown編輯器的限制,作不到這點,因此此處直接放出來了)
下面的一些項值得你進一步關注:
NSWindowWillOrderOffScreenNotification, DVTBezelAlertPanel
NSWindowDidOrderOffScreenNotification, DVTBezelAlertPanel
NSWindowDidOrderOffScreenAndFinishAnimatingNotification, DVTBezelAlertPanel
你 應該挑其中一個,並進一步探索它,看看是否是能夠從中獲得一些重要的信息。例如 NSWindowWillOrderOffScreenNotification是幹什麼的? 很好,你選擇了進一步探索NSWindowWillOrderOffScreenNotification。
回到父Xcode中的Rayrolled.m文件,定位到handleNotification:方法,添加一個斷點到方法中的第一行,而且按照以下來設置這個斷點:
鼠標停在這個斷點,右擊這個斷點,選擇 Edit Breakpoint
在彈出的斷點編輯框中的condition輸入框中,添加[notification.name isEqualToString:@"NSWindowWillOrderOffScreenNotification"]
在Action部分,添加 po notification.object
如 果父xcode已經處在運行狀態了,從新讓它運行debug,而後在生成的子xcode中,再編譯運行一個工程。父xcode中的斷點應該會停在 NSWindowWillOrderOffScreenNotification通知。觀察控制檯輸出的-[notification object]的值DVTBezelAlertPanel,這也是第一個值得你深刻關注的諸多私有類中的一員
你如今知道了有一個類的名字叫DVTBezelAlertPanel,更重要的是,你知道內存中有一個這個類的實例。不過不幸的是,你找到不到任何關於這個類的頭文件可以告訴你,這個類是否就是展現Xcode的警示框的。
實際上,仍是能夠獲取到這些信息的。儘管咱們沒有關於這個類的頭文件,但是你有一個調試器鏈接到子Xcode,內存中的信息照樣能夠告訴你關於這個類的相關信息,就如同你閱讀其頭文件同樣。
注 意:在這個系列的教程中,LLDB的輸出一般是伴隨在標準的控制檯輸出中的。任何以(lldb)打頭的行,能夠認爲是輸入行,在此處你能夠輸入一些命令。 三個點…輸出在控制檯,表示控制檯來打印不及,忽略了其中某些。若是在控制檯顯示了太多的打印日誌,能夠直接按 ? + K來清楚當前的輸出,而且從新接受輸出
確保你父Xcode是調試狀態的,程序停在斷點處,輸入如下的lldb命令道父Xcode的lldb控制檯:
1
2
3
|
(lldb) image lookup -rn DVTBezelAlertPanel
17 matches found
in
/Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework/Versions/A/DVTKit:
...(這裏的...是表示省略了上面那句命令的輸出內容,由於實在太多了)
|
這 句命令搜索任何的加載到xcode進程中的frameworks,libraries,plugins,查找名爲DVTBezelAlertPanel的 類的相關信息,而後輸出查找到的信息。觀察搜索結果列出的方法。你是否已經可以找到一些方法能夠用來關聯DVTBezelAlertPanel類和子 Xocde中出現的編譯成功/失敗的警示框?下面我提供了一些方法的列表,這些能夠幫助到你。(原文中,如下的列表是隱藏的,你能夠點擊展現它,做者鼓勵 你們本身先找找看,若是找不到,再打開下面的這個提示,因爲我使用的Markdown編輯器的限制,作不到這點,因此此處直接放出來了).
有幫助的方法
如下列出的DVTBezelAlertPanel類的方法,值得你進一步探索:
initWithIcon:message:parentWindow:duration
initWithIcon:message:controlView:duration:
controlView
以上的兩個初始話方法中的任何一個,基本上就能夠幫助你驗證是否關聯了類DVTBezelAlertPanel和出現的提示框中的內容了
注意:LLDB的image lookup命令只列出在內存中實現了的方法。當你使用這個查找某些類時候,它並不包含那些繼承於父類,可是子類並無重載的方法,也就是說它只列出本身實現了的方法。
確保依然停留在父Xcode的斷點出,在父類的LLDB控制檯中輸入如下命令來 檢測contentView
1
2
|
(lldb) po [notification.object controlView]
[nil](此處尖括號替換爲方括號) (這個是上句輸出的結果,yohunl注)
|
控 制臺輸出的是nil.(⊙o⊙)…,多是由於這個contentView在這個時候尚未被初始化吧。不要緊,咱們嘗試下一 個:initWithIcon:message:parentWindow:duration和 initWithIcon:message:controlView:duration: ,由於你已經瞭解,內存中已經存在DVTBezelAlertPanel類的實例了,這意味着這兩個初始化方法,已經被調用過了。你須要給這兩個方法添加 調試斷點,由於咱們沒有它的實現文件,因此這裏咱們用LLDB控制檯來添加斷點。而後再次觸發這個類的初始化。
父Xcode依然停留在斷點出,輸入如下命令
1
2
3
4
|
(lldb) rb
'DVTBezelAlertPanel\ initWithIcon:message:'
Breakpoint 1: 2 locations.
(lldb) br l
...
|
yohunl備註:在xcode7.2.1中,顯示的是
1
2
3
4
|
(lldb) rb
'DVTBezelAlertPanel\ initWithIcon:message:'
Breakpoint 2: 4 locations.
(lldb) br l
........
|
這個正則表達式形式的斷點將會給上面的兩個初始化方法都添加一個斷點,這是由於兩個方法都有一個相同的起始字符,正則表達式將匹配它們兩個。別忘記上面正則表達式中空格前的\符號,還有就是用單引號'來包含整個表達式,這樣LLDB才知道怎麼解析它
切換到子Xcode,從新編譯子工程(ctrl+B)。父Xcode將會命中initWithIcon:message:parentWindow:duration斷點
若是沒有命中斷點,檢查一下,是否是將斷點設在父Xcode中(假如你設在子xcode中,固然不起做用呀),是否是在子Xcode中編譯了一個工程。,由於找不到相應的源碼文件,Xcode將會斷點在方法的彙編代碼中。
如今,你在沒有源代碼的狀況下,斷點進入了一個方法。你須要一個方式來打印出傳遞給該方法的參數。是時候讓咱們談一談。。彙編了…:]
彙編之旅
當你面對私有API的時候,你要作的每每是分析寄存器(registers ),而不是像在擁有源碼狀況下的調試同樣使用調試符號(debug symbols )。瞭解寄存器(registers)在x86-64架構下的行爲,將會給你提供不少的幫助.
儘管不是一篇必讀文章,這篇文章是一篇很是好的關於x86 Mach-0 彙編的文章。在本教程的第三部分,你將會經過方法的部分反彙編代碼去了解方法究竟是作什麼的。不過如今,你須要的只是簡單的瞭解。
如下的寄存器和其是怎樣工做的,值得你關注:
$rdi:這個寄存器表明傳遞給方法的參數self,這也是第一個傳遞的參數。
$rsi:表示Selector,這是傳遞給的第二個參數
$rdx:傳遞給函數的第三個參數,也是咱們看到的Objective-C的第一個參數(由於self和Selector是隱含的參數)
$rcx:傳遞給函數的第四個參數,也是咱們看到的Objective-C的第2個參數(由於self和Selector是隱含的參數)
$r8:傳遞給函數的第五個參數。若是須要傳遞更多的參數,$r9將會做爲跟隨以後的第6個參數的棧幀
$rax:函數的返回值存放在此寄存器中。例如,當咱們執行完方法-[aClass description],$rax將會存放aClass對象的描述NSString。
注意:以上描述的不是絕對的。在某些二進制中,會使用不一樣的寄存器來存放不一樣類型的參數,例如:doubles使用$xmm寄存器組。上面的只是做爲一個快速的參考!
下面咱們採用如下的方法,來將上述的理論運用到實踐中來
1
2
3
4
5
6
7
8
9
10
11
|
@interface aClass : NSObject
- (NSString *)aMethodWithMessage:(NSString *)message;
@end
@implementation aClass
- (NSString *)aMethodWithMessage:(NSString *)message {
return
[NSString stringWithFormat:@
"Hey the message is: %@"
, message];
}
@end
|
使用以下的代碼來執行它:
1
2
|
aClass *aClassInstance = [[aClass alloc] init];
[aClassInstance aMethodWithMessage:@
"Hello World"
];
|
編譯後,對方法aMethodWithMessage的調用將會由Runtime層準換爲對objc_msgSend的調用,基本上相似於以下:
1
|
objc_msgSend(aClassInstance, @selector(aMethodWithMessage:), @
"Hello World"
)
|
aClass的方法aMethodWithMessage調用,會使得一些寄存機的內容被改變:
調用方法aMethodWithMessage前
$rdi: 存放aClass類型的一個實例變量
$rsi:存放SEL類型的aMethodWithMessage:,實際上它是一個chra * 類型的字符串(能夠經過在lldb中 輸入 po (SEL)$rsi來驗證)
$rdx:包含傳遞給方法的message,此處,是一個字符串 @"Hello World"
當調用方法結束後
$rax:存放方法執行後的返回值,在此處是一個NSString。在這個特定的例子中,存放的是字符串@"Hey the message is: Hello World"
x86寄存器
通 過以上的內容,你已經有了一份寄存器指南了,是時候從新審視DVTBezelAlertPanel的初始化方法 initWithIcon:message:parentWindow:duration:了。但願你的父Xcode的斷點還停留在此方法處。固然,若是 不是,也不要緊,從新運行子Xcode,再次停留在父類的斷點initWithIcon:message:parentWindow:duration: 處。記住,你是在尋找將類DVTBezelAlertPanel和顯示Xcode的編譯成功/失敗提示框之間的線索。
當程序斷點在initWithIcon:message:parentWindow:duration處,在LLDB控制檯輸入如下內容
1
|
(lldb) re re
|
這條命令是register read的縮寫,它是用來輸出當前你機器上可見的重要寄存器內容的命令。
運 用你所學到的關於x86寄存器的知識,去查看哪一個寄存器是用來存放message參數的和objc_msgSend方法的第四個參數。是否這個內容就是我 們所但願獲得的警示框的提示內容呢?(原文中,如下的列表是隱藏的,你能夠點擊展現它,做者鼓勵你們本身先找找看,若是找不到,再打開下面的這個提示,由 於我使用的markdown編輯器的限制,作不到這點,因此此處直接放出來了)
是的,你應該查看寄存器$rcx,你將會看到,它的內容就是message參數的內容,也就是顯示在xcode的編譯提示框中的提示信息
輸入如下命令來進一步深刻了解:
1
2
|
(lldb) po $rcx
Build Failed
|
注 意:Xcode輸出寄存器內容是採用默認的AT&T彙編格式的,在這種格式中,源操做符和目標操做符的位置是交換過的,意思是AT&T語 法第一個爲源操做數,第二個爲目的操做數,方向從左到右,這個同Intel的彙編格式是相反的。(譯者注:關於AT&T彙編,能夠參考http://blog.csdn.net/bigloomy/article/details/6581754)
看起來這個就是咱們要找的寄存器呀!
試着更改$rcx的內容爲一個新的字符串,看看是否是警示框的內容改變了:
1
2
3
4
5
6
7
8
|
(lldb) po [$rcx class]
__NSCFConstantString
//個人xcode7.2.1上輸出顯示的是 __NSCFString
(lldb) po id $a = @
"Womp womp!"
;
(lldb) p/x $a
(id) $a = 0x000061800203faa0
//yohunl備註,這裏是上一句p/x $a的輸出,在個人xcode7.2.1上,輸出的是(__NSCFString *) $a = 0x00006080026379c0 @"Womp womp!",在你的機子上輸出的地址也極可能是不同的,下一句中的地址要換成你機子上本處顯示的地址值
(lldb) re w $rcx 0x000061800203faa0
(lldb) c
|
應用程序將恢復運行。注意觀察顯示的編譯成功/失敗的提示框的內容是否是變成了咱們修改的字符串。你將會看到,它的確是變成了咱們設置的新字符串,這也驗證了咱們的假定- DVTBezelAlertPanel就是用來顯示這個提示信息的。
代碼注入(Injection)
你已經找到了你所須要的類,是時候,咱們經過代碼注入來擴展DVTBezelAlertPanel的行爲,在編譯提示框中展現lovely Rayrolling(人名)的頭像。
咱們採用的是 metthod swizzling技術。
你可能要swizzle來自不少不一樣的類的大量的方法,因此最佳建議是建立一個NSObject的category,在其中提供一個便捷的方法,來創建全部的swizzle邏輯。
在Xocde中,選擇File\New\File…,而後選擇 OS X\Source\Objective-C File,創建名稱爲MethodSwizzler的文件,確保它的形式是NSObject的category。
打開NSObject+MethodSwizzler.m,將其替換成以下的代碼:
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
|
#import "NSObject+MethodSwizzler.h"
// 1
#import
@implementation NSObject (MethodSwizzler)
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod
{
Class cls = [self class];
Method originalMethod;
Method swizzledMethod;
// 2
if
(isClassMethod) {
originalMethod = class_getClassMethod(cls, originalSelector);
swizzledMethod = class_getClassMethod(cls, swizzledSelector);
}
else
{
originalMethod = class_getInstanceMethod(cls, originalSelector);
swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
}
// 3
if
(!originalMethod) {
NSLog(@
"Error: originalMethod is nil, did you spell it incorrectly? %@"
, originalMethod);
return
;
}
// 4
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
|
其中的關鍵代碼都加了序號,下面一一解釋:
這是使用method swizzle所須要的頭文件
isClassMethod指示,這個方法是實例方法仍是類方法
若是不借助於編譯器的方法提示,那很容易拼錯上述的方法。這段代碼是用來檢查的,確保你的拼寫是正確的
這個是關鍵函數,用來交換方法的實現的
在頭文件NSObject+MethodSwizzler。h中添加方法swizzleWithOriginalSelector:swizzledSelector:isClassMethod的聲明,以下所示:
1
2
3
4
5
|
#import
@interface NSObject (MethodSwizzler)
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod;
@end
|
接下來,就能夠完成實際的swizzle了。建立一個新的名爲Rayrolling_DVTBezelAlertPanel的category,這個category一樣是NSObject的category。
替換建立的NSObject+Rayrolling_DVTBezelAlertPanel。m的代碼爲以下代碼
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
|
#import
// 2
@interface NSObject ()
// 3
- (id)initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4;
@end
// 4
@implementation NSObject (Rayrolling_DVTBezelAlertPanel)
// 5
+ (void)load
{
static dispatch_once_t onceToken;
// 6
dispatch_once(&onceToken, ^{
// 7
[NSClassFromString(@
"DVTBezelAlertPanel"
) swizzleWithOriginalSelector:@selector(initWithIcon:message:parentWindow:duration:) swizzledSelector:@selector(Rayrolling_initWithIcon:message:parentWindow:duration:) isClassMethod:NO];
});
}
// 8
- (id)Rayrolling_initWithIcon:(id)icon message:(id)message parentWindow:(id)window duration:(double)duration
{
// 9
NSLog(@
"Swizzle success! %@"
, self);
// 10
return
[self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];
}
@end
|
上面的代碼比較簡單,咱們來分析:
引入用來swizzling的頭文件
前向聲明全部你打算使用到的方法。雖然這不是必須的,可是這個使得編譯器可以智能感知完成你的代碼,另外,這也消除了編譯器提示的找不到方法聲明的警告
這是你實際上須要swizzle的方法
由於咱們不想從新聲明一個私有類,替代的方式是聲明一個category。
這個方法是觸發代碼注入的地方。你應該將代碼注入都放到load中。這個load是惟一的一個"一對多關係"的方法,也就是說,多個category都有load方法,那麼全部的category的load方法都可以獲得執行
由於load可能會被屢次執行,因此,使用dispatch_once確保只執行一次
swizzle前面聲明的方法爲你本身的實現。固然了,其中使用了NSClassFromString來動態的獲取內存中的類!
這是你寫的用來取代原來的方法的方法,建議是它採用獨特的命令方式,這樣從名字立馬知道它是作什麼的
輸出一下,確保swizzle成功了
因 爲你已經swizzle了原始的方法,那麼你調用swizzled後的方法(此處是[self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];),它將會調用的是原來的方法。這意味着你在原來的方法執行以前或者以後,添加任何你想要的代碼,甚至是更改傳遞 給原始方法的參數…固然了,這裏你已經在這麼作了
祝賀你,你已經成功的注入代碼到一個私有類的私有方法中了!編譯父xcode,而後在子xcode中編譯運行一個工程,查看父xcode的控制檯輸出,看是否是成功的wswizzled了。
1
|
Swizzle success! [DVTBezelAlertPanel: 0x11e42d300](因爲識別問題,此處將尖括號替換爲方括號)
|
接下來,你就能夠替換編譯成功/失敗的提示框上的圖標爲Rayrolling頭像啦。從這裏下載頭像資源Crispy from here,而後添加到工程中來,確保選擇了 Copy Items if Needed。
如今,導航到方法Rayrolling_initWithIcon:message:parentWindow:duration,將其代碼改成:
1
2
3
4
5
6
7
8
9
|
- (id)Rayrolling_initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4
{
if
(arg1) {
NSBundle *bundle = [NSBundle bundleWithIdentifier:@
"com.raywenderlich.Rayrolling"
];
NSImage *newImage = [bundle imageForResource:@
"IDEAlertBezel_Generic_Rayrolling.pdf"
];
return
[self Rayrolling_initWithIcon:newImage message:arg2 parentWindow:arg3 duration:arg4];
}
return
[self Rayrolling_initWithIcon:arg1 message:arg2 parentWindow:arg3 duration:arg4];
}
|
這 個方法首先檢查是否一個圖片參數被傳遞給了原方法,而後將其替換成咱們自定義的圖片。注意:此處你是使用[NSBundle bundleWithIdentifier:@"com。raywenderlich。Rayrolling"];來加載圖片的,這是由於xcode的 MainBundle並不包含咱們的資源。
從新編譯父Xcode,而後在子Xcode中編譯一個工程,你會看到
添加一個開關和持久化
設計這個plugin是用來娛樂用的,因此你確定須要一個開關,讓它起做用或者不起做用。咱們經過NSUserDefaults來持久化存放使它起做用或者不起做用的變量.
導航到Rayrolling.h ,添加以下代碼
1
|
+ (BOOL)isEnabled;
|
在Rayrolling.m文件中添加
1
2
3
4
5
6
7
|
+ (BOOL)isEnabled {
return
[[NSUserDefaults standardUserDefaults] boolForKey:@
"com.raywenderlich.Rayrolling.shouldbeEnable"
];
}
+ (void)setIsEnabled:(BOOL)shouldBeEnabled {
[[NSUserDefaults standardUserDefaults] setBool:shouldBeEnabled forKey:@
"com.raywenderlich.Rayrolling.shouldbeEnable"
];
}
|
你已經有了持久化你的選擇的邏輯,下面是將它關聯到GUI上去
回到Rayrolling.m中,修改-(void)doMenuAction的代碼爲下面的:
1
2
3
4
|
- (void)doMenuAction:(NSMenuItem *)menuItem {
[Rayrolling setIsEnabled:![Rayrolling isEnabled]];
menuItem.title = [Rayrolling isEnabled] ? @
"Disable Rayrolling"
: @
"Enable Rayrolling"
;
}
|
這是個用來切換的bool值,啓用或者禁用Rayrolling
最後,更改在didApplicationFinishLaunchingNotification:中的菜單項的初始化代碼,改成以下:
1
2
3
4
5
6
7
8
|
NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@
"Edit"
];
if
(menuItem) {
[[menuItem submenu] addItem:[NSMenuItem separatorItem]];
NSString *title = [Rayrolling isEnabled] ? @
"Disable Rayrolling"
: @
"Enable Rayrolling"
;
NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(doMenuAction:) keyEquivalent:@
""
];
[actionMenuItem setTarget:self];
[[menuItem submenu] addItem:actionMenuItem];
}
|
這個菜單項將會保留你選擇的是否啓用的邏輯,即便Xcode重啓後也不要緊,由於你的選擇已經持久化存儲了。
導航到文件NSObject+Rayrolling_DVTBezelAlertPanel.m,添加一行頭文件
1
|
#import "Rayrolling.h"
|
最後,打開方法Rayrolling_initWithIcon:message:parentWindow:duration:,將
1
|
if
(arg1) {
|
替換爲
1
|
if
([Rayrolling isEnabled] && arg1) {
|
構建並運行程序,以便更改插件的行爲。
如今,你建立了一個能夠用來改變xcode編譯成功/失敗提示框圖標和內容的插件,而且它仍是可以被選擇是否打開。這一天的工做成果至關不錯,難道不是嗎???
接下來作什麼?
你能夠從這裏 下載完整的demo工程。
你已經取得了很大的進步,可是依然有不少事情要作!在本教程的第二部分,你將會學習到DTrace基本知識,而且深刻到一些LLDB的高級特性,諸如查找正在運行的進程,好比正在運行的xcode進程。
若是你想更進一步,那麼在你前往教程3以前,你還有一些工做要作。在教程3中,你將會看到大量的彙編代碼。確保在這以前,你已經開始瞭解了相關的x86_64彙編知識,這裏有2篇Mike Ash的介紹分析彙編的系列文章 文章1,文章2,能夠給你提供相關的幫助。