DXXcodeConsoleUnicodePlugin源码解析

Xcode插件开发

嘿嘿,今天带大家学习一下基于Xcode的插件开发。可能很多人一听到插件开发,想到的都是Sublime Text,Atom这样轻量级的编辑器的扩展插件,但是实际上,无论是VisualStudio, Eclipse以及Xcode这样重量级的IDE,都是支持自定义的插件开发的。学习好了Xcode的插件开发,不仅可以打造度身定做的神器,也有助于你将来进行Mac OS的应用开发。

DXXcodeConsoleUnicodePlugin

DXXcodeConsoleUnicodePlugin是一个帮助你自动将\u6061这样的unicode码转换成对应的汉字的插件。

这个有什么用呢?想想看,我们在网络传输的时候,服务器如果返回的数据是中文(或者非ASCII码),通过NSLog在console输出的内容是不直观的,基本都是类似\u6061这种,这对于我们开发调试来说是非常困难的。

因此,这款插件可以自动帮助我们将检测到的Unicode字符进行转换,直接输出成我们想要的对应内容。怎么样?让我们赶快来一探究竟吧!

在开始探讨实现之前,我个人首先强调一点,基于Unicode检测对应的字符是一个非常难的问题。不仅仅是中文,韩文、日文、big-5字符等等都属于Unicode,这些字符集之间好常常有交集。现有比较好的开源实现是Mozilla的UcharSet**。

实现

首先打开工程,文件结构如下:

  • DXXcodeConsoleUnicodePlugin.h/.m
  • RegExCategories.h/.m

其中,DXXcodeConsoleUnicodePlugin是入口。同传统的iOS/Mac OS开发不同,插件开发并不存在传统意义上的main函数,更多的是利用所谓的Template Method设计模式将你需要的自定义部分进行复写。

于是,我们可以看到如下三段函数:

+ (void)pluginDidLoad:(NSBundle *)plugin
{
  static dispatch_once_t onceToken;
  NSString *currentApplicationName = [[NSBundle mainBundle] infoDictionary][@"CFBundleName"];
  if ([currentApplicationName isEqual:@"Xcode"]) {
    dispatch_once(&onceToken, ^{
      sharedPlugin = [[self alloc] initWithBundle:plugin];

      [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(menuDidChange)
                                                   name:NSMenuDidChangeItemNotification
                                                 object:nil];
    });
  }
}

- (id)initWithBundle:(NSBundle *)plugin
{
  if (self = [super init]) {
    // reference to plugin's bundle, for resource acccess
    self.bundle = plugin;

    // Create menu items, initialize UI, etc.

    // Sample Menu Item:
    [self createMenu];

    IMP_IDEConsoleItem_initWithAdaptorType = ReplaceInstanceMethod(NSClassFromString(@"IDEConsoleItem"), @selector(initWithAdaptorType:content:kind:),
                                                                   [XcodeConsoleUnicode_IDEConsoleItem class], @selector(initWithAdaptorType:content:kind:));
  }

  return self;
}

- (void)createMenu
{
  NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@"Edit"];
  if (menuItem && !self.convertInConsoleItem) {
    [[menuItem submenu] addItem:[NSMenuItem separatorItem]];

    NSMenuItem *convertItem = [[NSMenuItem alloc] initWithTitle:@"ConvertUnicode" action:@selector(convertAction) keyEquivalent:@"c"];
    [convertItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
    [convertItem setTarget:self];
    [[menuItem submenu] addItem:convertItem];

    self.convertInConsoleItem = [[NSMenuItem alloc] initWithTitle:@"ConvertUnicodeInConsole"
                                                           action:@selector(convertUnicodeInConsoleAction)
                                                    keyEquivalent:@""];
    [self.convertInConsoleItem setTarget:self];
    [[menuItem submenu] addItem:self.convertInConsoleItem];

    sIsConvertInConsoleEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:sConvertInConsoleEnableKey];
    if (sIsConvertInConsoleEnabled) {
      self.convertInConsoleItem.state = NSOnState;
    } else {
      self.convertInConsoleItem.state = NSOffState;
    }
  }
}

上面三段函数我们一一进行解析。

  1. pluginDidLoad,大家可以理解为插件的程序入口,在这个入口中我们通过单例进行我们自己开发的插件加载。**之所以使用单例是因为这个pluginDidLoad可能会由于加载多个插件而被多次触发。

  2. initWithBundle函数是我们自定义插件的构造函数,我们通过它进行自己任务的创建和调用。

  3. createMenu则是对Xcode编辑器上的菜单添加属于我们自己的选项。

在这里,作者在Edit菜单下创建了属于自己的ConvertUnicode以及ConvertUnicodeInConsole,并对这些选项进行了快捷键绑定。

这些东西,除了自定义的菜单项及操作需要我们自己写以外,我们都可以通过Plugin Template这个插件自动生成。

到现在,我们还没有看到任何实质性的转换内容,别急,在initWithBundle中,作者通过Method SwizzlingIDEConsoleItem- (id)initWithAdaptorType:(id)arg1 content:(id)arg2 kind:(int)arg3和自己实现的XcodeConsoleUnicode_IDEConsoleItem进行的调换。

然后在替换后的方法中,实现解析,代码如下:

- (id)initWithAdaptorType:(id)arg1 content:(id)arg2 kind:(int)arg3
{
  id item = IMP_IDEConsoleItem_initWithAdaptorType(self, _cmd, arg1, arg2, arg3);

  if (sIsConvertInConsoleEnabled) {
    NSString *logText = [item valueForKey:@"content"];
    NSString *resultText = [DXXcodeConsoleUnicodePlugin convertUnicode:logText];
    [item setValue:resultText forKey:@"content"];
  }

  return item;
}

这个方法非常简单,通过原方法获取console中的item,并获取对应的content进行解析。而解析也仅仅是采用了UTF8StringEncoding直接进行转换。

补充知识:NSRegularExpression和正则表达式

在本文的实现当中,作者对于中文字符的Unicode的表达方式\u4582这样的格式,采用了正则表达式进行了提取。在传统的Unicode的格式中,单独一个\表示为转义字符,不能直接表达一般字符。所以,在正则表达式中,我们需要采用\\来表示一个\。同时,对于4582这样的字符,我们当然可以认为其模式为四个连续的字符,所以我们可以采用\w{4}。(切记,不能采用\W。大写的\W表征的是非字符。)然后{4}表示前面的模式重复4次,即\w连续出现4次。

好了,综上所述,我们不难写出针对中文Unicode提取的正则表达式:\\u\w{4}

但是,在作者的代码中,作者的正则表达式却是:\\\\[uU]\\w{4},那这个是怎么回事呢?
原因在于, 对于在字符串形式出现的正则表达式,首先解析的是字符串规则,然后才是正则表达式引擎的解析。

所以,\\\\被字符串解析成\\,然后正则解析成\。然后对于[uU],是一个组,表示或者u或者U,因为有些输出的文本里,对于U的大小写并没有规定,所以两种情况都需要考虑。

后面的就不再赘述了,原理一致。大家有兴趣的自己深入学习下吧。