zoukankan      html  css  js  c++  java
  • Xcode 插件开发

    我最近一年来都在开发ios应用,不过感觉公司的app维护起来非常麻烦。

    因为公司要为很多个企业订做app,每个app的功能基本相同,只是界面上的一些图片和文字要换掉,功能也有一些小改动。考虑到代码维护的问题,比较好的做法就是只维护一份代码,然后用不同的配置文件来管理各个target的内容。

    当工程里达到上百个target的时候,为工程新增文件就成了一件非常痛苦的事情。

    xcode_plugin_lots_of_targets.png

    我必须一个一个地去勾选所有的targets,往往要花上几分钟的时间来重复无聊的操作,既浪费时间又影响心情,而Xcode居然没有自带全选targets的功能。因此我萌生了一个想法:写一个能自动勾选所有targets的插件。

    google一下Xcode的制作教程,找到了VVDocumenter插件作者写的一篇教程:《Xcode 4 插件制作入门》。

    这篇教程很适合入门,不过里面有些东西由于年代久远,已经不兼容最新的Xcode 6.1了。但是教程里很多细节都写得很详细,建议先看完这篇教程。我看了教程后加上自己的摸索,终于完成了插件的开发,因此在这里把插件的开发过程分享出来。

    本插件的源码下载地址:https://github.com/poboke/AllTargets

    一、安装插件模板

    Alcatraz是一款开源的Xcode包管理器,源码下载地址为:https://github.com/supermarin/Alcatraz

    编译完成之后,重启Xcode,然后点击Xcode顶部菜单”Windows”中的”Package Manager”就可以打开Alcatraz包管理器面板。

    搜索关键字”Xcode Plugin”,可以找到一个”Xcode Plugin”模板,该模板可以用来创建Xcode 6+的插件。

    xcode_plugin_alcatraz.png

    点击左边的图标按钮就可以把模板安装到Xcode里。

    新建一个Xcode工程,选择”Xcode Plugin”模板,本例子的工程名为AllTargets。

    xcode_plugin_template.png

    该模板的部分初始代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    - (id)initWithBundle:(NSBundle *)plugin
    {
        if (self = [super init]) {
            // reference to plugin's bundle, for resource access
            self.bundle = plugin;
             
            // 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 setTarget:self];
                [[menuItem submenu] addItem:actionMenuItem];
            }
        }
        return self;
    }
      
    // Sample Action, for menu item:
    - (void)doMenuAction
    {
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:@"Hello, World"];
        [alert runModal];
    }

    初始代码会在Xcode的”Edit”菜单里加入一个名字为”Do Action”的子菜单,当你点击这个子菜单的时候,会调用doMenuAction函数弹出一个提示框,提示内容为”Hello, World”。

    二、需求分析

    在Xcode里按command+alt+A打开添加文件窗口:

    xcode_plugin_add_to_targets.png

    所有的targets都位于白色矩形视图里,可以猜测该矩形视图是一个NSTableView(大小差不多为320*170),勾选的按钮是一个NSCell。

    首先要获得NSTableView对象,《Xcode 4 插件制作入门》里提到可以使用递归打印subviews的方法来得到某个NSView对象。

    不过我发现一种更简便的方法,在本例子中比较适用。在没打开添加文件窗口之前,NSTableView是不会创建的,而视图创建设置尺寸时都会调用NSViewDidUpdateTrackingAreasNotification通知。所以我们可以先监听该通知,再打开添加文件窗口,这样就能得到添加文件窗口里所有视图对象了,修改代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - (void)doMenuAction
    {
        //监听视图更新区域大小的通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationListener:) name:NSViewDidUpdateTrackingAreasNotification object:nil];
    }
      
    - (void)notificationListener:(NSNotification *)notification
    {
        //打印出视图对象以及视图的大小
        NSView *view = notification.object;
        if ([view respondsToSelector:@selector(frame)]) {
            NSLog(@"view : %@, frame : %@", view, [NSValue valueWithRect:view.frame]);
        }
    }

    编译代码后重启Xcode,打开控制台(Control+空格,输入console),并清空控制台里的log。

    点击Xcode的”Do Action”子菜单开始监听消息,这时打开添加文件的窗口会看到控制台输出一堆log。

    把log复制到MacVim里,搜索”NSTableView”,可以找到一条结果:

    1
    view : < NSTableView: 0x7fb206c65f40>, frame : NSRect: {{0, 0}, {321, 170}}

    可以发现,此TableView的大小为321*170,看来正是我们正在寻找的对象。

    三、hook私有类

    由于NSCell的值是由NSTableView的数据源所控制的,所以我们必须找到NSTableView的数据源,修改一下代码打印出数据源:

    1
    2
    3
    4
    5
    6
    7
    8
    - (void)notificationListener:(NSNotification *)notification
    {
        NSView *view = notification.object;
        if ([view.className isEqualToString:@"NSTableView"]) {
            NSTableView *tableView = (NSTableView *)view;
            NSLog(@"dataSource : %@", tableView.dataSource);
        }
    }

    可以看到控制台输出了log:

    1
    dataSource : < Xcode3TargetMembershipDataSource: 0x7fadb7352830>

    Xcode3TargetMembershipDataSource是Xcode的私有类,位于 /Applications/Xcode.app/Contents/PlugIns/Xcode3UI.ideplugin/Contents/MacOS/Xcode3UI 里。由于这个私有类没有frameworks可引用,所以只能通过NSClassFromString来Hook该私有类的函数。

    在这里可以下载从Xcode 6.1 dump出来的私有类头文件:https://github.com/luisobo/Xcode-RuntimeHeaders/tree/xcode6-beta1

    打开Xcode3TargetMembershipDataSource.h,部分代码如下:

    1
    2
    3
    4
    5
    6
    7
    @interface Xcode3TargetMembershipDataSource : NSObject {
        NSMutableArray *_wrappedTargets;
        //......
    }
      
    - (void)updateTargets;
    //......

    _wrappedTargets数组很有可能保存着targets的信息,updateTargets函数的作用应该是用来更新targets的值,所以可以试试hook updateTargets函数,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    //originalImp用来保存原私有类的方法
    static IMP originalImp = NULL;
      
    @implementation AllTargets
      
    //......
      
    - (void)doMenuAction
    {
        [self hookClass];
    }
      
    - (void)hookMethod
    {
        SEL method = @selector(updateTargets);
         
        //获取私有类的函数
        Class originalClass = NSClassFromString(@"Xcode3TargetMembershipDataSource");
        Method originalMethod = class_getInstanceMethod(originalClass, method);
        originalImp = method_getImplementation(originalMethod);
         
        //获取当前类的函数
        Class replacedClass = self.class;
        Method replacedMethod = class_getInstanceMethod(replacedClass, method);
      
        //交换两个函数
        method_exchangeImplementations(originalMethod, replacedMethod);
    }
      
    - (void)updateTargets
    {
        //先调用原私有类的函数
        originalImp();
         
        //查看_wrappedTargets数组里保存了什么类型的对象
        NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets"];
        for (id wrappedTarget in wrappedTargets) {
            NSLog(@"target : %@", wrappedTarget);
        }
    }

    可以看到控制台输出了log,由于工程只有一个target,所以只有一个对象:

    1
    target : < Xcode3TargetWrapper: 0x7f8b59264ab0>

    在Xcode的私有类里找到Xcode3TargetWrapper.h,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @interface Xcode3TargetWrapper : NSObject
    {
        PBXTarget *_pbxTarget;
        Xcode3Project *_project;
        NSString *_name;
        NSImage *_image;
        BOOL _selected;
    }
      
    @property(readonly) NSImage *image; // @synthesize image=_image;
    @property(readonly) NSString *name; // @synthesize name=_name;
    @property BOOL selected; // @synthesize selected=_selected;
    //......

    可以看到,该类有三个属性:图片、名字和是否选中,我们只要把selected属性改为YES就行了。

    我们把updateTargets函数修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (void)updateTargets
    {
        //先调用原私有类的函数
        originalImp();
         
        //修改wrappedTarget的属性
        NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets"];
        for (id wrappedTarget in wrappedTargets) {
            [wrappedTarget setValue:@YES forKey:@"selected"];
        }
    }

    再次编译重启Xcode,打开添加文件窗口,可以发现所有targets都自动选中了。

    xcode_plugin_auto_select_all_targets.png

    四、添加菜单

    考虑到有时可能要关闭这个功能,所以可以给菜单加上是否选中的状态,此外还可以给Xcode加上一个独立的Plugins菜单,大部分插件就可以放在这个菜单里,以方便管理。

    xcode_plugin_plugins_menu.png

    创建菜单的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    - (void)addPluginsMenu
    {
        //增加一个"Plugins"菜单到"Window"菜单前面
        NSMenu *mainMenu = [NSApp mainMenu];
        NSMenuItem *pluginsMenuItem = [mainMenu itemWithTitle:@"Plugins"];
        if (!pluginsMenuItem) {
            pluginsMenuItem = [[NSMenuItem alloc] init];
            pluginsMenuItem.title = @"Plugins";
            pluginsMenuItem.submenu = [[NSMenu alloc] initWithTitle:pluginsMenuItem.title];
            NSInteger windowIndex = [mainMenu indexOfItemWithTitle:@"Window"];
            [mainMenu insertItem:pluginsMenuItem atIndex:windowIndex];
        }
         
        //添加"Auto Select All Targets"子菜单
        NSMenuItem *subItem = [[NSMenuItem alloc] init];
        subItem.title = @"Auto Select All Targets";
        subItem.target = self;
        subItem.action = @selector(toggleMenu:);
        subItem.state = NSOnState;
        [pluginsMenuItem.submenu addItem:subItem];
    }
      
    - (void)toggleMenu:(NSMenuItem *)menuItem
    {
        //改变菜单选中状态
        menuItem.state = !menuItem.state;
      
        //重新交换函数,hook与unhook
        [self hookMethod];
    }

    本插件的源码下载地址:https://github.com/poboke/AllTargets

  • 相关阅读:
    用C#实现宽带重新拨号
    CALLBACK FUNCTION 回调函数
    编译程序 VS 解释程序
    《围城》读书笔记
    鼠标点击器
    工作与找工作的日子
    Windows 7下VS2003的查找无响应问题
    收藏几句关于程序的名言
    static知识小结
    如何定义和实现一个类的成员函数为回调函数(转)
  • 原文地址:https://www.cnblogs.com/ligun123/p/4299845.html
Copyright © 2011-2022 走看看