怎樣創(chuàng)建一個(gè)xcode插件 第一部分/3部分

本文翻譯自 https://www.raywenderlich.com/94020/creating-an-xcode-plugin-part-1

原作者:Derek Selander

譯者:@yohunl

譯者注:原文使用的是xcode6.3.2,我翻譯的時(shí)候,使用的是xcode7.2.1,經(jīng)過驗(yàn)證,文章中說說的依然是有效的.在文中你可以學(xué)習(xí)到一系列的技能,非常值得一看

蘋果的"一個(gè)足以應(yīng)付所有"策略使得它的產(chǎn)品越來越像一個(gè)難以下咽的藥丸.盡管蘋果已經(jīng)將一些工作流帶給了ios/os x的開發(fā)者,我們?nèi)匀幌Mㄟ^插件來使得xcode更加順手!

雖然蘋果并沒有提供任何的官方文檔來指導(dǎo)我們?nèi)绾蝿?chuàng)建一個(gè)xcode插件,但是開發(fā)者社區(qū)做了大量的工作開發(fā)了一些非常有用的工具,通過這些工具,可以用來幫助開發(fā)者.

自動(dòng)完成圖片名的插件,到 清除緩存的插件 再到 使得xcode變成一個(gè)vim編輯器的插件,xcode的插件社區(qū)已經(jīng)拓展了我們的思維,我們可以讓xcode變得更加智能

在這個(gè)不算短的三部分教程中,你將創(chuàng)建一個(gè)xcode的插件來娛樂你的同事,其特色莫過于顯示的內(nèi)容并不是他所期望看到的肴裙!盡管這個(gè)插件是很輕量級(jí)的,你仍然可以學(xué)習(xí)到很多知識(shí),例如,通過調(diào)試xcode,怎樣找出你感興趣的元素并且修改它,怎樣將系統(tǒng)的功能函數(shù)替換為你自己的函數(shù)(通過swizzle技術(shù))!

你將會(huì)使用 x86匯編知識(shí),代碼定技能,LLDB調(diào)試技能來查閱未公開的私有framework,并且要探索這些私有framework中的私有apis,還要使用 method swizzleing來進(jìn)行代碼的注入.正因?yàn)橛羞@么多內(nèi)容,所以本教程的講解速度會(huì)很快.在繼續(xù)之前,請(qǐng)務(wù)必確定你已經(jīng)掌握了相關(guān)的 iOS/OS X的開發(fā).

使用swift來開發(fā)插件,還是一個(gè)比較復(fù)雜的專題,并且swift的調(diào)試工具依然比Objective-C要弱很多.就目前而言,這意味著插件開發(fā)的最佳選擇(本教程A状住)是的Objective-C

開始


為了慶祝 惡作劇你的同事日,你的xcode插件將會(huì)Rayroll你的受害者.等等... 什么是Rayrolling?它是一個(gè)免費(fèi)和無版權(quán)的Rayrolling瑞克搖擺-就是你看到的內(nèi)容并不是你期望的內(nèi)容,有點(diǎn)掛羊頭賣狗肉的意思.當(dāng)你完成了這個(gè)系列,你的插件將會(huì)更改xcode顯示的內(nèi)容:

  1. 用Ray's的頭像來替換xcode的某些警示框(例如, Build成功或者失敗的xcode提示框)


    Xcode_Swizzle_DispalAlert-700x215.png
  2. 替換xcode的標(biāo)題內(nèi)容為Ray的熱門歌曲的一句歌詞,Never Gonna Live You Up
    Plugin_Swizzled_Titlebar-700x56.png
  3. 替換所有的xcode中的搜索文檔內(nèi)容為一個(gè)視頻 Rayroll’d video
    Plugin_Swizzle_Documentation-700x288.png

在教程的第一部分,我們將聚焦于尋找到負(fù)責(zé)展示"Build成功"警示框的那個(gè)類,并且將其圖片改為Ray的頭像這張圖片

安裝插件管理插件 Alcatraz


在開始之前,需要先安裝Alcatraz,它是xcode插件管理工具.
典型的安裝Alcatraz的方式是通過命令行

  curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh

當(dāng)這條命令結(jié)束后,重啟xcode.你可能會(huì)看到一個(gè)提示 Alcatraz bundle的警示框;點(diǎn)擊 Load Bundle 繼續(xù),以便xcode能夠加載Alcatraz插件,這樣這個(gè)Alcatraz插件才能起作用

Xcode6.3.2-480x270.png

注意:如果你一不小心點(diǎn)擊了"skip bundle",你可以通過命令行輸入以下命令來重新顯示它!
defaults delete com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode-7.2.1
以上的Xcode-7.2.1是你機(jī)子上的xcode版本號(hào),如果你的不是7.2.1,改為你的對(duì)應(yīng)的版本號(hào)就可以了

你將會(huì)在xcode的Window菜單下看到一個(gè)新的菜單項(xiàng):Package Manager.創(chuàng)建一個(gè)xcode插件,需要你通過設(shè)置Build Settings來運(yùn)行另一個(gè)新的xcode的實(shí)例來加載才可以,這是一個(gè)枯燥和乏味的過程(如果想知道這個(gè)枯燥的過程,可以參考我的文章xcode7 插件制作入門),幸好,已經(jīng)有人替你完成了這件事情了,有人開發(fā)了一個(gè)xcode的工程模板,可以讓你很方便的創(chuàng)建一個(gè)插件工程.

打開Alcatraz(Window->Package Manager).在Alcatraz的搜索框中輸入Xcode Plugin.務(wù)必確保你選中了搜索框中的AllTemplates兩個(gè)屬性.一旦你搜索到了.單機(jī)其左邊的Install來安裝它!!

XcodeAlcatrazPlugin-700x208.png

如果你搜索不到,也沒關(guān)系,你可以前往 https://github.com/kattrali/Xcode-Plugin-Template自己下載下來的方式來加載它,具體安裝方式可以見工程的說明

一旦Alcatraz下載完了Xcode Plugin插件,你就可以創(chuàng)建一個(gè)插件工程了(File->New -> Project...),選擇這個(gè)新的OS X ->Xcode Plugin ->Xcode Plugin模板,然后,點(diǎn)擊下一步.

XcodePluginSelection-480x282.png

給工程取名字Rayrolling,組織的標(biāo)識(shí)符為com.raywenderlich(這一步非常重要),選擇Objective-C作為代碼語(yǔ)言..保存工程到任何一個(gè)你想放置的目錄中.
Plugin_Xcode_Setup-480x281.png

Hello World插件模板


編譯,然后運(yùn)行這個(gè)Rayroll工程,你將會(huì)看到一個(gè)新的xcode實(shí)例出現(xiàn).這個(gè)xcode實(shí)例在Edit菜單欄下多了一個(gè)菜單項(xiàng)Do Action:

XcodePluginHelloWorld-232x320.png

選擇這個(gè)菜單項(xiàng),將會(huì)出現(xiàn)一個(gè)模態(tài)的彈出框:
Screen-Shot-2015-05-11-at-8.48.27-PM-480x239.png

從xcode5開始,插件都只能運(yùn)行在特定版本的xcode中.這也就意味著當(dāng)新的xcode更新安裝后,所有的第三方插件都將失效,除非你添加了該版本xcode的UUID.如果部分模板沒有起作用,你也沒看到一個(gè)新的菜單項(xiàng),可能的原因之一就是因?yàn)闆]有對(duì)應(yīng)版本的UUID,你需要添加對(duì)應(yīng)該版本xcode的支持.
為了添加UUID,首先是在命令行中運(yùn)行以下命令

defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID

這條命令會(huì)輸出當(dāng)前版本xcode的UUID.打開Rayroll工程的Info.plist文件.導(dǎo)航到DVTPlugInCompatibilityUUID,添加它

DVTCompatibilityUUIDs.gif

注意:通過本教程,你會(huì)運(yùn)行和修改已經(jīng)安裝了的插件.這將會(huì)改變xcode的默認(rèn)行為,當(dāng)然,這也可能會(huì)導(dǎo)致xcode crash!!如果你想禁止某個(gè)插件,你可以手動(dòng)的通過終端去刪除它

  cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/
      rm -r Rayrolling.xcplugin/

然后重啟一下xcode

找到我們要修改的xcode的某項(xiàng)特性


最直接最有效的得到幕布后發(fā)生什么的方式是 通過注冊(cè)一個(gè)NSNotification observer 來監(jiān)聽所有的xcode事件.通過xcode和監(jiān)聽這些消息通知,你將會(huì)深入到一些內(nèi)部類的內(nèi)部.

打開Rayrollling.m,在類中添加如下的屬性

@property (nonatomic, strong) NSMutableSet *notificationSet;

這個(gè)NSMutableSet用來存儲(chǔ)所有的xcode的控制臺(tái)打印出來的NSNotification的名字
下一步,在initWithBundle:中,if (self = [super init]) {之后,添加如下代碼

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil];
 
self.notificationSet = [NSMutableSet new];

name參數(shù)傳遞nil指示,偶們需要監(jiān)聽xcode的所有NSNotification.

現(xiàn)在,實(shí)現(xiàn)handleNotification:方法:

- (void)handleNotification:(NSNotification *)notification {
  if (![self.notificationSet containsObject:notification.name]) {
    NSLog(@"%@, %@", notification.name, [notification.object class]);
    [self.notificationSet addObject:notification.name];
  }
}

handleNotification:檢查獲取到的通知名稱是不是在notificationSet中,如果不是,則在控制臺(tái)打印出它的通知名和通知多對(duì)應(yīng)的類.然后添加到notificationSet中.通過這種方式,你只會(huì)在控制臺(tái)看到每一種類型的通知一次

下一步,找到添加menu item的聲明,將其替換成下面的代碼

NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Reset Logger"
 action:@selector(doMenuAction) keyEquivalent:@""];

這段代碼只是簡(jiǎn)單的更改了NSMenuItem的標(biāo)題,以便讓你知道,當(dāng)你點(diǎn)擊它的時(shí)候,它將會(huì)重置存放**NSNotification **的set對(duì)象.

最后,替換doMenuAction的實(shí)現(xiàn)代碼為下面的

- (void)doMenuAction {
  [self.notificationSet removeAllObjects];
}

這個(gè)菜單項(xiàng)將會(huì)重置所有存放在notificationSet屬性中的通知.這樣做的目的是讓你在控制臺(tái)中很容易的觀察到你感興趣的通知,而不至于被控制臺(tái)的重復(fù)消息所刷屏.讓你更加專注.

再一次編譯運(yùn)行,請(qǐng)確認(rèn)你分清了哪個(gè)是你工程的xcode(也就是父xcode),哪個(gè)是你debug出來的一個(gè)xcode實(shí)例(子xcode),為什么要分清楚呢?因?yàn)槲覀兊拿恳淮胃淖?當(dāng)重新debug的時(shí)候,debug出來的xcode中已經(jīng)起作用了,而父xcode,只有等到你重啟后,才能看到效果的.

在子xcode中,隨便點(diǎn)擊點(diǎn)擊按鈕,打開一些窗口,瀏覽瀏覽程序,你會(huì)在父xcode的控制臺(tái)中看到消息的觸發(fā).

查找和監(jiān)測(cè)編譯的提示框


現(xiàn)在,你已經(jīng)學(xué)會(huì)了基本的查看由xcode本身引起的通知(NSNotification),現(xiàn)在你需要明確的找出來顯示編譯狀態(tài)的提示框所關(guān)聯(lián)的類是哪一個(gè).

運(yùn)行xcode插件,在子xcode中,打開任何一個(gè)工程,確保打開了xcode設(shè)置中的bezel notifications,Succeeds和Fails中的bezel notifications都要打開.當(dāng)然了,請(qǐng)?jiān)俅未_定你操作的是子xcode實(shí)例!


rayroll-bezel-685x500.png

通過你在xcode的edit菜單中創(chuàng)建的 菜單項(xiàng)Reset Logger來重置notificationSet,然后運(yùn)行你的子code(上面讓你在子xcode中打開了任何一個(gè)工程,現(xiàn)在你在子xcode中運(yùn)行這個(gè)打開的工程)

當(dāng)子xcode的工程編譯結(jié)果出來后(或者成功,或者失敗都沒關(guān)系),關(guān)注父xcode中控制臺(tái)輸出的信息.粗略的瀏覽一遍,看是否有能引起你關(guān)注的通知.你能夠發(fā)現(xiàn)一些值得你進(jìn)一步關(guān)注的notifications么?下面的這些可能能給你一些幫助(原文中,以下的列表是隱藏的,你可以點(diǎn)擊展示它,作者鼓勵(lì)大家自己先找找看,如果找不到,再打開下面的這個(gè)提示,由于我使用的markdown編輯器的限制,做不到這點(diǎn),所以此處直接放出來了)

下面的一些項(xiàng)值得你進(jìn)一步關(guān)注:

  • NSWindowWillOrderOffScreenNotification, DVTBezelAlertPanel
  • NSWindowDidOrderOffScreenNotification, DVTBezelAlertPanel
  • NSWindowDidOrderOffScreenAndFinishAnimatingNotification, DVTBezelAlertPanel


    XcodeNotificationListeningSourceKitCrash-700x272.png

你應(yīng)該挑其中一個(gè),并進(jìn)一步探索它,看看是不是可以從中得到一些重要的信息.例如 NSWindowWillOrderOffScreenNotification是干什么的? 很好,你選擇了進(jìn)一步探索NSWindowWillOrderOffScreenNotification.

回到父xcode中的Rayrolled.m文件,定位到handleNotification:方法,添加一個(gè)斷點(diǎn)到方法中的第一行,并且按照如下來設(shè)置這個(gè)斷點(diǎn):

Xcode_Add_Symbolic_Breakpoint-700x288.png

  1. 鼠標(biāo)停在這個(gè)斷點(diǎn),右擊這個(gè)斷點(diǎn),選擇 Edit Breakpoint
  2. 在彈出的斷點(diǎn)編輯框中的condition輸入框中,添加[notification.name isEqualToString:@"NSWindowWillOrderOffScreenNotification"]
  3. 在Action部分,添加 po notification.object
  4. 如果父xcode已經(jīng)處在運(yùn)行狀態(tài)了,重新讓它運(yùn)行debug,然后在生成的子xcode中,再編譯運(yùn)行一個(gè)工程.父xcode中的斷點(diǎn)應(yīng)該會(huì)停在NSWindowWillOrderOffScreenNotification通知.觀察控制臺(tái)輸出的-[notification object]的值DVTBezelAlertPanel,這也是第一個(gè)值得你深入關(guān)注的諸多私有類中的一員

你現(xiàn)在知道了有一個(gè)類的名字叫DVTBezelAlertPanel,更重要的是,你知道,內(nèi)存中有一個(gè)這個(gè)類的實(shí)例.不過不幸的是,你找到不到任何關(guān)于這個(gè)類的頭文件能夠告訴你,這個(gè)類是否就是展示xcode的警示框的.

實(shí)際上,還是可以獲取到這些信息的.盡管我們沒有關(guān)于這個(gè)類的頭文件,可是你有一個(gè)調(diào)試器連接到子xcode,內(nèi)存中的信息照樣可以告訴你關(guān)于這個(gè)類的相關(guān)信息,就如同你閱讀其頭文件一樣.

注意:在這個(gè)系列的教程中,LLDB的輸出通常是伴隨在標(biāo)準(zhǔn)的控制臺(tái)輸出中的.任何以(lldb)打頭的行,可以認(rèn)為是輸入行,在此處你可以輸入一些命令.三個(gè)點(diǎn)...輸出在控制臺(tái),表示控制臺(tái)來打印不及,忽略了其中某些.如果在控制臺(tái)顯示了太多的打印日志,可以直接按** ? + K**來清楚當(dāng)前的輸出,并且重新接受輸出

確保你父xcode是調(diào)試狀態(tài)的,程序停在斷點(diǎn)處,輸入以下的lldb命令道父xcode的lldb控制臺(tái):

(lldb) image lookup -rn DVTBezelAlertPanel
17 matches found in /Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework/Versions/A/DVTKit: 
...(這里的...是表示省略了上面那句命令的輸出內(nèi)容,因?yàn)閷?shí)在太多了)

這句命令搜索任何的加載到xcode進(jìn)程中的frameworks,libraries,plugins,查找名為DVTBezelAlertPanel的類的相關(guān)信息,然后輸出查找到的信息.觀察搜索結(jié)果列出的方法.你是否已經(jīng)能夠找到一些方法可以用來關(guān)聯(lián)DVTBezelAlertPanel類和子xocde中出現(xiàn)的編譯成功/失敗的警示框?下面我提供了一些方法的列表,這些可以幫助到你.(原文中,以下的列表是隱藏的,你可以點(diǎn)擊展示它,作者鼓勵(lì)大家自己先找找看,如果找不到,再打開下面的這個(gè)提示,由于我使用的markdown編輯器的限制,做不到這點(diǎn),所以此處直接放出來了)

有幫助的方法
以下列出的DVTBezelAlertPanel類的方法,值得你進(jìn)一步探索

  • initWithIcon:message:parentWindow:duration
  • initWithIcon:message:controlView:duration:
  • controlView
    以上的兩個(gè)初始話方法中的任何一個(gè),基本上就可以幫助你驗(yàn)證是否關(guān)聯(lián)了類DVTBezelAlertPanel和出現(xiàn)的提示框中的內(nèi)容了

注意:LLDB的image lookup命令只列出在內(nèi)存中實(shí)現(xiàn)了的方法.當(dāng)你使用這個(gè)查找某些類時(shí)候,它并不包含那些繼承于父類,但是子類并沒有重載的方法,也就是說它只列出自己實(shí)現(xiàn)了的方法.

確保依然停留在父xcode的斷點(diǎn)出,在父類的LLDB控制臺(tái)中輸入以下命令來 檢測(cè)contentView

(lldb) po [notification.object controlView]
<nil> (這個(gè)是上句輸出的結(jié)果,yohunl注)

控制臺(tái)輸出的是nil.(⊙o⊙)…,可能是因?yàn)檫@個(gè)contentView在這個(gè)時(shí)候還沒有被初始化吧.沒關(guān)系,我們嘗試下一個(gè):initWithIcon:message:parentWindow:durationinitWithIcon:message:controlView:duration: ,因?yàn)槟阋呀?jīng)了解,內(nèi)存中已經(jīng)存在DVTBezelAlertPanel類的實(shí)例了,這意味著這兩個(gè)初始化方法,已經(jīng)被調(diào)用過了.你需要給這兩個(gè)方法添加調(diào)試斷點(diǎn),因?yàn)槲覀儧]有它的實(shí)現(xiàn)文件,所以這里我們用LLDB控制臺(tái)來添加斷點(diǎn).然后再次觸發(fā)這個(gè)類的初始化.
父xcode依然停留在斷點(diǎn)出,輸入以下命令

(lldb) rb 'DVTBezelAlertPanel\ initWithIcon:message:'
Breakpoint 1: 2 locations.
(lldb) br l
...

yohunl備注:在xcode7.2.1中,顯示的是

(lldb) rb 'DVTBezelAlertPanel\ initWithIcon:message:'
Breakpoint 2: 4 locations.
(lldb) br l
........

這個(gè)正則表達(dá)式形式的斷點(diǎn)將會(huì)給上面的兩個(gè)初始化方法都添加一個(gè)斷點(diǎn),這是因?yàn)閮蓚€(gè)方法都有一個(gè)相同的起始字符,正則表達(dá)式將匹配它們兩個(gè).別忘記上面正則表達(dá)式中空格前的**符號(hào),還有就是用單引號(hào)'來包含整個(gè)表達(dá)式,這樣LLDB才知道怎么解析它

切換到子xcode,重新編譯子工程(ctrl+B).父xcode將會(huì)命中initWithIcon:message:parentWindow:duration斷點(diǎn)

duandian.png

如果沒有命中斷點(diǎn),檢查一下,是不是將斷點(diǎn)設(shè)在父xcode中(假如你設(shè)在子xcode中,當(dāng)然不起作用呀),是不是在子xcode中編譯了一個(gè)工程.,因?yàn)檎也坏较鄳?yīng)的源碼文件,xcode將會(huì)斷點(diǎn)在方法的匯編代碼中.

現(xiàn)在,你在沒有源代碼的情況下,斷點(diǎn)進(jìn)入了一個(gè)方法.你需要一個(gè)方式來打印出傳遞給該方法的參數(shù).是時(shí)候讓我們談一談..匯編了...:]


AssemblyRage-700x328-2.png

匯編之旅


當(dāng)你面對(duì)私有api的時(shí)候,你要做的往往是分析寄存器(registers ),而不是像在擁有源碼情況下的調(diào)試一樣使用調(diào)試符號(hào)(debug symbols ).了解寄存器(registers)在x86-64架構(gòu)下的行為,將會(huì)給你提供很多的幫助

盡管不是一篇必讀文章,這篇文章是一篇非常好的關(guān)于x86 Mach-0 匯編的文章.在本教程的第三部分,你將會(huì)通過方法的部分反匯編代碼去了解方法到底是做什么的.不過現(xiàn)在,你需要的只是簡(jiǎn)單的了解.

以下的寄存器和其是怎樣工作的,值得你關(guān)注:

  • $rdi:這個(gè)寄存器代表傳遞給方法的參數(shù)self,這也是第一個(gè)傳遞的參數(shù).
  • $rsi:表示Selector,這是傳遞給的第二個(gè)參數(shù)
  • $rdx:傳遞給函數(shù)的第三個(gè)參數(shù),也是我們看到的Objective-C的第一個(gè)參數(shù)(因?yàn)閟elf和Selector是隱含的參數(shù))
  • $rcx:傳遞給函數(shù)的第四個(gè)參數(shù),也是我們看到的Objective-C的第2個(gè)參數(shù)(因?yàn)閟elf和Selector是隱含的參數(shù))
  • $r8:傳遞給函數(shù)的第五個(gè)參數(shù).如果需要傳遞更多的參數(shù),$r9將會(huì)作為跟隨之后的第6個(gè)參數(shù)的棧幀
  • $rax:函數(shù)的返回值存放在此寄存器中.例如,當(dāng)我們執(zhí)行完方法–[aClass description],$rax將會(huì)存放aClass對(duì)象的描述NSString.

注意:以上描述的不是絕對(duì)的.在某些二進(jìn)制中,會(huì)使用不同的寄存器來存放不同類型的參數(shù),例如:doubles使用$xmm寄存器組.上面的只是作為一個(gè)快速的參考!

下面我們采用以下的方法,來將上述的理論運(yùn)用到實(shí)踐中來

@interface aClass : NSObject
- (NSString *)aMethodWithMessage:(NSString *)message;
@end
 
@implementation aClass 
 
- (NSString *)aMethodWithMessage:(NSString *)message {
  return [NSString stringWithFormat:@"Hey the message is: %@", message]; 
}
 
@end

使用如下的代碼來執(zhí)行它:

aClass *aClassInstance = [[aClass alloc] init];
[aClassInstance aMethodWithMessage:@"Hello World"];

編譯后,對(duì)方法aMethodWithMessage的調(diào)用將會(huì)由Runtime層準(zhǔn)換為對(duì)objc_msgSend的調(diào)用,基本上類似于如下:

objc_msgSend(aClassInstance, @selector(aMethodWithMessage:), @"Hello World")

aClass的方法aMethodWithMessage調(diào)用,會(huì)使得一些寄存機(jī)的內(nèi)容被改變:
調(diào)用方法aMethodWithMessage前

  • $rdi: 存放aClass類型的一個(gè)實(shí)例變量
  • $rsi:存放SEL類型的aMethodWithMessage:,實(shí)際上它是一個(gè)**chra * **類型的字符串(可以通過在lldb中 輸入 po (SEL)$rsi來驗(yàn)證)
  • $rdx:包含傳遞給方法的message,此處,是一個(gè)字符串** @"Hello World" **
    當(dāng)調(diào)用方法結(jié)束后
  • $rax:存放方法執(zhí)行后的返回值,在此處是一個(gè)NSString.在這個(gè)特定的例子中,存放的是字符串@"Hey the message is: Hello World"

x86寄存器


通過以上的內(nèi)容,你已經(jīng)有了一份寄存器指南了,是時(shí)候,重新審視DVTBezelAlertPanel的初始化方法initWithIcon:message:parentWindow:duration:了.希望你的父xcode的斷點(diǎn)還停留在此方法處.當(dāng)然,如果不是,也沒關(guān)系,重新運(yùn)行子xcode,再次停留在父類的斷點(diǎn)initWithIcon:message:parentWindow:duration:處.記住,你是在尋找將類DVTBezelAlertPanel和顯示xcode的編譯成功/失敗提示框之間的線索.

當(dāng)程序斷點(diǎn)在initWithIcon:message:parentWindow:duration處,在LLDB控制臺(tái)輸入以下內(nèi)容

(lldb) re re

這條命令是register read的縮寫,它是用來輸出當(dāng)前你機(jī)器上可見的重要寄存器內(nèi)容的命令.

運(yùn)用你所學(xué)到的關(guān)于x86寄存器的知識(shí),去查看哪個(gè)寄存器是用來存放message參數(shù)的和objc_msgSend方法的第四個(gè)參數(shù).是否這個(gè)內(nèi)容就是我們所希望得到的警示框的提示內(nèi)容呢?(原文中,以下的列表是隱藏的,你可以點(diǎn)擊展示它,作者鼓勵(lì)大家自己先找找看,如果找不到,再打開下面的這個(gè)提示,由于我使用的markdown編輯器的限制,做不到這點(diǎn),所以此處直接放出來了)

是的,你應(yīng)該查看寄存器$rcx,你將會(huì)看到,它的內(nèi)容就是message參數(shù)的內(nèi)容,也就是顯示在xcode的編譯提示框中的提示信息
輸入以下命令來進(jìn)一步深入了解:

(lldb) po $rcx 
Build Failed

注意:xcode輸出寄存器內(nèi)容是采用默認(rèn)的AT&T*匯編格式的,在這種格式中,源操作符和目標(biāo)操作符的位置是交換過的,意思是AT&T語(yǔ)法第一個(gè)為源操作數(shù)兜辞,第二個(gè)為目的操作數(shù),方向從左到右,這個(gè)同Intel的匯編格式是相反的 *,關(guān)于AT&T匯編,可以參考http://blog.csdn.net/bigloomy/article/details/6581754
看起來這個(gè)就是我們要找的寄存器呀

試著更改$rcx的內(nèi)容為一個(gè)新的字符串,看看是不是警示框的內(nèi)容改變了:

(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!",在你的機(jī)子上輸出的地址也很可能是不一樣的,下一句中的地址要換成你機(jī)子上本處顯示的地址值
(lldb) re w $rcx 0x000061800203faa0
(lldb) c
Womp_Womp_Xcode-700x275.png

應(yīng)用程序?qū)⒒謴?fù)運(yùn)行.注意觀察顯示的編譯成功/失敗的提示框的內(nèi)容是不是變成了我們修改的字符串.你將會(huì)看到,它的確是變成了我們?cè)O(shè)置的新字符串,這也驗(yàn)證了我們的假定- DVTBezelAlertPanel就是用來顯示這個(gè)提示信息的.

代碼注入(Injection)


你已經(jīng)找到了你所需要的類,是時(shí)候,我們通過代碼注入來擴(kuò)展DVTBezelAlertPanel的行為,在編譯提示框中展示lovely Rayrolling(人名)的頭像.

我們采用的是 metthod swizzling技術(shù)

你可能要swizzle來自很多不同的類的大量的方法,所以最佳建議是創(chuàng)建一個(gè)NSObjectcategory,在其中提供一個(gè)便捷的方法,來建立所有的swizzle邏輯.

在xocde中,選擇File\New\File…,然后選擇 OS X\Source\Objective-C File,建立名稱為MethodSwizzler的文件,確保它的形式是NSObject的category.

打開NSObject+MethodSwizzler.m,將其替換成如下的代碼:

#import "NSObject+MethodSwizzler.h"
 
// 1
#import <objc/runtime.h>
 
@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

其中的關(guān)鍵代碼都加了序號(hào),下面一一解釋:

  1. 這是是使用method swizzle所需要的頭文件
  2. isClassMethod指示,這個(gè)方法是實(shí)例方法還是類方法
  3. 如果不借助于編譯器的方法提示,那很容易拼錯(cuò)上述的方法.這段代碼是用來檢查的,確保你的拼寫是正確的
  4. 這個(gè)是關(guān)鍵函數(shù),用來交換方法的實(shí)現(xiàn)的

在頭文件NSObject+MethodSwizzler.h中添加方法swizzleWithOriginalSelector:swizzledSelector:isClassMethod的聲明,如下所示:

#import <Foundation/Foundation.h>
 
@interface NSObject (MethodSwizzler)
+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod;
 
@end

接下來,就可以完成實(shí)際的swizzle了.創(chuàng)建一個(gè)新的名為Rayrolling_DVTBezelAlertPanel的category,這個(gè)category同樣是NSObject的category.
替換創(chuàng)建的NSObject+Rayrolling_DVTBezelAlertPanel.m的代碼為如下代碼

#import "NSObject+Rayrolling_DVTBezelAlertPanel.h" 
 
// 1
#import "NSObject+MethodSwizzler.h"
#import <Cocoa/Cocoa.h>
 
// 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

上面的代碼比較簡(jiǎn)單,我們來分析:

  1. 引入用來swizzling的頭文件
  2. 前向聲明所有你打算使用到的方法.雖然這不是必須的,但是這個(gè)使得編譯器能夠智能感知完成你的代碼,另外,這也消除了編譯器提示的找不到方法聲明的警告
  3. 這是你實(shí)際上需要swizzle的方法
  4. 因?yàn)槲覀儾幌胫匦侣暶饕粋€(gè)私有類,替代的方式是聲明一個(gè)category.
  5. 這個(gè)方法是觸發(fā)代碼注入的地方.你應(yīng)該將代碼注入都放到load中.這個(gè)load是唯一的一個(gè)"一對(duì)多關(guān)系"的方法,也就是說,多個(gè)category都有l(wèi)oad方法,那么所有的category的load方法都能夠得到執(zhí)行
  6. 因?yàn)?strong>load可能會(huì)被多次執(zhí)行,所以,使用dispatch_once確保只執(zhí)行一次
  7. swizzle前面聲明的方法為你自己的實(shí)現(xiàn).當(dāng)然了,其中使用了NSClassFromString來動(dòng)態(tài)的獲取內(nèi)存中的類!
  8. 這是你寫的用來取代原來的方法的方法,建議是它采用獨(dú)特的命令方式,這樣從名字立馬知道它是做什么的
  9. 輸出一下,確保swizzle成功了
  10. 因?yàn)槟阋呀?jīng)swizzle了原始的方法,那么你調(diào)用swizzled后的方法(此處是[self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];),它將會(huì)調(diào)用的是原來的方法.這意味著你在原來的方法執(zhí)行之前或者之后,添加任何你想要的代碼,甚至是更改傳遞給原始方法的參數(shù)...當(dāng)然了,這里你已經(jīng)在這么做了

祝賀你,你已經(jīng)成功的注入代碼到一個(gè)私有類的私有方法中了!編譯父xcode,然后在子xcode中編譯運(yùn)行一個(gè)工程,查看父xcode的控制臺(tái)輸出,看是不是成功的wswizzled了.

 Swizzle success! <DVTBezelAlertPanel: 0x11e42d300>

接下來,你就可以替換編譯成功/失敗的提示框上的圖標(biāo)為Rayrolling頭像啦.從這里下載頭像資源Crispy from here,然后添加到工程中來,確保選擇了 Copy Items if Needed.

現(xiàn)在,導(dǎo)航到方法Rayrolling_initWithIcon:message:parentWindow:duration,將其代碼改為:

- (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];
}

這個(gè)方法首先檢查是否一個(gè)圖片參數(shù)被傳遞給了原方法,然后將其替換成我們自定義的圖片.注意:此處你是使用[NSBundle bundleWithIdentifier:@"com.raywenderlich.Rayrolling"];來加載圖片的,這是因?yàn)閤code的MainBundle并不包含我們的資源.

重新編譯父xcode,然后在子xcode中編譯一個(gè)工程,你會(huì)看到


Xcode_Alert_Closeup.png

添加一個(gè)開關(guān)和持久化


你設(shè)計(jì)這個(gè)plugin是用來娛樂用的,所以你肯定需要一個(gè)開關(guān),讓它起作用或者不起作用.我們通過NSUserDefaults來持久化存放使它起作用或者不起作用的變量

導(dǎo)航到**Rayrolling.h **,添加如下代碼

+ (BOOL)isEnabled;

Rayrolling.m文件中添加

+ (BOOL)isEnabled {
  return [[NSUserDefaults standardUserDefaults] boolForKey:@"com.raywenderlich.Rayrolling.shouldbeEnable"];
}
 
+ (void)setIsEnabled:(BOOL)shouldBeEnabled {
  [[NSUserDefaults standardUserDefaults] setBool:shouldBeEnabled forKey:@"com.raywenderlich.Rayrolling.shouldbeEnable"];
}

你已經(jīng)有了持久化你的選擇的邏輯,下面是將它關(guān)聯(lián)到GUI上去
回到Rayrolling.m中,修改-(void)doMenuAction的代碼為下面的:

- (void)doMenuAction:(NSMenuItem *)menuItem {
  [Rayrolling setIsEnabled:![Rayrolling isEnabled]];
  menuItem.title = [Rayrolling isEnabled] ? @"Disable Rayrolling" : @"Enable Rayrolling"; 
}

這是個(gè)用來切換的bool值,啟用或者禁用Rayrolling

最后,更改在didApplicationFinishLaunchingNotification:中的菜單項(xiàng)的初始化代碼,改為如下:

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];
}

這個(gè)菜單項(xiàng)將會(huì)保留你選擇的是否啟用的邏輯,即使xcode重啟后也沒關(guān)系,因?yàn)槟愕倪x擇已經(jīng)持久化存儲(chǔ)了.

導(dǎo)航到文件NSObject+Rayrolling_DVTBezelAlertPanel.m,添加一行頭文件

#import "Rayrolling.h"

最后,打開方法Rayrolling_initWithIcon:message:parentWindow:duration:,將

if (arg1) {

替換為

if ([Rayrolling isEnabled] && arg1) {

構(gòu)建并運(yùn)行程序,以便更改插件的行為.

現(xiàn)在,你創(chuàng)建了一個(gè)可以用來改變xcode編譯成功/失敗提示框圖標(biāo)和內(nèi)容的插件,并且它還是能夠被選擇是否打開.這一天的工作成果相當(dāng)不錯(cuò),難道不是嗎???

接下來做什么?


你可以從這里 下載完整的demo工程.

你已經(jīng)取得了很大的進(jìn)步,但是依然有很多事情要做!在本教程的第二部分,你將會(huì)學(xué)習(xí)到DTrace基本知識(shí),并且深入到一些LLDB的高級(jí)特性,諸如查找正在運(yùn)行的進(jìn)程,比如正在運(yùn)行的xcode進(jìn)程.

如果你想更進(jìn)一步,那么在你前往教程3之前,你還有一些工作要做.在教程3中,你將會(huì)看到大量的匯編代碼.確保在這之前,你已經(jīng)開始了解了相關(guān)的x86_64匯編知識(shí),這里有2篇Mike Ash的介紹分析匯編的系列文章 文章1,文章2,可以給你提供相關(guān)的幫助.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乃秀,一起剝皮案震驚了整個(gè)濱河市菱蔬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌砾层,老刑警劉巖漩绵,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異肛炮,居然都是意外死亡止吐,警方通過查閱死者的電腦和手機(jī)宝踪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碍扔,“玉大人瘩燥,你說我怎么就攤上這事〔煌” “怎么了颤芬?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)套鹅。 經(jīng)常有香客問我,道長(zhǎng)汰具,這世上最難降的妖魔是什么卓鹿? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮留荔,結(jié)果婚禮上吟孙,老公的妹妹穿的比我還像新娘。我一直安慰自己聚蝶,他們只是感情好杰妓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碘勉,像睡著了一般巷挥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上验靡,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天倍宾,我揣著相機(jī)與錄音,去河邊找鬼胜嗓。 笑死高职,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辞州。 我是一名探鬼主播怔锌,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼变过!你這毒婦竟也來了埃元?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤媚狰,失蹤者是張志新(化名)和其女友劉穎亚情,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哈雏,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡楞件,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年衫生,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土浸。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罪针,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黄伊,到底是詐尸還是另有隱情泪酱,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布还最,位于F島的核電站墓阀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拓轻。R本人自食惡果不足惜斯撮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扶叉。 院中可真熱鬧勿锅,春花似錦、人聲如沸枣氧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)达吞。三九已至张弛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酪劫,已是汗流浹背乌庶。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留契耿,地道東北人瞒大。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像搪桂,于是被迫代替她去往敵國(guó)和親透敌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容