Xcode7插件开发,制作朗读代码的插件

来源:互联网 发布:淘宝冰帝 编辑:程序博客网 时间:2024/06/11 11:27

附录

演示环境Xcode7.3
完整插件 https://github.com/JXnan/Literate-camels/tree/master
插件模板下载 https://github.com/kattrali/Xcode-Plugin-Template
插件下载 https://github.com/JXnan/Literate-camels/tree/master
插件存放目录 ~/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

相关文档

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


配置环境

首先下载插件模板,将下载下来的文件复制到~/Library/Developer/Xcode/Templates/Project Templates/Application Plug-in/Xcode Plugin.xctemplate下,如果没有这个目录则创建.

重启Xcode在OSX目录下将会有一个新的选项用于创建Xcode插件程序

制作一个简单的插件

运行demo

创建一个新的plugin工程,完毕后发现模板已经自动生成了两个类和一个.xcscheme文件,xcscheme文件是插件的配置文件,一般情况下无需改动,模板作者已经配置好了的.
NSObject_Extension类是一个单例类,用于插件在整个Xcode生命周期中都存在.
pluginDemo是作者编写的一个demo,现在不进行任何改动运行下这个demo.

运行后出现提示框询问是否加载插件,一定要选择Load Bundles.然后会启动一个新的Xcode.因为我们是制作一个Xcode的插件,这个新启动的Xcode就是调试用的模拟器了,注意,在Xcode模拟器中修改代码一样会影响到源代码.
那么,这个demo有什么作用呢?点击菜单栏Edit选项,发现下面多了一个按钮

点击按钮弹出,hello world窗口,这就是这个插件所带来的效果.

查看代码

来看看怎么实现的吧,进入pluginDemo.m文件.首先是入口函数- (id)initWithBundle:(NSBundle *)plugin 初始化中注册了一个通知,在程序加载完毕后调用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按钮下创建了一个新的按钮,并为这个按钮Do Action增加了一个响应事件

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

在按钮的响应事件中.展示提示信息.整个插件完成.

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

分析

上面的代码都很简单熟悉OC语言的基本都能看的懂,唯一的区别就是大部分OC开发者是做iOS开发的使用的是cocoa touch框架,而Xcode插件属于OSX程序,使用的则是cocoa框架.当然区别并不大,只是UIView转NSView而以.里面的方法也有些微小的区别

制作让代码发声的插件

主要功能是在输入代码后,Xcode会自动朗诵输入的代码

获得代码文本

首先如果想朗读输入的代码,那么得到输入的文本是必不可少的,如何做呢?
iOS中有很多的通知,OSX中同样也有,而且更加丰富,关于如何得到通知其实很简单,只要创建一个没有参数的通知就可以.将中didApplicationFinishLaunchingNotification:方法中所有代码全部删除.因为我们要等Xcode加载完以后才朗读内容,所以在这里添加通知.最后创建一个没有name参数的通知,这样就可以接受到整个程序所有的通知了.输出通知名称,方便查找我们需要的通知.

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

再次运行demo.发现控制台输出大量的通知信息.或许需要的通知就在这里面.如果觉得通知太多不容易找,可以在输出前增加条件,比如包含change字符的通知才输出.
通过寻找发现这样一条通知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];}//输出通知中获得的参数 使用object是因为info中之前测试了没有任何信息.- (void)notified:(NSNotification *)sender{    NSLog(@"%@",sender.object);}

这次再运行输出就少很多了.

这些通知是模拟器启动加载原有内容造成的通知,我们将控制台清空,然后在模拟器中输入代码看看输出结果.发现有时候不写代码也会有通知出现,比如移动光标的位置什么的.看来任何改变都会调用这个通知.

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}

发现通知传递进来的对象是一个DVTSourceTextView对象,猜测这个对象就是代码输入框的View.试着查看一下这个类,发现这个类是Xcode的私有类,无法看到类的声明文件,但是可以通过类名发现它可能继承于TextView,因为是cocoa库,所以是NStextVieww而不是UITextView.测试一下

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

运行插件,在Xcode中随便输入一个文本,接着就会发现控制台输出了xcode中所有的代码.

当然,这还不够,我们还需要得到输入的代码.这里就不往下继续了,因为只要能获得全部的代码就表示可以获得输入的内容,但是得到的往往是单个字母,而不是整个句子,所以想要朗读,必须得到整个代码的句子.

hook技术

如果之前有过破解程序或编写其他应用插件的也许不陌生这个词,hook是编写插件最常用的技术,主要功能就是让程序运行的时候来调用插件中得方法,插件方法运行后继续运行程序内部的方法.

通过这种方式,就可以在不影响程序原有功能的情况加增加功能.得益于oc中得黑魔法(runtime)实现起来非常简单.这里最难的不是代码,而是找到输入文本后xcode调用的方法.

寻找xcode原有的方法.

在输入代码的时候,通常不会手动全部打出来,只需要打上首字母(xcode7.3之后更是增加了模糊搜索)xcode就会出现一个代码列表框,选择想到的代码,按下回车代码就出现在xcode中了,想让xcode朗读写下的代码.可以找到选择代码完毕,将选择写入代码编辑框的这个方法.然后再这个方法前后插入朗读代码即可.
之前使用通知输出所有的代码的时候就已经知道,代码编辑框是一个DVTSourceTextView对象,所以就需要找到这个类,但是这个是私有类,如何才能知道这个类有什么方法呢?两种办法.

1.使用runtime

黑魔法中有一个可以打印类方法的方法.首先导入#import <objc/runtime.h>库,在通知调用的方法中写入代码

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

调试一下,查看运行结果

2.导出私有库

前往->应用程序->右键Xcode选择显示包内容->Contents->SharedFrameworks 在这个文件夹下存放这一个DVTKit库,很显然DVTSourceTextView就在这里面,将DVTKit库拷贝出来备用,怎么导出这个库的头文件呢?请自行百度.因为我尝试过很多次都没有成功,可能是Xcode7加密了.也可能是没有做对方法,总之失败了,好消息是很多大神已经将导出的头文件放到了github上,这里感谢大婶们.下载地址:https://github.com/luisobo/Xcode-RuntimeHeaders

输入文字时到底调用了那个方法?

感谢OC编程规范,很多方法看名字我们就知道干什么的了.但是对于英文能力基本为0的我来说通过方法名称找方法依旧不是简单的事情,但是我知道两个关键字是这个方法所必须得.一个是NSString一个是NSRange,因为想要为DVTSourceTextView增加文本,很有可能要传递这样的参数.最终使用NSRange成功找到方法selectFirstPlaceholderInCharacterRange,这个方法是DVTSourceTextView父类DVTCompletingTextView的方法.

hook方法

首先将之前拷贝出来的DVTKit库文件到程序中,不同平时添加库文件,要像使用第三方一样拖进去
创建一个.h文件名字为DVTSourceTextView.h 将里面的代码删除键入下面的代码

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

这样就伪造了一个DVTSourceTextView.h类并且开放了一个selectFirstPlaceholderInCharacterRange方法的接口.
接下来为这个类创建一个分类,这时候系统可能找不到’DVTSourceTextView.h’这个类,可以把这个.h随便导入一个类编译一下就可以找到了.最后结果文件是这样的

然后再分类的.m中键入以下代码

#import "DVTSourceTextView+Hook.h"#import <objc/runtime.h> //导入runtime库@implementation DVTSourceTextView (Hook)+ (void)load{   //hook方法,最好是在load方法中使用,以免出现问题    Method obj1 = class_getInstanceMethod(self, @selector(selectFirstPlaceholderInCharacterRange:));    Method obj2 = class_getInstanceMethod(self, @selector(jx_selectFirstPlaceholderInCharacterRange:));    method_exchangeImplementations(obj1, obj2);    // 上面三行的作用是将selectFirstPlaceholderInCharacterRange:方法和jx_selectFirstPlaceholderInCharacterRange:方法调换,这样当系统调用selectFirstPlaceholderInCharacterRange:方法时 实际上是调用的jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1 方法}//用于调换的自建方法 你觉得这里会造成递归? NO! - (void)jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1{    NSLog(@"%@",NSStringFromRange(arg1));    //由于方法被调换,所以这里运行时调用的是selectFirstPlaceholderInCharacterRange方法    [self jx_selectFirstPlaceholderInCharacterRange:arg1];}@end

运行插件,在模拟器中输入一行代码.查看输出结果
输入一个 N 没有输出结果 再输入 S 还是没有 这时代码提示出现,选择 NSArray 然后回车,此时控制台输出:2016-05-13 15:18:36.702 Xcode[3722:1670418] {520, 7}
这个方法只有选择代码提示输入才会调用,并且能返回输入的位置和长度,这样就可以完整的得到输入内容,而且不是单个的字母而是整个单词.接下来就是利用自带的语音库让代码发声了.

会叫得代码

- (void)jx_selectFirstPlaceholderInCharacterRange:(struct _NSRange)arg1{    NSLog(@"%@",NSStringFromRange(arg1));    //得到输入的内容    NSString * str = [self.textStorage.string substringWithRange:arg1];    //系统语音库    NSSpeechSynthesizer * speech = [[NSSpeechSynthesizer alloc] init];    [speech startSpeakingString:str];    [self jx_selectFirstPlaceholderInCharacterRange:arg1];}

完成了!

制作过程中得坑

  • 尝试自己导出私有API的库,但是总是失败,最后原因确实Xcode7加壳了,吐血.有兴趣的可以研究下破壳,网上有教程,感谢将头文件上传到git的前辈大神

  • 在build Phase中导入DVTKit总是会报错,后来直接拖进去反而好了.

  • 新建的DVTSourceTextView.h系统编译不到,然后就无法创建类别进行hook,这里纠结了好半天,看了下别人的代码都可以创建,怎么都想不通,也编译了几次都不行,最后将这个.h导入了一个类才编译出来.坑啊

1 0