手工构建 Mac OS APP (一)
手工构建 Mac OS APP (一)Table of Contents手工建立 Mac OS APP?main 函数中的故事最简结构app 程序的简单结构做点事情略进一步带主菜单的 app状态栏菜单手工调试再进一步?Aout Me手工建立 Mac OS APP?Mac OS App 开发并不复杂,XCode 提供了很好的开发环境。但是离开XCode呢?对于个人开发者,其实 XCode 是一个非常
手工构建 Mac OS APP (一)
Table of Contents
手工建立 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 命令行、多点触控的触摸板、对内容而非滚动条的内容推拉,等等等等。在使用的过程中,我逐渐开始有了一些想法,最终,催生了这次创业。
更多推荐



所有评论(0)