zoukankan      html  css  js  c++  java
  • Xcode7插件开发:从开发到拉到恶魔岛

    Xcode很强大,但是有些封闭,官方并没有提供Xcode插件开发的文档。喵神的教程比较全,也比较适合入门。本文的教程只是作为我在开发FKConsole的过程中的总结,并不会很全面。

    FKConsole是我开发的一个用于在Xcode控制台显示中文的插件,很小,很简单。这个插件开发的初衷是因为一个朋友有这种需求,而又没有找到相应的插件。如果不使用插件,就要在工程中嵌入文件,他并不乐意。所以FKConsole在设计上只会去修改Xcode控制台内的文字显示,绝不会去修改你的文件,这点大家可以放心。

    模板

    因为现在已经有很多人做Xcode插件开发了,所以插件模板这种东西也就应运而生了。

    Xcode-Plugin-Template是一个Xcode插件开发的基本模板,可以使用Alcatraz直接安装,支持Xcode 6+。

    安装完成之后,在创建工程的时候,会出现一个Xcode的插件的选项,这个就是Xcode中的插件工程模板。

    模板会生成NSObject_Extension和你的工程名称一样的两个文件(.M)。

    NSObject_Extension.m中的+(无效)pluginDidLoad:(*一个NSBundle)插件方法也是整个插件的入口。

    一般来说,我们希望我们的插件是存活于整个Xcode的生命周期的,所以一般是一个单例,这个在另一个文件中会有体现。

    添加按钮

    这篇博文是记录FKConsole开发过程的,自然以此举例。

    Xcode启动之后,会发出NSApplicationDidFinishLaunchingNotification的通知,模板上已经做了监听,我们在程序启动之后要在头部工具栏上加一个FKConsole的选项,以设置FKConsole插件的开关。

    Mac软件开发和iOS开发有一些不同,它使用的是AppKit的UI库,而不是UIKit,所以可能会感觉有些别扭。

    NSApp表示中的[NSApp表示mainMenu]方法可以获取到头部的主按钮,里面会包含很有NSMenuItem,我们将在Xcode的Window选项之前插入一个Plugins选项(参考破博客的做法),然后在这个选项中添加一个FKConsole的选项。(之所以添加一个Plugins选项是因为有些插件会添加到Edit中,有些会添加到ViewWindow中,我找半天都没找到选项在哪,还不如直接建一个Plugins选项,用户一眼就能知道插件在哪。)

    NSMenu * MAINMENU = [NSApp表示MAINMENU] ;
    如果(!MAINMENU)
    {
        返回;
    }
    
    NSMenuItem * pluginsMenuItem = [MAINMENU itemWithTitle:@ “插件” ] ;
    如果(!pluginsMenuItem)
    {
        pluginsMenuItem = [[NSMenuItem的alloc]初始化] ; 
        pluginsMenuItem .title伪 = @ “插件” ; 
        pluginsMenuItem .submenu = [[NSMenu页头] initWithTitle:pluginsMenuItem .title伪 ] ; 
        NSInteger的windowIndex = [MAINMENU indexOfItemWithTitle:@ “窗口” ] ; 
        [MAINMENU insertItem:pluginsMenuItem atIndex:windowIndex] ;
    }
    
    NSMenuItem * subMenuItem = [[NSMenuItem的alloc]初始化] ; 
    subMenuItem .title伪 = @ “FKConsole” ; 
    subMenuItem .TARGET =自我; 
    subMenuItem .action = @selector(toggleMenu :) ; 
    subMenuItem .STATE =值.boolValue NSOnState:NSOffState ; 
    [pluginsMenuItem .submenu的addItem:subMenuItem] ;
    

    我们需要一个状态来表示插件的开关,刚好NSMenuItem上有一个state可以表示状态,而刚好显示效果也不错,我们就用它了。

    图层

    按钮添加完之后,我们现在需要获取到控制台的实例。很遗憾,苹果并没有给出文档。

    很抱歉,我没有找到Mac软件开发上类似于Reveal的那种图层查看工具。喵神推荐了一个NSViewDumping类,代码如下:

    于来自http://onevcat.com/2013/02/xcode-plugin/

    - (无效)dumpWithIndent :( 的NSString *)缩进{
         的NSString *类= NSStringFromClass([ 个体经营类]);
         的NSString *信息= @ “” ;
         如果([ 自 respondsToSelector:@selector(标题)]){
             的NSString *标题= [ 自 performSelector:@selector(标题)];
             如果(标题=!零 && [标题长度]> 0){
                信息= [信息stringByAppendingFormat:@ “称号=%@” ,标题]
            }
        }
        如果([ 自 respondsToSelector:@selector(stringValue的)]){
             的NSString *字符串= [ 自 performSelector:@selector(stringValue的);
             如果(字符串!= 零 && [字符串长度]> 0){
                信息= [信息stringByAppendingFormat:@ “stringValue的=%@” ,字符串];
            }
        }
        的NSString *提示= [ 自我工具提示];
         如果(提示=!零 && [提示长度]> 0){
            信息= [信息stringByAppendingFormat:@ “提示=%@” ,提示]。
        }
    
        的NSLog(@ “%@%@%@” ,缩进,类信息);
    
        如果([ 自子视图]计数]> 0){  
             的NSString * subIndent = [ 的NSString stringWithFormat:@ “%@%@” ,缩进,([缩进长度] / 2)%2 == 0?@ “|”:@ “:” ];  
             为(*的NSView在[子视图自子视图]){  
                [子视图dumpWithIndent:subIndent];  
            }  
        }  
    } 
    

    效果类似于如下:

    除了这种做法之外,我用的是chisel,这是facebook开源的一个LLDB的命令行辅助调试的工具。里面包含有一个pviews命令,可以直接递归打印整个key窗口,效果如下:

    导入私有API

    我们在里面找到了一个叫做IDEConsoleTextView的类,这是在上图中看到的所有View中唯一包含Console这个关键字的,我们查看一下它的frame,确定控制台就是它。

    苹果并没有给将这个IDEConsoleTextView放到AppKit中,它是一个私有类,我们现在想要修改它,那么就需要拿到它的头文件。

    Github上上有很多转储出来的Xcode中header,大家可以看一下:https://github.com/search?utf8=%E2%9C%93&q=xcode+header。我们在header中找到了IDEConsoleTextView.h ,处于IDEKit中。

    在头文件中可以看到,IDEConsoleTextView是继承自DVTCompletingTextView -> DVTTextView ->NSTextViewNSTextView中保存文字内容使用的是NSTextStorage *textStorage,所以我们要修改的是IDEConsoleTextViewtextStorage。但是我们在NSTextStorage的头文件中并没有找到具体文字保存的属性,那我们这就去找。

    功能开发

    我们循环遍历所有的的NSView,找到IDEConsoleTextView,我们看一下它的信息:

    我们没有找到它的textStorage属性,我们尝试在控制台中打一下:

    它是有这个属性的,只是在调试区没有看到。

    textStorage的代表中有两个方法,分别是:

    // 发送 -Process内编辑固定属性之前。   代表们可以改变文字或属性。
     - (无效) textStorage: NSTextStorage *) textStorage willProcessEditing: NSTextStorageEditActions) editedMask范围:NSRange) editedRange changeInLength: NSInteger的)增量NS_AVAILABLE 10 _11,7 _0) ;
    
    //内发送正确-processEditing通知布局管理器前,   代表们可以改变属性。
     - (无效) textStorage: NSTextStorage *) textStorage didProcessEditing: NSTextStorageEditActions) editedMask范围:NSRange) editedRange changeInLength: NSInteger的)增量NS_AVAILABLE 10 _11,7 _0) ;
    

    textStorage中字符或者描述被修改之后,会触发这个代理,那我们实现一下这个代理方法:

    .fkConsoleTextView .textStorage .delegate = 自我 ;
    
    - (无效)textStorage:(NSTextStorage *)textStorage
     willProcessEditing:(NSTextStorageEditActions)editedMask
                  范围:( NSRange)editedRange
         changeInLength :( NSInteger的)三角洲
    {
    
    }
    

    OK,这次我们找到了,IDEConsoleTextView中有一个_contents属性,这是一个继承自NSMutableAttributedString的类,这个里面的mutableString保存文字,mutableAttributes保存对文字的描述。我们需要修改的就是这个mutableString属性。

    我们在代理方法中使用valueForKeyPath:可以获取到mutableString属性,那么,现在我们将它进行转换。

    FKConsole是用来调整控制台中文显示的,目的是将类似于这种的Unicode编码( U6d4bU8bd5" )修改为( "测试啊" )这种的正常显示。

    我在计算器。上找到一种解决办法代码类似于这样:

    于来自http://stackoverflow.com/questions/13240620/uilabel-text-with-unicode-nsstring

    - (的NSString *)stringByReplaceUnicode :( 的NSString *)的字符串
    {
        的NSMutableString * convertedString = [字符串mutableCopy]
        [convertedString replaceOccurrencesOfString:@ “\ U” withString:@ “\ U”选项:0范围:NSMakeRange(0,convertedString 。长度)];
        CFStringRef变换= CFSTR( “ 任意六角/ Java”的);
        CFStringTransform((__桥CFMutableStringRef)convertedString,NULL,变换,YES);
         返回 convertedString;
    }
    

    我们使用的setValue:forKeyPath:的方式去修改mutableString属性。

    运行,确实可以,但是有一些问题。

    1. 如果使用findView的方式去查找IDEConsoleTextView,然后去设置代理的话,那么,在什么时候去findView呢,如果这时候又新打开几个页面呢,这是不确定的。
    2. 修改后的文字长度和原先的不一样,哪怕修改了editedRange也没有用。这样的话,如果在控制台上输入文字或者调试命令,可能会崩溃,崩溃的主要原因是IDEConsoleTextView_startLocationOfLastLine_lastRemovableTextLocation这两个属性去控制文字起始位置和删除位置,在设置mutableString之后,由于长度不一,可能会发生字符串取值越界的问题,而NSTextStorage的代理中又是获取不到持有它的IDEConsoleTextView的。

    监听通知

    针对第一个问题,我们可以使用通知的方式去解决。

    参照喵神的博客,可以监听全部的通知,然后去查找哪个是你所需要的。

    - (ID)的init { 
         如果(自 = [ 超级初始化]){
            [ NSNotificationCenter defaultCenter]的addObserver:自我  
                选择:@选择(的NotificationListener :)
                    名称:无目标:零 ]
        } 
        回归 自我 ;
    } 
    
    - (无效)的NotificationListener :( NSNotification *)的NotI {   
         的NSLog(@ “通知:%@”,[NotI位名]);   
    } 
    

    我们这里只需要监听NSTextDidChangeNotification就行,然后在方法内去判断一下,之后再设置代理。

    - (无效)textStorageDidChange:(NSNotification *)的NotI
    {
        如果([NotI位。对象 isKindOfClass:NSClassFromString(@“IDEConsoleTextView”)&&
            ((IDEConsoleTextView *)的NotI。对象).textStorage。委派!=个体经营)
        {
            (。(IDEConsoleTextView *)的NotI 对象)。.textStorage 委托 =自我;
        }
    }
    

    这样就解决了第一个问题。

    添加方法和手段的交叉混合

    这里有兴趣的话,可以参考我另外一篇博客:Objective-C运行常见用法,里面以举例的方式讲解了常见的运行时用法。

    针对第二个问题,我采用的办法是在适当的时候去修改IDEConsoleTextView_startLocationOfLastLine_lastRemovableTextLocation属性。经实验,崩溃的方法主要是IDEConsoleTextView的这些方法:

    - (void) insertText: (id)arg1;
     - (void) insertNewline: (id)arg1;
     - (void)clearConsoleItems;
     - ( BOOL ) shouldChangeTextInRanges: (id)arg1 replacementStrings: (id)arg2;
    

    我给IDEConsoleTextView在运行时添加了以下的方法:

    - (void) fk_insertText: (id)arg1;
     - (void) fk_insertNewline: (id)arg1;
     - (void)fk_clearConsoleItems;
     - ( BOOL ) fk_shouldChangeTextInRanges: (id)arg1 replacementStrings: (id)arg2;
    

    之后,使用JRSwizzle来交换,混合方法,类似于这样:

    - (无效) addMethodWithNewMethod: SEL) newMethod originMethod: SEL) originMethod
    {
        方法targetMethod = class_getInstanceMethod NSClassFromString(@ “IDEConsoleTextView” ),newMethod);
    
        方法consoleMethod = class_getInstanceMethod (self.class,新方法) ;
        IMP consoleIMP = method_getImplementation (控制台方法) ;
    
        如果(!目标的方法)
        {
            class_addMethod NSClassFromString(@ “IDEConsoleTextView” ),newMethod,consoleIMP,method_getTypeEncoding (控制台方法));
    
            如果(原产地法)
            {
                NSError *错误;
                [NSClassFromString (@ “IDEConsoleTextView” )
                 jr_swizzleMethod:newMethod
                 withMethod:originMethod
                 错误:错误]
                的NSLog (@ “错误=%@” ,错误) ;
            }
        }
    }
    

    fk_开头的系列方法中,添加了对IDEConsoleTextView的检查:

    - (无效) fk_checkTextView: IDEConsoleTextView *)的TextView
    {
        如果(文本查看的.text 存储。长度<[文本查看值ForKeyPath:克StartLocationOfLastLineKey ]长的longValue ])
        {
            [TextView中的setValue:@ (文本查看的.text 存储。长度) forKeyPath:kStartLocationOfLastLineKey];
        }
        如果(文本查看的.text 存储。长度<[文本查看值ForKeyPath:克LastRemovableTextLocationKey ]长的longValue ])
        {
            [TextView中的setValue:@ (文本查看的.text 存储。长度) forKeyPath:kLastRemovableTextLocationKey];
        }
    }
    
    
    - (无效) fk_insertText:(ID) ARG1
    {
        [个体经营fk_checkTextView: IDEConsoleTextView *)个体经营];
        [个体经营fk_insertText:ARG1];
    }
    

    这样,就解决了第二个问题。

    OK,FKConsole这就基本开发完成了。

    恶魔岛


    上文也提到了,Alcatraz是一个开源的Xcode包管理器。事实上,Alcatraz也成为了我们目前安装Xcode插件的最主要的工具。

    现在我们将FKConsole提交到恶魔上。

    填写

    alcatraz-packagesAlcatraz的包仓库列表,packages.json保存了所有Alcatraz支持的插件、色彩主题、模板。

    我们fork一下alcatraz-packages我们的代码仓库中。之后,仿照这种格式,添加上我们的项目。

     {
           “ 名 ”:“FKConsole” ,
           “ url可 ”:“https://github.com/Forkong/FKConsole” ,
           “ 说明 ”:“FKConsole是一个插件的Xcode调整控制台显示器(对中国)。” ,
           “ 屏幕截图 ”:“https://raw.githubusercontent.com/Forkong/FKConsole/master/Screenshots/demo.gif”
     }
    

    respec

    rspec是用ruby写的一个测试框架,这里作者写了一个用于测试你修改过后的packages.json是否合法的脚本。直接切到alcatraz-packages目录下,运行rspec命令即可。通过的话,会这样显示:

    RSpec的使用红宝石的宝石就能直接装上。

    校验没有问题之后,我们拉入请求,我们的提交就出现在恶魔的程序包拉请求上了:

    https://github.com/alcatraz/alcatraz-packages/pull/461

    (大家千万不要像我一样,没看清除,直接添加到最后面了。它是有三个分类的,一定要看清楚,要添加到插件的分类上。)

     
  • 相关阅读:
    Mybatis学习-ResultMap
    MySql模糊查询 concat()函数
    Spring学习-依赖注入
    Struts2学习-struts执行过程简述
    Struts2学习-jsp中超链接传参问题
    Struts2学习-struts.xml文件配置
    第四次作业
    第三次作业
    Django -Ajax
    Django -ORM
  • 原文地址:https://www.cnblogs.com/nenhallgg/p/5559007.html
Copyright © 2011-2022 走看看