手工构建 Mac OS APP (一)

杂谈 专栏收录该内容
55 篇文章 0 订阅

手工构建 Mac OS APP (一)

手工建立 Mac OS APP?

Mac OS App 开发并不复杂,XCode 提供了很好的开发环境。但是离开XCode呢?

对于个人开发者,其实 XCode 是一个非常好的 IDE,它有完整的项目组织、代码编辑和浏览、调试、测试、发布功能,并且内置了版本管理支持。但是我仍然有一些理由,去尝试纯手工开发。

  • 如果两个GIT分支分别向同一个项目添加文件,很容易在合并的时候把项目文件搞乱
  • 有时候我们希望快速建立一个原型,XCode够快,但是如果能基于纯文本建立一个模板系统,就更快了
  • 你在开发 APP 的时候升级过你的mac port、homebrew、fink之类的工具吗?我做过……
  • nib 文件的可视化设计和MVC模式非常漂亮,不愧是 Smalltalk 血统。但是有时候我们希望能从编码的角度审视设计
  • 如果需要把项目分发给别人使用,例如开源项目;或者需要无人值守的测试、集成等工作,基于脚本要方便的多
  • 就是想知道项目构建的每个细节
  • ……

还需要更多的理由吗?那我再加一个:我喜欢Emacs……

所以,这里我们会通过几个简单的例子,讨论一下如何纯手工开发 Mac OS App。

main 函数中的故事

最简结构

默认使用 Objective C 这个前提下,最简单的mac 程序,我甚至可以默写:

//simple.m
#import <Foundation/Foundation.h>

int main(int argc, const char *argv[]) {
    return 0;
}

这个程序用clang可以直接编译,不过它什么功能也没有。我们直接跳过 Hello World什么的,看下一步。

app 程序的简单结构

我们看一下XCode生成的项目的话,会发现 main.m 简单到离谱:

//simple.m
#import <Cocoa/Cocoa.h>

int main(int argc, const char* argv[]) {
    return NSApplcationMain(argc, (const char**)argv);
}

这次,编译的时候,你需要加上framework:

clang -framework Cocoa -o simple simple.m

好的,这次编译过了,也生成了二进制文件,但是如果你直接执行 ./simple,会发现系统报错给你看(我用的 Mountain Lion)。

这是因为我们缺少一些配置信息,这个问题我们后面讨论,暂时我们先继续研究如何建立一个 app。

最简单的 app 很容易构造,我们随便打开一个 app (右键,然后选“查看包内容”),就可以看到它的结构,招方抓药:

  1. 建立 simple.app/Contents/MacOS 目录
  2. 把编译出来的可执行文件 simple 复制进去

然后,你就可以执行 open simple.app 运行这个app了。

做点事情

我在 https://github.com/Dwarfartisan/BlackCookbook/tree/master/objc 放了几个示例程序,现在大家可以先看 noxcode ,这个项目很简单。

首先,你需要一个继承自 NSWindow 的新 window 类型,其实我们仅仅是需要重载它的 canBecomeKeyWindow 方法。

这个示例是我按照 http://forums.macnn.com/t/209595/cocoa-without-nib-file-need-help 写的,改了一些东西,所以类型名按原例定为 myWindow,头文件里没什么特别的东西,.m 里也只需要一个定义:

#import "myWindow.h"

@implementation myWindow

-(BOOL) canBecomeKeyWindow {
  return YES;
}

@end

其实,noxcode项目的代码可以精简成只有 myWindow 和这样一个 main.m:

#import <Cocoa/Cocoa.h>
#import "myWindow.h"

int main(int argc, const char *argv[]) {
  @autoreleasepool {
  NSWindow *window = [[myWindow alloc] initWithContentRect:NSMakeRect(50, 100, 200, 300)
     styleMask:NSTitledWindowMask | NSResizableWindowMask
      backing:NSBackingStoreBuffered
         defer:YES];

         NSTextField *text=[[NSTextField alloc] initWithFrame:NSMakeRect(10, 60, 180, 32)];
         text.stringValue = @"sample text";
         NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(10, 10, 180, 32)];
         [button setBezelStyle:NSRoundedBezelStyle];
         [button setTitle:@"Quit"];
         [button setTarget:NSApp];
         [button setAction:@selector(terminate:)];

         [window setTitle:@"test1"];

         [[window contentView] addSubview:text];
         [[window contentView] addSubview:button];

         [NSApplication sharedApplication];
         [window makeKeyAndOrderFront:nil];
         [NSApp run];
  }
  return 0;
}

原示例中还有个 myView ,是原作者演示自定义view的,可以去掉,这样我们就有了一个带窗口的app。

然后你可以手工编译它,自己建立对应的app包,也可以用这样一个 Makefile:

CC=clang
BUILD=$(CC) -fobjc-arc
LINK=$(BUILD) -framework Cocoa

.PHONY: all run clean
all: build
    mkdir -p mytest.app/Contents/MacOS
    cp mytest mytest.app/Contents/MacOS/

run: all
    open mytest.app

build: myWindow.o main.o
    $(LINK) -o mytest myWindow.o main.o

myWindow.o:
    $(BUILD) -c myWindow.m

main.o:
    $(BUILD) -c main.m

clean:
    -rm mytest myWindow.o main.o
    -rm *~
    -rm -r mytest.app

Makefile 的详细使用方法不多解释了,这个东西我确实也不是内行,只是看了一下教程然后写来图省事的……

略进一步

Congratulations ! 我们有了带窗口的 app 。但是很多程序在启动的时候,并没有一个初始窗口。我们接下来构造两种常见的 app ,一种带有主菜单,一种带有状态栏菜单。

带主菜单的 app

完整的项目示例在这里:

https://github.com/Dwarfartisan/BlackCookbook/tree/master/objc/mainmenu

这里我们自定义了一个 MainMenu 类型,主要是为了把菜单结构的构造封装起来,跳过这一步,我们先看 main.m :

#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        NSApplication *app = [NSApplication sharedApplication];
        id delegate = [[AppDelegate alloc] init];
        app.delegate = delegate;

        return NSApplicationMain(argc, (const char**)argv);
    }
}

这里跟以前的例子不同的是,我构造了一个 app delegate 结构的应用,实际的 GUI 拼装过程是从 delegate 内部进行的。另外,上个例子中有个 [NSApp run],这很关键。它是 Cocoa 程序的事件循环。如果没有它,我们需要一个 Info.plist ,告诉系统启动 app 的时候,如何找到 NSPrincipalClass 。 在这个项目的代码库中,我们可以找到这个Info.plist :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
</dict>
</plist>

Info.plist 还可以描述很多非常有用的信息,例如设定app不在dock上显示图标。这个可以查阅 apple 的官方文档或者 google,不多讨论了。在 Makefile 里,我把它复制到了app包对应的位置。

我们看看关键的 AppDelegate.h :

/* -*- mode:objc -*- */

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>

-(IBAction) quit:(id)sender;

@end

和 AppDelegate.m :

/* -*- mode:objc -*- */

#import "AppDelegate.h"
#import "MainMenu.h"

@implementation AppDelegate

-(void) applicationDidFinishLaunching:(NSNotification *)notification {
    NSApplication *app = [NSApplication sharedApplication];
    MainMenu *mainMenu = [[MainMenu alloc] init];

    mainMenu.quitItem.target = self;
    mainMenu.quitItem.action = @selector(quit:);

    app.mainMenu = mainMenu;
}

-(IBAction) quit:(id)sender {
    [NSApp terminate:self];
}

@end

在这里,delegate 完成了设定 Main Menu 的工作,其实做过 iOS 开发的朋友应该知道,XCode的默认iOS app模板,就是在这个函数中构造 window 对象的。Mac OS app的项目中我们没有看到这个代码,其实是通过 Info.plist 设置了 nib ,由nib加载过程完成了这部分操作。

MainMenu 类型内部没有什么技术含量,其实就是通过代码完成了 interface builder 的工作。然后暴露出用于绑定事件的menu item。需要注意的是,MainMenu 的第一个子菜单总是被设定为应用的主菜单。它的title会被应用名覆盖。各个子菜单会顺序出现在菜单栏上,成为应用程序的菜单。

另外,用于绑定nib的对象属性总是设置为弱引用(非arc的assign,或者arc项目的weak),而我手工绑定,就把它设置为 strong(对应非arc项目的retain)。

下面是头文件:

/* -*- mode:objc -*- */
#import <Cocoa/Cocoa.h>

@interface MainMenu:NSMenu {

}

@property (strong, nonatomic) IBOutlet NSMenuItem* quitItem;
@property (strong, nonatomic) IBOutlet NSMenuItem* aboutItem;

@end

和代码文件:

/* -*- mode:objc -*- */
#import "MainMenu.h"

@implementation MainMenu
@synthesize quitItem, aboutItem;

-(id) init {
    // the title will be ignore
    self = [super initWithTitle:@"Main Menu"];
    if(self){
        // NSMenu.menuBarVisible = YES;

        // this title will be ignore too
        NSMenuItem * appItem = [[NSMenuItem alloc] initWithTitle:@"App Item" action:Nil keyEquivalent:@""];
        [self addItem:appItem];
        // this title will be ignore too
        NSMenu *appMenu = [[NSMenu alloc] initWithTitle:@"application"];
        self.aboutItem  = [[NSMenuItem alloc] initWithTitle:@"about" action:Nil keyEquivalent:@""];
        [appMenu addItem:self.aboutItem];
        [appMenu addItem:[NSMenuItem separatorItem]];
        self.quitItem = [[NSMenuItem alloc] initWithTitle:@"quit" action:Nil keyEquivalent:@""];
        [appMenu addItem:self.quitItem];
        [self setSubmenu:appMenu forItem:appItem];

        // this title will be ignore too
        NSMenuItem * windowItem = [[NSMenuItem alloc] initWithTitle:@"Window Item" action:Nil keyEquivalent:@""];
        [self addItem:windowItem];
        NSMenu *windowMenu = [[NSMenu alloc] initWithTitle:@"window"];
        [windowMenu addItemWithTitle:@"hide me" action:Nil keyEquivalent:@""];
        [windowMenu addItemWithTitle:@"hide others" action:Nil keyEquivalent:@""];
        [self setSubmenu:windowMenu forItem:windowItem];
    }
    return self;
}

@end

状态栏菜单

屏幕右上角的 status bar 是常驻型工具(如qq或evernote)的必争之地。构造这种类型的应用其实不比main menu更复杂,只要能拿到 status bar item ,把菜单挂上去就可以。 这个例子

https://github.com/Dwarfartisan/BlackCookbook/tree/master/objc/statusmenu

演示了相关的方法,这里我们只要看跟 main menu 示例有区别的地方,也就是app delgate:

/* -*- mode:objc -*- */

#import "AppDelegate.h"
#import "MainMenu.h"

@implementation AppDelegate
@synthesize statusItem;

-(void) applicationDidFinishLaunching:(NSNotification *)notification {
    self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    self.statusItem.title = @"dwarf clip";
    MainMenu *menu = [[MainMenu alloc] init];
    menu.quitItem.target = self;
    menu.quitItem.action = @selector(quit:);
    self.statusItem.menu = menu;
}

-(IBAction) quit:(id)sender {
    [NSApp terminate:self];
}

@end

事实上,mac app 中完全可以同时有 main menu 和 status menu。另外,如果想要去掉 dock icon,可以修改 Info.plist 来设定。这个不演示了,网上有很多介绍,可以直接抄过来试试,还有通过编程来修改的。

手工调试

对于命令行老鸟们,调试 mac os app 没有什么特殊的,clang 编译的时候,加上 -g ,就可以加入编译信息。然后可以用 lldb your.app 进入调试状态。

再进一步?

大家看到了,有得就有失,如果要通过命令行复现 XCode 的所有工作,还有很多路要走,例如加入签名、打包成 dmg、设置图标(这个其实倒很简单)、集成调试,以及,设置好你的编辑器,等等。我们的目标并不是完全排斥xcode,而是摸清app开发中的项目管理细节,更好的运用整个操作系统和开发工具提供给我们的所有资源,让工作更简单,更可靠。

我们现在已经可以手工构造基本的 mac os app了,进一步的技巧,我会随着研发工作的进一步深入,继续整理发布。

Aout Me

我是一个刚刚开始创业的工程师,我的工作室 Dwarf Artisan(矮人工匠)主要的定位是 Mac OS 和相关平台的效率类工具。

Mac OS真是个迷人的系统,特别是升级到 Lion 以后,我感觉自己真的喜欢上了还在分期付款的 MacBook Pro。全屏、Unix 命令行、多点触控的触摸板、对内容而非滚动条的内容推拉,等等等等。在使用的过程中,我逐渐开始有了一些想法,最终,催生了这次创业。

Date: 2012-09-27 20:27:55 CST

Author: March Liu <March.liu@gmail.com>

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0
  • 6
    点赞
  • 13
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值