手工构建 Mac OS APP (一)
Table of Contents
- 手工建立 Mac OS APP?
- main 函数中的故事
- 最简结构
- app 程序的简单结构
- 做点事情
- 略进一步
- 带主菜单的 app
- 状态栏菜单
- 手工调试
- 再进一步?
- Aout Me
手工建立 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 #importint main(int argc, const char *argv[]) { return 0; }
这个程序用clang可以直接编译,不过它什么功能也没有。我们直接跳过 Hello World什么的,看下一步。
app 程序的简单结构
我们看一下XCode生成的项目的话,会发现 main.m 简单到离谱:
//simple.m #importint 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 (右键,然后选“查看包内容”),就可以看到它的结构,招方抓药:
- 建立 simple.app/Contents/MacOS 目录
- 把编译出来的可执行文件 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#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#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 :
NSPrincipalClass NSApplication
Info.plist 还可以描述很多非常有用的信息,例如设定app不在dock上显示图标。这个可以查阅 apple 的官方文档或者 google,不多讨论了。在 Makefile 里,我把它复制到了app包对应的位置。
我们看看关键的 AppDelegate.h :
/* -*- mode:objc -*- */ #import@interface AppDelegate : NSObject -(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@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 命令行、多点触控的触摸板、对内容而非滚动条的内容推拉,等等等等。在使用的过程中,我逐渐开始有了一些想法,最终,催生了这次创业。