Xcode插件開發(fā)

標(biāo)簽(空格分隔): 插件


[toc]


附錄

演示環(huán)境Xcode7.3
插件模板下載 https://github.com/kattrali/Xcode-Plugin-Template
插件下載 https://github.com/JXnan/JXSpeechCode
插件存放目錄 ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins
DVTKit目錄 /Applications/Xcode.app/Contents/SharedFrameworks
插件模板目錄 ~/Library/Developer/Xcode/Templates/Project Templates/Application Plug-in/Xcode Plugin.xctemplate

相關(guān)文檔

http://www.cnblogs.com/zhw511006/p/4299960.html 插件制作詳解
http://www.cocoachina.com/ios/20160229/15476.html xcode7插件制作詳解


配置環(huán)境

首先下載插件模板,將下載下來的文件復(fù)制到~/Library/Developer/Xcode/Templates/Project Templates/Application Plug-in/Xcode Plugin.xctemplate下,如果沒有這個(gè)目錄則創(chuàng)建.


重啟Xcode在OSX目錄下將會有一個(gè)新的選項(xiàng)用于創(chuàng)建Xcode插件程序

制作一個(gè)簡單的插件

運(yùn)行demo

創(chuàng)建一個(gè)新的plugin工程,完畢后發(fā)現(xiàn)模板已經(jīng)自動生成了兩個(gè)類和一個(gè).xcscheme文件,xcscheme文件是插件的配置文件,一般情況下無需改動,模板作者已經(jīng)配置好了的.
NSObject_Extension類是一個(gè)單例類,用于插件在整個(gè)Xcode生命周期中都存在.
pluginDemo是作者編寫的一個(gè)demo,現(xiàn)在不進(jìn)行任何改動運(yùn)行下這個(gè)demo.



運(yùn)行后出現(xiàn)提示框詢問是否加載插件,一定要選擇Load Bundles.然后會啟動一個(gè)新的Xcode.因?yàn)槲覀兪侵谱饕粋€(gè)Xcode的插件,這個(gè)新啟動的Xcode就是調(diào)試用的模擬器了,注意,在Xcode模擬器中修改代碼一樣會影響到源代碼.
那么,這個(gè)demo有什么作用呢?點(diǎn)擊菜單欄Edit選項(xiàng),發(fā)現(xiàn)下面多了一個(gè)按鈕



點(diǎn)擊按鈕彈出,hello world窗口,這就是這個(gè)插件所帶來的效果.

查看代碼

來看看怎么實(shí)現(xiàn)的吧,進(jìn)入pluginDemo.m文件.首先是入口函數(shù)- (id)initWithBundle:(NSBundle *)plugin 初始化中注冊了一個(gè)通知,在程序加載完畢后調(diào)用didApplicationFinishLaunchingNotification:方法

- (id)initWithBundle:(NSBundle *)plugin
{
    if (self = [super init]) {
        // reference to plugin's bundle, for resource access
        self.bundle = plugin;
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didApplicationFinishLaunchingNotification:)
                                                     name:NSApplicationDidFinishLaunchingNotification
                                                   object:nil];
    }
    return self;
}

在通知方法中,首先查找edit按鈕接著在edit按鈕下創(chuàng)建了一個(gè)新的按鈕,并為這個(gè)按鈕Do Action增加了一個(gè)響應(yīng)事件

- (void)didApplicationFinishLaunchingNotification:(NSNotification*)noti
{
    //removeObserver
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidFinishLaunchingNotification object:nil];
    
    // 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 setKeyEquivalentModifierMask:NSAlphaShiftKeyMask | NSControlKeyMask];
        [actionMenuItem setTarget:self];
        [[menuItem submenu] addItem:actionMenuItem];
    }
}

在按鈕的響應(yīng)事件中.展示提示信息.整個(gè)插件完成.

- (void)doMenuAction
{
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setMessageText:@"Hello, World"];
    [alert runModal];
}

分析

上面的代碼都很簡單熟悉OC語言的基本都能看的懂,唯一的區(qū)別就是大部分OC開發(fā)者是做iOS開發(fā)的使用的是cocoa touch框架,而Xcode插件屬于OSX程序,使用的則是cocoa框架.當(dāng)然區(qū)別并不大,只是UIView轉(zhuǎn)NSView而以.里面的方法也有些微小的區(qū)別

制作讓代碼發(fā)聲的插件

主要功能是在輸入代碼后,Xcode會自動朗誦輸入的代碼

獲得代碼文本

首先如果想朗讀輸入的代碼,那么得到輸入的文本是必不可少的,如何做呢?
iOS中有很多的通知,OSX中同樣也有,而且更加豐富,關(guān)于如何得到通知其實(shí)很簡單,只要?jiǎng)?chuàng)建一個(gè)沒有參數(shù)的通知就可以.將中didApplicationFinishLaunchingNotification:方法中所有代碼全部刪除.因?yàn)槲覀円萖code加載完以后才朗讀內(nèi)容,所以在這里添加通知.最后創(chuàng)建一個(gè)沒有name參數(shù)的通知,這樣就可以接受到整個(gè)程序所有的通知了.輸出通知名稱,方便查找我們需要的通知.

- (void)didApplicationFinishLaunchingNotification:(NSNotification*)noti
{
    //removeObserver
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidFinishLaunchingNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notified:) name:nil object:nil];
}

- (void)notified:(NSNotification *)sender
{
    NSLog(@"%@",sender.name);
}

再次運(yùn)行demo.發(fā)現(xiàn)控制臺輸出大量的通知信息.或許需要的通知就在這里面.如果覺得通知太多不容易找,可以在輸出前增加條件,比如包含change字符的通知才輸出.
通過尋找發(fā)現(xiàn)這樣一條通知TextDidChangeNotification通過方法名的可以看出這是文本改變后的通知.試試從通知中能不能得到輸入的代碼.
將通知的name改成TextDidChangeNotification

- (void)didApplicationFinishLaunchingNotification:(NSNotification*)noti
{
    //removeObserver
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidFinishLaunchingNotification object:nil];
    //這里
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notified:) name:TextDidChangeNotification object:nil];
}
//輸出通知中獲得的參數(shù) 使用object是因?yàn)閕nfo中之前測試了沒有任何信息.
- (void)notified:(NSNotification *)sender
{
    NSLog(@"%@",sender.object);
}

這次再運(yùn)行輸出就少很多了.



這些通知是模擬器啟動加載原有內(nèi)容造成的通知,我們將控制臺清空,然后在模擬器中輸入代碼看看輸出結(jié)果.發(fā)現(xiàn)有時(shí)候不寫代碼也會有通知出現(xiàn),比如移動光標(biāo)的位置什么的.看來任何改變都會調(diào)用這個(gè)通知.

2016-05-13 11:27:14.658 Xcode[1895:880535] <DVTSourceTextView: 0x117e79690>
    Frame = {{0.00, 0.00}, {833.00, 1348.00}}, Bounds = {{0.00, 0.00}, {833.00, 1348.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {833.00, 534.00}, MaxSize = {10000000.00, 10000000.00}
2016-05-13 11:27:15.168 Xcode[1895:880535] <DVTSourceTextView: 0x117e79690>
    Frame = {{0.00, 0.00}, {833.00, 1348.00}}, Bounds = {{0.00, 0.00}, {833.00, 1348.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {833.00, 534.00}, MaxSize = {10000000.00, 10000000.00}
2016-05-13 11:27:15.249 Xcode[1895:880535] <DVTSourceTextView: 0x117e79690>
    Frame = {{0.00, 0.00}, {833.00, 1348.00}}, Bounds = {{0.00, 0.00}, {833.00, 1348.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {833.00, 534.00}, MaxSize = {10000000.00, 10000000.00}
2016-05-13 11:27:15.832 Xcode[1895:880535] <DVTSourceTextView: 0x117e79690>
    Frame = {{0.00, 0.00}, {833.00, 1348.00}}, Bounds = {{0.00, 0.00}, {833.00, 1348.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {833.00, 534.00}, MaxSize = {10000000.00, 10000000.00}

發(fā)現(xiàn)通知傳遞進(jìn)來的對象是一個(gè)DVTSourceTextView對象,猜測這個(gè)對象就是代碼輸入框的View.試著查看一下這個(gè)類,發(fā)現(xiàn)這個(gè)類是Xcode的私有類,無法看到類的聲明文件,但是可以通過類名發(fā)現(xiàn)它可能繼承于TextView,因?yàn)槭莄ocoa庫,所以是NStextVieww而不是UITextView.測試一下

- (void)notified:(NSNotification *)sender
{
    if ([sender.object isKindOfClass:[NSTextView class]]) {
        NSTextView * textView = (NSTextView *)sender.object;
        NSLog(@"%@",textView.textStorage.string);
    }
}

運(yùn)行插件,在Xcode中隨便輸入一個(gè)文本,接著就會發(fā)現(xiàn)控制臺輸出了xcode中所有的代碼.



當(dāng)然,這還不夠,我們還需要得到輸入的代碼.這里就不往下繼續(xù)了,因?yàn)橹灰塬@得全部的代碼就表示可以獲得輸入的內(nèi)容,但是得到的往往是單個(gè)字母,而不是整個(gè)句子,所以想要朗讀,必須得到整個(gè)代碼的句子.

hook技術(shù)

如果之前有過破解程序或編寫其他應(yīng)用插件的也許不陌生這個(gè)詞,hook是編寫插件最常用的技術(shù),主要功能就是讓程序運(yùn)行的時(shí)候來調(diào)用插件中得方法,插件方法運(yùn)行后繼續(xù)運(yùn)行程序內(nèi)部的方法.



通過這種方式,就可以在不影響程序原有功能的情況加增加功能.得益于oc中得黑魔法(runtime)實(shí)現(xiàn)起來非常簡單.這里最難的不是代碼,而是找到輸入文本后xcode調(diào)用的方法.

尋找xcode原有的方法.

在輸入代碼的時(shí)候,通常不會手動全部打出來,只需要打上首字母(xcode7.3之后更是增加了模糊搜索)xcode就會出現(xiàn)一個(gè)代碼列表框,選擇想到的代碼,按下回車代碼就出現(xiàn)在xcode中了,想讓xcode朗讀寫下的代碼.可以找到選擇代碼完畢,將選擇寫入代碼編輯框的這個(gè)方法.然后再這個(gè)方法前后插入朗讀代碼即可.
之前使用通知輸出所有的代碼的時(shí)候就已經(jīng)知道,代碼編輯框是一個(gè)DVTSourceTextView對象,所以就需要找到這個(gè)類,但是這個(gè)是私有類,如何才能知道這個(gè)類有什么方法呢?兩種辦法.

1.使用runtime

黑魔法中有一個(gè)可以打印類方法的方法.首先導(dǎo)入#import <objc/runtime.h>庫,在通知調(diào)用的方法中寫入代碼

- (void)notified:(NSNotification *)sender
{
    if ([sender.object isKindOfClass:[NSTextView class]]) {
        NSString *className = NSStringFromClass([sender.object class]);
        const char *cClassName = [className UTF8String];
        id theClass = objc_getClass(cClassName);
        unsigned int outCount;
        Method *m =  class_copyMethodList(theClass,&outCount);
        
        NSLog(@"%d",outCount);
        for (int i = 0; i<outCount; i++) {
            SEL a = method_getName(*(m+i));
            NSString *sn = NSStringFromSelector(a);
            NSLog(@"%@",sn);
        }
    }
}

調(diào)試一下,查看運(yùn)行結(jié)果


2.導(dǎo)出私有庫

前往->應(yīng)用程序->右鍵Xcode選擇顯示包內(nèi)容->Contents->SharedFrameworks 在這個(gè)文件夾下存放這一個(gè)DVTKit庫,很顯然DVTSourceTextView就在這里面,將DVTKit庫拷貝出來備用,怎么導(dǎo)出這個(gè)庫的頭文件呢?請自行百度.因?yàn)槲覈L試過很多次都沒有成功,可能是Xcode7加密了.也可能是沒有做對方法,總之失敗了,好消息是很多大神已經(jīng)將導(dǎo)出的頭文件放到了github上,這里感謝大嬸們.下載地址:https://github.com/luisobo/Xcode-RuntimeHeaders

輸入文字時(shí)到底調(diào)用了那個(gè)方法?

感謝OC編程規(guī)范,很多方法看名字我們就知道干什么的了.但是對于英文能力基本為0的我來說通過方法名稱找方法依舊不是簡單的事情,但是我知道兩個(gè)關(guān)鍵字是這個(gè)方法所必須得.一個(gè)是NSString一個(gè)是NSRange,因?yàn)橄胍獮?code>DVTSourceTextView增加文本,很有可能要傳遞這樣的參數(shù).最終使用NSRange成功找到方法selectFirstPlaceholderInCharacterRange,這個(gè)方法是DVTSourceTextView父類DVTCompletingTextView的方法.

hook方法

首先將之前拷貝出來的DVTKit庫文件到程序中,不同平時(shí)添加庫文件,要像使用第三方一樣拖進(jìn)去
創(chuàng)建一個(gè).h文件名字為DVTSourceTextView.h 將里面的代碼刪除鍵入下面的代碼

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
@interface DVTSourceTextView : NSTextView 
- (BOOL)selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1;
@end

這樣就偽造了一個(gè)DVTSourceTextView.h類并且開放了一個(gè)selectFirstPlaceholderInCharacterRange方法的接口.
接下來為這個(gè)類創(chuàng)建一個(gè)分類,這時(shí)候系統(tǒng)可能找不到'DVTSourceTextView.h'這個(gè)類,可以把這個(gè).h隨便導(dǎo)入一個(gè)類編譯一下就可以找到了.最后結(jié)果文件是這樣的


然后再分類的.m中鍵入以下代碼


#import "DVTSourceTextView+Hook.h"
#import <objc/runtime.h> //導(dǎo)入runtime庫
@implementation DVTSourceTextView (Hook)
+ (void)load{   //hook方法,最好是在load方法中使用,以免出現(xiàn)問題
    Method obj1 = class_getInstanceMethod(self, @selector(selectFirstPlaceholderInCharacterRange:));
    Method obj2 = class_getInstanceMethod(self, @selector(jx_selectFirstPlaceholderInCharacterRange:));
    method_exchangeImplementations(obj1, obj2);
    // 上面三行的作用是將selectFirstPlaceholderInCharacterRange:方法和jx_selectFirstPlaceholderInCharacterRange:方法調(diào)換,這樣當(dāng)系統(tǒng)調(diào)用selectFirstPlaceholderInCharacterRange:方法時(shí) 實(shí)際上是調(diào)用的jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1 方法
}

//用于調(diào)換的自建方法 你覺得這里會造成遞歸? NO! 
- (void)jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1{
    NSLog(@"%@",NSStringFromRange(arg1));
    //由于方法被調(diào)換,所以這里運(yùn)行時(shí)調(diào)用的是selectFirstPlaceholderInCharacterRange方法
    [self jx_selectFirstPlaceholderInCharacterRange:arg1];
}

@end

運(yùn)行插件,在模擬器中輸入一行代碼.查看輸出結(jié)果
輸入一個(gè) N 沒有輸出結(jié)果 再輸入 S 還是沒有 這時(shí)代碼提示出現(xiàn),選擇 NSArray 然后回車,此時(shí)控制臺輸出:2016-05-13 15:18:36.702 Xcode[3722:1670418] {520, 7}
這個(gè)方法只有選擇代碼提示輸入才會調(diào)用,并且能返回輸入的位置和長度,這樣就可以完整的得到輸入內(nèi)容,而且不是單個(gè)的字母而是整個(gè)單詞.接下來就是利用自帶的語音庫讓代碼發(fā)聲了.

會叫得代碼

- (void)jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1{
    NSLog(@"%@",NSStringFromRange(arg1));
    //得到輸入的內(nèi)容
    NSString * str = [self.textStorage.string substringWithRange:arg1];
    //系統(tǒng)語音庫
    NSSpeechSynthesizer * speech = [[NSSpeechSynthesizer alloc] init];
    [speech startSpeakingString:str];
    [self jx_selectFirstPlaceholderInCharacterRange:arg1];
}

完成了!

制作過程中得坑

  • 嘗試自己導(dǎo)出私有API的庫,但是總是失敗,最后原因確實(shí)Xcode7加殼了,吐血.有興趣的可以研究下破殼,網(wǎng)上有教程,感謝將頭文件上傳到git的前輩大神

  • 在build Phase中導(dǎo)入DVTKit總是會報(bào)錯(cuò),后來直接拖進(jìn)去反而好了.

  • 新建的DVTSourceTextView.h系統(tǒng)編譯不到,然后就無法創(chuàng)建類別進(jìn)行hook,這里糾結(jié)了好半天,看了下別人的代碼都可以創(chuàng)建,怎么都想不通,也編譯了幾次都不行,最后將這個(gè).h導(dǎo)入了一個(gè)類才編譯出來.坑啊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容