手工构建 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
#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 (右键,然后选“查看包内容”),就可以看到它的结构,招方抓药:
- 建立 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 <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