一个修正 Mach-O 文件加载共享库路径的脚本

在 Mac 上用到一些开源的程序,经常需要自己编译,这个时候一般会使用 macports 所提供的工具链进行编译。

在编译的过程中,开源程序所引用的各种其他库,例如 libssl、libz、libgd 之类的,一般都会在 /opt/local/lib 下,而如果把这个编译好的二进制文件给其他人用时,如果其他人没有安装 macports,那么就会缺失这些共享库,从而导致编译好的二进制程序无法运行。

具体原因就是因为 Mac 下链接共享库时,会在链接时将共享库的路径写入到最终的二进制文件中,而默认情况下,这个路径是绝对路径,例如 /opt/local/lib/libz.1.dylib。

所幸苹果提供了 install_name_tool 这个工具来修改共享库的路径。

下面这个脚本就是用来批量替换二进制文件中的共享库路径。

这个脚本会在当前目录下创建 lib 目录,将二进制文件所依赖的 /opt/local/lib 中的共享库复制到 lib 目录中,并修改二进制文件中保存的共享库的路径。

脚本的使用方法,将上面的脚本保存为 fixlibpath.sb,并加上可执行权限,放到要修正的二进制文件所在的目录:

chmod +x fixlibpath.sh
./fixlibpath.sh mybinary

还有共享库会依赖其他共享库的情况,所以也需要对 lib 中复制过来的共享库进行同样的处理:

for f in lib/*; do
    ./fixlibpath.sh $f
done

可以观察一下脚本的输出,如果输出的内容中,每个共享库所依赖的库路径中不存在 /opt/local/lib,那就表示处理完成了。每个共享库都会有一个 /opt/local/lib 路径的,但是名称与这个共享库一样的路径,这个是不用处理的。

要测试处理完成的二进制文件是否正常,可以将该二进制文件和 lib 目录复制到一台没有安装 macports 或者没有 macports 安装相关共享库的机器上运行,如果能正常运行就表示处理成功了。

参考资料

  1. http://www.mikeash.com/pyblog/friday-qa-2009-11-06-linking-and-install-names.html
  2. http://stackoverflow.com/questions/4677044/how-to-use-dylib-in-mac-os-x-c#answer-11585225

— EOF —

在使用 Linux 系统的 NAS 上部署 Fitbit 数据同步程序

使用 Fitbit 已经三个多月了,用它来记录每天的运动量以及睡眠质量感觉很方便。

不过因为我用的电脑是笔记本,如果要同步 Fitbit 的数据,就需要把 Fitbit Base Station 连接在笔记本电脑上,然后 Fitbit 的数据才会同步到官网上。

因为家里没有 24 小时开机的电脑,所以没办法直接把 Fitbit Base Station 连接在一台电脑上实现回家自动同步。

但是家里有下载机是 24 小时运行的,于是去 Fitbit 官网看看有没有提供 Linux 下的 Fitbit 同步程序。

很不幸的是,Fitbit 官方只提供了 Windows 和 Mac OS X 版本的同步程序,没有提供 Linux 下的 Fitbit 同步程序。

昨天突然想到去网上找找看有没有非官方的 Linux 平台 Fitbit 同步程序,没想到还真找到了。

在网上找到了两个项目,用来提供跨平台的 Fitbit 同步功能。

开源的 Fitbit 同步程序

libfitbit

项目地址:

libfitbit is an implementation of the data retrieval protocol for the fitbit health tracking device. It also implements a synchronization client for synchronizing data with the fitbit website on platforms not supported by Fitbit currently.

fitbitd

项目地址:

fitbitd synchronises FitBit trackers with the FitBit.com service. You simply leave it running in the background and it will synchronise the tracker periodically, just like the official FitBit software does for Windows or Mac OS.

libfitbit 是用 python 编写的,功能上可能相对简陋一些,而 fitbitd 是使用 C 编写的,功能相对于 libfitbitd 强大一点,并且提供了 Linux 桌面环境中的状态栏插件。

不过因为我是要在 DS211j 上安装,而 DS211j 的 CPU 是 ARM 而非 x86,因此如果要安装这些程序都需要自己来编译。

在多次尝试之后,我选择了 libfitbit 而不是 fitbitd,因为 fitbitd 要用到 dbus,在编译时会遇到很多问题。如果只是为了同步 Fitbit 数据,libfitbit 已经基本够用,并且像 daemon 模式,也可以通过 nohup 来实现。

在 DS211j 上安装包管理程序 ipkg

注意:文中所有在 NAS 上的操作均以 root 用户进行

首先需要在 DS211j 的控件台里启用 SSH 服务,然后使用 SSH 连接到 NAS,安装 ipkg:

wget http://ipkg.nslu2-linux.org/feeds/optware/cs08q1armel/cross/unstable/syno-mvkw-bootstrap_1.2-7_arm.xsh
chmod +x syno-mvkw-bootstrap_1.2-7_arm.xsh
./syno-mvkw-bootstrap_1.2-7_arm.xsh
ipkg update

注意,如果安装完 ipkg 可能需要修改一下 .profile 文件,防止 /opt 可能没有加到 PATH 环境变量的问题,需要把:

PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
export PATH

修改为:

PATH=/opt/bin:/opt/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
export PATH

安装所需软件

编译以及运行所需软件包如下:

  • gcc
  • python27

直接使用 ipkg 安装即可:

ipkg install gcc python27

编译 libusb-1.0

DS211j 使用的软件源中的 libusb 是 0.1 版本的,而 libfitbit 需要的是 1.0。如果使用 libusb-0.1 虽然 libfitibt 可以运行,但是无法接收数据。

先去 下载一个 libusb-1.0.9,解压到放到 NAS 上,然后进行编译安装。

注意在编译前需要修改 libusb 的 Makefile,默认情况下编译的话,在使用时会出现“undefined reference to `clock_gettime’” 错误。

./configure --prefix=/opt
vi libusb/Makefile
# 修改 LDFLAGS= 为 LDFLAGS=-lrt
make
make install

使用 libfitbit

下载 libfitbit,解压后把 python 目录放到 NAS 的 /root 中,并且重命令为 libfitbit。

下载 libfitbit 依赖的 pyusb 模块,解压后把 usb 目录放到 NAS 的 /root/libfitbit 目录中。

这时需要把 Fitbit Base Station 连接到 DS211j 上。

在 NAS 上进入 /root/libfitbit 目录,运行 python2 fitbit.py 测试安装是否正常。

Start reset () {}
    sent: ['a4', '01', '4a', '00', 'ef']
received: ['a4', '01', '6f', '20', 'ea']
End reset None
# 其他日志省略...
End open_channel None
Waiting for receive

如果出现 Waiting for receive 就说明 fitbit.py 已经正常运行了,如果有 Fitbit Ultra Tracker 在 Fitbit Base Station 附近,那么就会继续出现其他日志。

接下来就要让 libfitbit 在后台一直运行了。libfitbit 本身提供了每隔 15 分钟同步一次数据的功能,但没有提供后台运行的功能,不过借助于 Linux 中的 nohup 命令可以轻松实现。

nohup python2 fitbit_client.py > /tmp/libfitbit.log &

注意,这里会把 libfitbit 产生的日志输出到 /tmp/libfitbit.log 中,但是 /tmp 的容量不会很大,所以正式使用时,可以直接将日志输出到 /dev/null:

nohup python2 fitbit_client.py > /dev/null &

结语

感谢 Kyle Machulis 带来这么棒的开源项目。

最开始一直尝试在 My Book Live 上配置 libfitbit 和 fitbitd,但是弄到最后突然发现,My Book Live 并没有 USB 接口⋯⋯

如果只是 Linux 用户希望在 Linux 系统下同步数据,如果又刚好是 Ubuntu 或 Mint 用户的话,那么使用 fitbitd 可能是一个更好的选择,因为作者已经提供了一个 PPA 源来直接安装 fitbitd,并且还有状态栏插件可以用。Arch 用户也可以直接用 sudo pacman -Sy fitbitd-git 来安装。

当然,如果你也像我一样,不喜欢电脑上拖着个小尾巴,家里也刚好有个带 USB 接口并且 24 小时运行的 Linux 设备,也么也可以折腾一下。

希望此文对爱好 量化生活 的朋友们有所帮助。

参考资料

  1. Overview on modifying the Synology Server, bootstrap, ipkg etc
  2. What kind of CPU does my NAS have
  3. http://sourceforge.net/projects/libusb/files/libusb-1.0/
  4. undefined reference to涉及的链接问题
  5. https://github.com/qdot/libfitbit
  6. http://sourceforge.net/projects/pyusb/files/PyUSB%201.0/
  7. http://www.paulburton.eu/projects/fitbitd/
  8. http://www.fitbit.com/

— EOF —

[Tips] 适配 iOS App 到 iPhone 5 和 iOS 6

iPhone 5 和 iOS 6 正式发布后,iOS 开发者们就要开始做 iPhone 5 和 iOS 6 的适配工作了 :)

在迁移我自己的几个 iOS App 过程中,发现了老的 Xcode 工程代码在 iOS 6 中无法响应屏幕旋转的问题,在这里记录一下。顺便记录一下怎样适配 iOS App 到 iPhone 5 给还不知道怎么做的朋友们。

适配 iPhone 5 的 4 寸屏幕


iOS App LetterBox Mode

iPhone 5 的屏幕分辨率为 640×1136,默认情况下,老的 iOS App 会以上下加两条黑边的模式来运行,程序实际占用的分辨率还是 640×960,保证了原有的 iOS App 在不修改的情况下也可以正常运行。

如果需要适配 iPhone 5 的 4 寸屏幕,是一件很简单的事,只需要添加一张对应 iPhone 5 启动图片即可。

iPhone 5 的分辨率为 640×1136,所以只需要添加一张分辨率为 640×1136 的启动图片到 iOS App 工程即可。启动图片需要命名为 Default-568h@2x.png,因为 iPhone 5 没有非 Retina 屏幕的型号,所以不需要 Default-568h.png 这样一个文件了。

添加了对应 iPhone 5 分辨率的启动图片后,iOS App 就会以 640×1136 的分辨率运行了。

当然了,如果 iOS App 里在计算 view 的 frame 时,使用了固定的 320×480 来计算的话,在显示时就会有一些问题,这时候就需要动态根据 view 的 frame 大小去计算,而不是使用 Hard Code。

另外,也可以灵活使用 autoresizingMask,这样在未来 iPhone 又有其他分辨率,又或者出现了 iPad mini 的时候更方便处理。

解决旧的 iOS App 在 iOS 6 中无法随屏幕旋转的问题

如果你的 iOS App 支持随屏幕旋转而旋转,但是到了 iOS 6 中,发现这个功能不生效了,那么就要检查一下是不是因为旧版本的 Xcode 创建工程的原因。

但是在 iOS SDK Release Notes for iOS 6.0 中的 UIKit 一节中说到“Autorotation is changing in iOS 6. In iOS 6, the shouldAutorotateToInterfaceOrientation: method of UIViewController is deprecated. In its place, you should use the supportedInterfaceOrientationsForWindow: and shouldAutorotate methods.”,并且详细解释中也说明了 iOS 6 中的改动:

More responsibility is moving to the app and the app delegate. Now, iOS containers (such as UINavigationController) do not consult their children to determine whether they should autorotate. By default, an app and a view controller’s supported interface orientations are set to UIInterfaceOrientationMaskAll for the iPad idiom and UIInterfaceOrientationMaskAllButUpsideDown for the iPhone idiom.

A view controller’s supported interface orientations can change over time—even an app’s supported interface orientations can change over time. The system asks the top-most full-screen view controller (typically the root view controller) for its supported interface orientations whenever the device rotates or whenever a view controller is presented with the full-screen modal presentation style. Moreover, the supported orientations are retrieved only if this view controller returns YES from its shouldAutorotate method. The system intersects the view controller’s supported orientations with the app’s supported orientations (as determined by the Info.plist file or the app delegate’s application:supportedInterfaceOrientationsForWindow: method) to determine whether to rotate.

The system determines whether an orientation is supported by intersecting the value returned by the app’s supportedInterfaceOrientationsForWindow: method with the value returned by the supportedInterfaceOrientations method of the top-most full-screen controller.

The setStatusBarOrientation:animated: method is not deprecated outright. It now works only if the supportedInterfaceOrientations method of the top-most full-screen view controller returns 0. This makes the caller responsible for ensuring that the status bar orientation is consistent.

For compatibility, view controllers that still implement the shouldAutorotateToInterfaceOrientation: method do not get the new autorotation behaviors. (In other words, they do not fall back to using the app, app delegate, or Info.plist file to determine the supported orientations.) Instead, the shouldAutorotateToInterfaceOrientation: method is used to synthesize the information that would be returned by the supportedInterfaceOrientations method.

说明中提到,在 iOS 6 中系统会通过查询 rootViewController 来判断是否可以旋转视图,但是在以前,使用 Xcode 创建一个工程时,通常会有以下代码来显示主窗口:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    // Add the tab bar controller's view to the window and display.
    [self.window addSubview:tabBarController.view];
    [self.window makeKeyAndVisible];
 
    return YES;
}

在这里,并没有设置 self.window 的 rootViewController,而且在 MainWindow.xib 中,也没在连接 window 的 rootViewController 到默认的 UITabBarController。

因此,我们有两种方法来解决这个问题:

1. 在代码中设置 rootViewController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    // Add the tab bar controller's view to the window and display.
    self.window.rootViewController = tabBarController;
    [self.window addSubview:tabBarController.view];
    [self.window makeKeyAndVisible];
 
    return YES;
}

2. 在 MainWindow.xib 中连接 window 的 rootViewController

两种方法任意选择一种就行了。

当然使用新版本 Xcode 创建的工程应该是没问题的,而且新版本 Xcode 的工程模板通常会使用代码来创建 rootViewController,而且默认也不再生成 MainWindow.xib。

参考资料

  1. iOS SDK Release Notes for iOS 6

—EOF—

Mac OS X 中实现支持任意位置拖动的不规则窗体

Mac OS X 中实现支持任意位置拖动的不规则窗体

在开发 Mac OS App 时,根据视觉设计师的需要,有些时候会要实现一些不规则的窗口。一般不规则窗口都不包含 Mac OS X 标准窗口的标题栏,这样用户就没有办法通过拖动标题栏来拖动窗口。

为了能让用户拖动一个无标题栏的窗口,我们就需要自己去处理这个移动窗口的过程。另外,由于窗口有可能是不规则的,那么同样就没有办法拖动窗口边缘来改变窗口大小了,同样需要自己进行处理。

因为要处理鼠标点击事件,因此我们先创建一个继承于 NSWindow 的类 JWBorderlessWindow,以下代码都是此类的代码实现。

初步实现

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//
 
@implementation JWBorderlessWindow
 
- (BOOL)canBecomeKeyWindow {
    return YES;
}
 
 
- (BOOL)canBecomeMainWindow {
    return YES;
}
 
 
- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
 
    // 窗口右下角 20x20 的区域为改变窗口的区域
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
 
    return resizeRect;
}
 
 
// 处理鼠标按下事件
- (void)mouseDown:(NSEvent *)event {
    NSPoint pointInView = [event locationInWindow];
 
    // 判断是否点击在改变窗口大小区域
    BOOL resize = NSPointInRect(pointInView, [self resizeAreaRect]);
 
    NSWindow *window = self;
    NSPoint originalMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSRect originalFrame = [window frame];
 
    while (YES) {
        // 捕获鼠标拖动或鼠标按键弹起事件
        NSEvent *newEvent = [window nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
 
        if ([newEvent type] == NSLeftMouseUp) {
            break;
        }
 
        // 计算鼠标移动的偏移
        NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]];
        NSPoint delta = NSMakePoint(newMouseLocation.x - originalMouseLocation.x,
                                    newMouseLocation.y - originalMouseLocation.y);
 
        NSRect newFrame = originalFrame;
 
        if (!resize) {
            // 移动窗口
            newFrame.origin.x += delta.x;
            newFrame.origin.y += delta.y;
 
        } else {
            NSSize maxSize = [window maxSize];
            NSSize minSize = [window minSize];
 
            // 改变窗口大小
            newFrame.size.width += delta.x;
            newFrame.size.height -= delta.y;
 
            // 控制窗口大小在限制范围内
            newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
            newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
            newFrame.origin.y -= newFrame.size.height - originalFrame.size.height;
        }
 
        [window setFrame:newFrame display:YES animate:NO];
    }
}
 
@end

以上代码借鉴自 RoundWindow 示例代码。

这些代码就实现了点击窗口就可以拖动窗口,点击 resizeAreaRect 所返回的区域就可以改变窗口大小。

但是在用的时候,会发现一个问题,如果这个窗口上有控件的话,例如有按钮、文本框等控件,那么就鼠标点击在按钮上时,就无法拖动这个窗口了。

有些时候可能整个窗口是一个列表,没有空余的地方露出 Window 本身,那想要拖动窗口就会变得很麻烦了。

实现任意位置移动

因为 NSWindow 的 mouseDown: 方法,只会在点击在窗口本身上时,才会被调用,而如果点击在了按钮或者文本框上,这个鼠标事件就不会传递到 NSWindow 上。

根据《Cocoa Event Handling Guide》可以知道,如果有事件过来,会由 NSApp 分发到 NSWindow,再通过 NSWindow,使用 sendEvent: 方法来分发,那么我们就可以在个方法中,去捕获鼠标点击事件。

Path of a mouse event
via Cocoa Event Handling Guide

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//
 
@interface JWBorderlessWindow : NSWindow {
    BOOL            mouseDraggedForMoveOrResize;
    BOOL            mouseDownInResizeArea;
    NSPoint         mouseDownLocation;
    NSRect          mouseDownWindowFrame;
}
@end
 
@implementation JWBorderlessWindow
 
- (BOOL)canBecomeKeyWindow {
    return YES;
}
 
 
- (BOOL)canBecomeMainWindow {
    return YES;
}
 
 
- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
 
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
 
    return resizeRect;
}
 
 
- (void)handleMoveOrResize:(NSEvent *)event {
    NSWindow *window = self;
 
    //
    // Work out how much the mouse has moved
    //
    NSPoint newMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSPoint delta = NSMakePoint(newMouseLocation.x - mouseDownLocation.x,
                                newMouseLocation.y - mouseDownLocation.y);
 
    NSRect newFrame = mouseDownWindowFrame;
 
    if (!mouseDownInResizeArea) {
        //
        // Alter the frame for a drag
        //
        newFrame.origin.x += delta.x;
        newFrame.origin.y += delta.y;
 
    } else {
        NSSize maxSize = [window maxSize];
        NSSize minSize = [window minSize];
 
        newFrame.size.width += delta.x;
        newFrame.size.height -= delta.y;
 
        newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
        newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
        newFrame.origin.y -= newFrame.size.height - mouseDownWindowFrame.size.height;
    }
 
    [window setFrame:newFrame display:YES animate:NO];
}
 
 
- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    if (event.type == NSLeftMouseDown) {
        mouseDraggedForMoveOrResize = NO;
        mouseDownLocation = [self convertBaseToScreen:[event locationInWindow]];
        mouseDownWindowFrame = [self frame];
        mouseDownInResizeArea = NSPointInRect([event locationInWindow], [self resizeAreaRect]);
 
        BOOL keepOn = YES;
 
        while (keepOn) {
            NSEvent *newEvent = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
                                                  untilDate:[NSDate distantFuture]
                                                     inMode:NSEventTrackingRunLoopMode
                                                    dequeue:NO];
            switch (newEvent.type) {
                case NSLeftMouseDragged:
                    // 处理鼠标移动事件
                    [self handleMoveOrResize:newEvent];
                    // 把事件从队列中删除
                    [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
                    mouseDraggedForMoveOrResize = YES;
                    break;
 
                case NSLeftMouseUp:
                    // 如果不是正在移动窗口或改变窗口大小,就把事件继续分发
                    if (!mouseDraggedForMoveOrResize) {
                        [super sendEvent:event];
                    }
                    keepOn = NO;
                    break;
 
                default:
                    keepOn = NO;
                    break;
            }
        }
 
    } else {
        [super sendEvent:event];
    }
}
 
@end

这时候又会有另外一个问题,虽然可以在窗口任意位置都可以拖动窗口,但是在 NSTextField 或者 NSTextView 获取焦点时,如果想用鼠标拖动光标来选择文本,是没办法做到的,在拖动的时候变成了在拖动整个窗口。

好吧,继续解决问题。

修正 NSTextField 及 NSTextView 兼容性

- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    // 判断鼠标点击所在位置的 view,如果是 NSTextView,就不处理,直接继续传递事件
    NSView *targetView = [self.contentView hitTest:[event locationInWindow]];
    if (event.type == NSLeftMouseDown && ![targetView isKindOfClass:[NSTextView class]]) {
        // 省略,与前一段代码相同
 
    } else {
        [super sendEvent:event];
    }
}

完成,在 NSTextField 或者 NSTextView 获得焦点时,可以用鼠标来拖动选中内容了。

结语

下载文中示例程序:JWBorderlessWindowTest (39KB)

参考资料

  1. [http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html](http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html)
  2. Cocoa Event Handling Guide: Event Architecture – Event Dispatch

— EOF —

在 Ubuntu 中使用 GNUstep 搭建 Objective-C 开发环境

GNUstep 介绍见

安装相关程序

直接 apt-get install 搞定。

  • build-essential
  • gnustep
  • gnustep-devel
  • gnustep-examples
  • gobjc
  • gobjc++

设置环境变量

为了方便使用 GNUstep 的各种编译工具,需要先在 .bashrc 中导入 GNUstep 的脚本。

# Setup GNUstep
GNUSTEP_MAKEFILES=/usr/share/GNUstep/Makefiles
export GNUSTEP_MAKEFILES
source $GNUSTEP_MAKEFILES/GNUstep.sh

编写 Makefile

使用 Makefile 来编译 Objective-C App,注意 Makefile 的文件名为 GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

APP_NAME=HelloWorld
HelloWorld_OBJC_FILES=test.m 

include $(GNUSTEP_MAKEFILES)/application.make

这个是编译出 Mac OS App Bundle 形式的目标,如果是编译命令行工具的话,可以使用如下 Makefile:

include $(GNUSTEP_MAKEFILES)/common.make

TOOL_NAME=HelloWorld
HelloWorld_OBJC_FILES=test.m 

include $(GNUSTEP_MAKEFILES)/tool.make

在编译命令行工具时,AppKit 就不会自动链接进来了,使用 NSColor 这样的类就会有问题。

HelloWorld

一个最简单的 HelloWorld 代码:

#import <foundation /Foundation.h>

int main() {
    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    NSLog(@"HelloWorld");
    [pool release];
    return 0;
}

保存为 HelloWorld.m,编写 Makefile:

include $(GNUSTEP_MAKEFILES)/common.make

APP_NAME=HelloWorld
HelloWorld_OBJC_FILES=HelloWorld.m 

include $(GNUSTEP_MAKEFILES)/application.make

保存 Makefile 为 GNUmakefile,执行 make,会在当前目录生成 HelloWorld.app,使用 openapp 命令运行:

openapp ./HelloWorld.app

当然如果是编译成命令行工具的目标的话,会在当前目录的 obj 目录中生成 HelloWorld 可执行文件,这个可以直接从命令行运行:

./obj/HelloWorld

参考资料

  1. http://www.gnustep.org
  2. http://www.qiongbupa.com/archives/678
  3. http://www.gnustep.org/resources/documentation/User/GNUstep/gnustep-howto_4.html#SEC8
  4. http://www.gnustep.it/nicola/Tutorials/WritingMakefiles/node6.html
  5. http://www.gnustep.it/nicola/Tutorials/index.html

— EOF —

在 Mac App 中支持全屏和文件版本控制

前言

在 Mac OS X 10.7 Lion 中,新增了两个特性对用户来说可能比较有用的特性,一个是全屏程序,一个是文件版本控制。

全屏程序可以使用户集中注意力到一个程序上,并且隐藏菜单栏、Dock 等不相关的界面元素,使得工作区最大化,更加有效地利用有限的屏幕空间。

文件版本控制可以让用户在不借助于 Time Machine、Git 或者 SVN 这些工具的时候,也可以轻松恢复文件到之前保存过的版本,防止一些误操作删除了重要内容。

全屏程序

全屏功能可以让程序以全屏方式显示,这样用户可以拥有最大的操作区域,并隐藏与程序无关的视觉元素。

在全屏方式下,Dock 和菜单栏都会自动隐藏,可以通过将鼠标移动到屏幕边缘的方式来重新显示 Dock 或者菜单栏。

目前全屏程序有一个问题是在使用双屏的情况下,有一个屏幕会无法使用,只能显示背景图片,而不能同时使用两个屏幕,或者将另外一个程序全屏在另一个屏幕。

另外,全屏程序切换是使用水平动画切换,对我来说可能动作太大,不是很习惯,这是题外话了。

要设置一个 App 支持全屏很简单,只需要设置对应 Window 的属性即可。

Full Screen 有两种模式,分别为 NSWindowCollectionBehaviorFullScreenPrimaryNSWindowCollectionBehaviorFullScreenAuxiliary,其中多数时候只需要使用 NSWindowCollectionBehaviorFullScreenPrimary 即可。

在设置 NSWindowCollectionBehaviorFullScreenPrimary 后,就会在相应的 Window 右上角显示一个全屏按钮,用户通过这个按钮就可以切换程序到全屏方式使用。

全屏模式切换菜单

有些时候,为了用户使用方便,需要设置一个快捷键,或者菜单来供用户进行全屏模式的切换,这时,只需要添加一个 NSMenuItem 到主菜单的 View 菜单中,将 selector 连接到 First Responder 的 toggleFullScreen: 即可。

Cocoa 在运行时会自动寻找 View 菜单中 selector 为 toggleFullScreen: 的菜单项目,并根据当前窗体的显示情况来自动更新菜单标题为 Enter Full Screen 或 Exit Full Screen。

文件版本控制

在 Lion 中对普通用户来说,一个非常有用的特性就是文件版本控制了。在不需要 Time Machine,不需要 Git、SVN 等版本控制参与的情况下,只需要程序支持这个功能,就可以直接找回编辑过文件的以前版本。

在文件版本控制之后,其实是自动保存功能在起作用,要实现文件版本控制,只需要 Mac App 实现自动保存即可,文件版本的控制会有框架来做,不需要开发者过多参与。

对于 NSDocument based 的 Mac App 来说,要实现文件自动保存,一般情况下,只需要在 NSDocument 的子类中实现以下方法就行了:

+ (BOOL)autosavesInPlace {
    return YES:
}

当然,这是在 Mac App 中,是以 NSDocument 提供的诸如 - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError 或者 - (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError 来保存文件内容为基础,而不是自己去实现写文件内容到磁盘,那样的话就算有 autosaveInPlace 也是不行的。

另外,同样需要使用 - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError 等 NSDocument 提供的 API 来获取内容。

另外,在 NSWindowDelegate 也有一些新的方法,可以控制程序在浏览文件的以往版本时,控制程序窗口的样式,例如大小,设置控件的状态等。例如,在默认情况下,浏览文件以往版本会改变当前文档窗口的大小,而如果不想修改文档窗口大小,只是缩放显示的话,可以在 window:willResizeForVersionBrowserWithMaxPreferredSize:maxAllowedSize: 方法中指定窗口要变化到的目标大小:

- (NSSize)window:(NSWindow *)window willResizeForVersionBrowserWithMaxPreferredSize:(NSSize)maxPreferredFrameSize maxAllowedSize:(NSSize)maxAllowedFrameSize {
    NSSize winSize = [window frame].size;
    return winSize;
}

NSWindowDelegate 的 Managing Presentation in Version Browsers 一节中,还有另外一个 delegate 方法,可以得知当前文档窗口正在进入版本浏览,或者退出版本浏览等状态,从而可以设置界面控件,将一些无关的界面元素隐藏,使用户可以更加清晰的观察到文件不同版本之间的差别。

要实现文件版本控制的支持,开发者并没有很多工作需要做,多数功能已经由框架实现。

参考资料

— EOF —

高性价比 NAS+HTPC 折腾记

最近组装了一台 NAS+HTPC 合体的机器,自认为还有比较好的性价比,在折腾了两个星期之后,终于差不多搞定所有功能。这台 HTPC 使用 XBMC 作为媒体中心,使用 OpenMediaVault 作为 NAS 系统,这篇博客主要记录 OpenMediaVault 和 XBMC 的安装过程和问题。另外,因为折腾的时候有些地方来来回回搞了好多次,记录的顺序可能并不严谨。

通过 XBMC 可以直接索引所有电影,使用 themoviedb.org 来获取电影海报、介绍、演员等信息,使用 IMDb 来获取电影的评分。使用 OpenMediaVault 来管理存储,设置共享文件夹等。

在设置成功 VA-API 后,XBMC 就可以使用显卡的硬件加速了,在播放 1080P 时,CPU 占用基本在 20% 左右,还算比较不错了。

当然,在一开始我也可以选择使用 Windows 系统,XBMC 也有 Windows 版本,在 Windows 上基本也不用操心驱动程序的事情,而且 Windows 上设置 Samba 共享也是很方便的。

注意:本篇文章都是以 AMD E350 APU 来讲安装过程的,如果你用的主板并不是 E350,或者型号与我使用的不完全一致,可能安装过程会有不同。另外,因为 OpenMediaVault 是基于 Linux 的,所以这需要你懂一些 Linux 知识。

PS. 另外,本篇文章的操作都是直接使用 root 账户进行,如果你在安装的时候使用了非 root 账户,那么多数操作可能需要加 sudo 来运行。

硬件

  • 机箱:万由 N400 4盘位 NAS 机箱
  • 主板:梅捷 SY-E350-U3M
  • 内存:芝奇 G.Skill DDR3 1333 4G
  • 电源:肯亿 200W 小 1U 电源
  • 系统磁盘:SSK SFD201 USB3.0 16G U盘

因为都是在促销,或者优惠的时候购买的,不包邮费总价在 1400 元左右。

软件

  • 基础系统:Debian Squeeze
  • NAS 系统:OpenMediaVault
  • 媒体中心:XBMC 10.1 Dharma

安装系统

创建 OpenMediaVault 安装盘

因为是用的 NAS 机箱,上面是没有光驱位的,而且我也没有现成的光驱可以用,因为需要用 U 盘来引导安装系统。

首先需要准备一个 512M 以上的 U 盘,然后从 openmediavault.org 下载最新版本的 OpenMediaVault,再使用下面的命令将镜像恢复到 U 盘上:

sudo dd if=openmediavault_0.2.5_i386.iso of=/dev/disk1 bs=4096

注意:disk1 是在我的机器上 U 盘所在的位置,另外,在 dd 恢复镜像的时候,要使用 /dev/disk1,而不是 disk1s1,disk1s1 就会将镜像恢复到某个分区而不是整个 U 盘了。

启动盘注意事项

因为是用的 USB 3.0 的 U 盘,我在安装系统的时候,把 U 盘插在了 USB 3.0 的接口上,但是在系统安装程序启动之后,USB 3.0 的 U 盘没有自动识别出来,因此要将 USB 3.0 的 U 盘插在 USB 2.0 的口上。

在安装的过程中,可以将 Debian 源的地址修改为网易的源镜像,这样安装软件的速度更快一些,网易源的地址为:

http://mirrors.163.com/debian/

安装基本软件

先安装一些基本的软件,包括 X.org,Gnome,GDM3,Vim 和 less:

apt-get install xorg gnome-core gdm3 vim less

Gnome 和 GDM3 其实到后面都不会用到,但是在运行一些需要图形界面的工具时,例如 glxinfo、fglrxinfo,就需要有 X 支持了。

设置 debian-multimedia 源

为了让一些包默认使用 debian-multimedia 的源,先将 debian-multimedia 的源添加到 /etc/apt/sources.list

deb http://www.debian-multimedia.org squeeze main non-free 

并将所有其他源行首添加 # 注释掉,运行 apt-get update 更新源。

在更新完成之后,再将其他源行首的 # 删掉,再次运行 apt-get update 更新源。

安装 debian-multimedia keyring

apt-get install debian-multimedia-keyring

设置 XBMC 用户

添加一个用户 xbmc 用户来运行 XBMC:

useradd -m -s /bin/bash xbmc
passwd xbmc

设置用户 openmediavault 密码

在后面的设置过程中,可能需要管理员权限,而这个时候是用的 openmediavault 这个用户,我不清楚这个用户的默认密码是什么,所以需要先设置这个用户的密码:

passwd openmediavault

启用 SSH

在安装完 OpenMediaVault 之后,就可以直接访问它的 Web 界面了,建议启用 SSH 以便直接在本机进行操作,省去使用键盘看着电视操作的麻烦。

OpenMediaVault 默认的管理员用户名为 admin,密码为 openmediavault

继续阅读 »

推开窗户看世界 — Objective-C之外

引子

App Store 如今正风靡世界,许多人都想尝试去自己开发一个 iOS App,不过苹果官方推荐使用的是 Objective-C 这个语言。而 Objective-C 与 C、Java 之类的语言风格差异有些大,短时间并不容易掌握,又或者许多人只是想尝试开发一个可以运行在 iOS 上的程序而已,不想再去额外去学一门新的语言,那这个时候就可以考虑另外的一些技术来开发 iOS App。

在移动开发领域,得益于现在手机的性能越来越好,浏览器的功能越来越强大,JavaScript 也成为一门很流行的程序开发语言,而对应到 iOS 平台上,也有多种使用 JavaScript 来开发 iOS App 的技术。这次要介绍的三种技术之中,就有两种使用了 JavaScript。

另外,许多开发跨平台移动应用开发的解决方案,首选要支持的就是 iOS,因此在 iOS 这个平台上可以看到很多种不使用 Objective-C 去开发 iOS App 的技术,而且其中技术所编写出来的程序,除了可以在 iOS 上运行之外,还可以运行在 Android 以及其他系统平台上。能少写一份代码,何乐而不为呢~

不使用 Objective-C

这次要介绍的是三个不使用 Objective-C 来开发 iOS App 的解决方案:

这三个解决方案各有特点,它们的运行机制也有所不同,至于在开发 iOS App 时选用何种方案,可以根据每种方案不同的特点去选择适合自己的。

Titanium Mobile

Titanium Mobile 是 AppCelerator 推出的一个基于 JavaScript 的跨平台移动应用开发技术。通过 Titanium Mobile,可以直接使用 JavaScript 开发能运行于 iOS 和 Android 平台的应用程序,而代码只需要写一次。

Titanium Mobile 是通过将 JavaScript 代码映射到对应平台的 Native Code,在 Titanium Mobile 中操作某一个 UI 对象,或者是其他对象时,实时上是在操作对应平台中实际的 UI 对象,例如使用 Ti.UI.createView() 创建一个视图,它在 iOS 中运行时会实际对应到 UIView,使用 Ti.UI.createTabGroup() 就会对应到 iOS 中的 UITabBarController。这样做的好处就是,可以直接使用 JavaScript 来创建出原生的 UI,而不需要使用额外的代码来让程序 UI 更像系统的 UI。

另外,AppCelerator 在收购了 Apatna 之后,推出了适用于 Titanium 的 IDE Titanium Studio,通过使用 Titanium Studio,可以很方便的创建项目、带智能提示的编辑器,以及很方便的调试项目。

在 Titanium Studio 中调试 iOS 项目时,可以做到单步调试,也就意味着,可以拥有不弱于使用 Xcode 开发项目的体验。断点、单步、变量查看功能一应具全。如果需要发布到 App Store,也可以很方便的通过图形化界面来打包。

当然,使用 Titanium Studio 也需要配合 Xcode 来使用,必须要在安装了 Xcode 的情况下,才可以使用 Titanium Studio 来调试和测试项目。

在 Titanium Studio 推出之前,Titanium Mobile 是靠一个 Developer Tool 来进行打包等操作的,相对比较繁琐,而有了 Titanium Studio 之后,让一切变得简单,我也是从那时开始关注 Titanium Mobile。

对于前端开发工程师来说,只需要理解了 iOS 开发中的一些概念,就可以使用 Titanium Mobile 来开发一个像模像样的 iOS App。并且 Titanium Mobile 实现了 CommonJS 规范,可以很方便来模块化程序代码。

PhoneGap

PhoneGap 是一个使用 HTML+CSS+JavaScript 来开发移动 App 的解决方案,使用它来开发 App 只需要有 Web 开发基础即可。它在今年10月份被 Adobe 收购了,然后加入到 Apache Software Foundation 进行孵化。

目前 PhoneGap 已经支持了市面上大多数的智能手机平台,其中就有 iOS。

在我看来,PhoneGap 其实只是提供了一个运行于各种智能手机平台的浏览器的壳,通过这个壳,PhoneGap 的 JavaScript 库可以和系统进行沟通,从而实现在 Web 页面中与系统交互的功能。

因为 PhoneGap 只提供了系统功能的 API 调用,而没有提供任何和界面相关的 API,那么界面就只能自己来折腾了。

幸好开源的世界是强大的,已经有了一堆这样的界面库来供我们选择,像 jQTouchjQuery MobileSencha Touch 之类,都提供了类似于 iOS 的界面组件,可以让我们省去许多界面上的编码工作。

使用浏览器来运行 App 的一个坏处就是:慢。因为整个 App 就是一个网页,如果编码不当,整个程序使用起来体验会比较差,和原生的应用会有很大区别。

在上面提到的几个 UI 框架之中,我比较喜欢的是 jQTouch,它只提供了基本 UI 框架和一些视图切换效果,做出来的程序是一个单页面 App,在整个程序的反应速度上会感觉比较好。不过 jQTouch 也缺少一些常用的组件像对话框,而这些在 jQuery Mobile 和 Sencha Touch 之中又有提供。选择使用哪个框架就要看个人喜好了,它们的网站都提供了在线预览功能

其实,如果用不到系统的 API,只是想把自己的程序包装成一个很像 App 的 App 的话,只要用一用 PhoneGap 提供的壳就行了,它的 API 几乎可以不用看,配合一些 UI 框架可以快速的产出一个 iOS App 来。

当然了,PhoneGap 最大的好处就是,几乎跨了几乎所有的主流智能手机平台,这对于初创团队来说,是迅速推出各个平台客户端的一个好方法。另外 PhoneGap 官方网站也提供了一系列配套服务,例如使用 PhoneGap Build 可以直接编译各个平台的程序安装包,而不需要开发者自己在本地配置每个平台的编译环境。

Mono Touch

Mono Touch 是一个使用 C# 来编写跨平台应用的框架,同样支持了 iOS 和 Android 两大平台。

Mono Touch 对于 .NET 程序员来说应该是一个好消息,除了调试,其他的都可以在 Windows 中搞定,调试的时候可以通过在虚拟机里运行 Mac OS X 来解决。Mono Touch 因为是基于 Mono 的,也有个配套的 IDE 可以用:Mono Develop。因为都是 .NET,当然也可以用 Vistual Studio 这个更强大的工具了。

与 Titanium Mobile 和 PhoneGap 不同的是,Mono Touch 如果要编译到设备,或者发布到 App Store 的话,是需要收费的。

另外,因为 Mono Touch 也是编译成可执行代码再部署到设备上的,因此运行速度相对于 PhoneGap 所制作的应用来说,应该会快上一些。

不过 Mono Touch 要收费,并且收费还不是很便宜,可能会影响到它的普及率。

小结

在上面讲的三个解决方案之外,还有其他好多大大小小的移动应用开发解决方案,但是用于 iOS 上的解决方案,主要也就是映射代码、或者是包装的形式了,对于应用而不是游戏类型来说,一般都是够用的,在具体选择的时候,也可以根据自己更为熟悉哪种语言,或者框架来挑选。

如果你发现了其他有意思的移动应用开发技术,也可以和我分享一下 :)

参考资料

  1. http://www.appcelerator.com/products/titanium-mobile-application-development
  2. http://developer.appcelerator.com/apidoc/mobile/latest
  3. http://phonegap.com/about
  4. https://build.phonegap.com
  5. http://www.mono-project.com/Main_Page
  6. http://xamarin.com/monotouch
  7. https://github.com/xamarin/monotouch-samples
  8. http://blog.zhaojie.me/2010/09/develop-ios-app-with-monotouch-in-visual-studio-1.html

— EOF —

使用 node.js + nginx 建设网站

昨天搞定了一个小网站的搭建,用了 node.js,另外为了能在一个 VPS 上搭建多个网站,用了 nginx 作为反向代理。

软件介绍

嗯,从维基上复制了一下~

node.js

Node.js是一个事件驱动I/O伺服端JavaScript环境,基于V8。目的是为了提供撰写可扩充网络程式,如web服务。第一个版本由Ryan Dahl于2009年释出,后来,Joyent雇用了Dahl,并协助发展Node.js。

nginx

nginx(发音同engine x)是一款由俄罗斯程序员Igor Sysoev所开发轻量级的网页服务器、反向代理服务器以及电子邮件(IMAP/POP3)代理服务器。

cluster

在 node.js 0.6.0 之前,有一个第三方的 node.js 模块 cluster,用来进行多核服务器上运行 node.js,以及提供扩展的支持。但是在 node.js 0.6.0 之后,node.js 本身就提供了 cluster 的支持,另外,第三方的 cluster 也与 node.js 0.6 有兼容性问题。目前 node.js 的稳定版本是 0.6.5,因此需要使用原生的 cluster 来代替第三方的 cluster。

幸好内置的 cluster 也足够简单,如果只是为了多核负载均衡,以及支持即时服务重启的话,只需要写一点的代码就可以完成这些功能了。

server.js

var path = require('path');
var http = require('http');
var cluster = require('cluster');

var NODE_ENV = process.env.NODE_ENV || 'production';
var appName = path.basename(__dirname);
var appPort = 9000;

var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    cluster.on('death', function(worker) {
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        cluster.fork();
    });

} else {
    process.title = appName + ' worker ' + process.env.NODE_WORKER_ID;
    console.log(process.title, '#' + process.pid, 'started');

    process.on('SIGHUP', function() {
        // 接收到 SIGHUP 信号时,关闭 worker
        process.exit(0);
    });

    http.Server(function(req, res) {
        res.writeHead(200);
        res.end('Worker ' + process.env.NODE_WORKER_ID);
    }).listen(8000);
}

运行服务 node server.js

nodejs master started
nodejs worker 1 #38928 started
nodejs worker 3 #38930 started
nodejs worker 2 #38929 started
nodejs worker 4 #38931 started

如果直接 kill 掉某一个 worker,kill 38928

nodejs worker #38931 died
nodejs worker 5 #38934 started

可以看到一个新的 worker 会马上启动,这就保证了服务的不间断性。

Virtual Host 支持

通常情况下,我们不会在一个 IP 上只部署一个网站。在使用 node.js 时,可以使用 connect 提供的 vhost 支持 Virtual Host,但是,这也限制了服务器只能用 node.js,而不能同时使用其他的服务,例如再安装一个 PHP 服务之类。

这时就可以使用 nginx 的反向代理来解决了,用户在访问网站时,请求先到 nginx 进行处理,如果是 node.js 站点的话,将请求转发到 node.js 的服务,然后再将 node.js 服务的结果返回给用户。

在 nginx 中设置反向代理很简单,一句 proxy_pass 就可以搞定:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:9000;
    }
}

在添加了 Virtual Host 之后,就可以把一些静态资源,例如 CSS、JavaScript 之类的文件,直接交给 nginx 来处理,而不是什么请求都需要到 node.js 这一层去处理,也省去反向代理这一关的消耗。

Session 支持

使用 express 这个 node.js web framework 来创建网站时,可以配合 connect 这个中间件来实现 session 支持。

默认情况下,connect 的 session 是使用内置的内存存储来存放 session 信息,这时如果 node.js 服务一旦重启,所有的 session 信息都会丢失,这对于用户来说不是个好体验,那么我们可以用外部的存储来存放 session 信息,例如 redis。

要让 connect 使用 redis 作为 session 存储的话也是很方便的:

var express = require('express');
var RedisStore = require('connect-redis')(express);

var app = express.createServer(
    express.session({ secret: 'keyboard cat', store : new RedisStore() })
);

监测文件改动

在调试的时候,经常需要重新启动 node.js 以便修改过的文件生效,原来第三方的 cluster 有一个配置项,可以很方便的配置监测时间间隔,文件改动后自动重新启动 worker,但是原生的 cluster 就没有这个功能了,需要自己来实现。

fs 模块提供了 watch 函数,可以方便的监测文件修改,使用这个就可以来实现文件修改后自动重启 woker 功能了。

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    var workers = [];

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        var worker = cluster.fork();
        workers.push(worker.pid);
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    // 监测文件改动,如果有修改,就将所有的 worker kill 掉
    fs.watch(__dirname, function(event, filename) {
        workers.forEach(function(pid) {
            process.kill(pid);
        });
    });

    cluster.on('death', function(worker) {
        var index = workers.indexOf(worker.pid);
        if (index != -1) {
            workers.splice(index, 1);
        }
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        worker = cluster.fork();
        workers.push(worker.pid);
    });

}

这样,每次文件保存之后,node.js 都会自动重启,从而避免了每次保存文件要手动重启服务的麻烦。

当然,在使用监测文件自动重启的时候,最好加上 NODE_ENV 的判断,在 development 的时候才进行自动重启,而 production 的时候使用手动重启就够了。

小结

总的来说,使用 node.js 来构建网站还是很方便的,加上 nginx 反向代理之后,与使用 PHP 之前也没有很大的区别,又可以享受到 node.js 的高效。

嗯,就这样了,希望此文对你有所帮助。

参考资料

  1. https://zh.wikipedia.org/wiki/Node.js
  2. http://nodejs.org/docs/v0.6.5/api/cluster.html
  3. http://learnboost.github.com/cluster/
  4. http://expressjs.com/
  5. http://senchalabs.github.com/connect/
  6. http://redis.io/

— EOF —

iOS App 自定义 URL Scheme 设计

在 iOS 里,程序之间都是相互隔离,目前并没有一个有效的方式来做程序间通信,幸好 iOS 程序可以很方便的注册自己的 URL Scheme,这样就可以通过打开特定 URL 的方式来传递参数给另外一个程序。

例如在 iPad 上浏览网页,并且 iPad 已经安装了 淘宝 HD,那么就打开下面这个链接就会在淘宝 HD 中查看这个商品的详细信息,也可以方便的使用淘宝 HD 进行购买、收藏等操作。

在淘宝 HD 中查看商品“2012西藏卓明谷方舟登舰卡(船票)”

当然,如果你在 Mac OS X 中打开这个链接,或者在没有安装 淘宝 HD 的 iPad 中打开这个链接,会提示没有程序来打开这个链接。

配置

要为 iOS 程序添加自定义协议的支持是一件很方便的事,只需要在程序的 Info.plist 添加一个 URL types 节点就可以了。在这个节点里,可以设置这个程序所支持的自定义协议名称,像 http、ftp 这种,一般我们可以设置为程序英文名称,像淘宝客户端中就设置了 taobao,这样 taobao:// 这个形式的 URL 就会关联到淘宝客户端的 App。

Info.plist config

实现

在 Info.plist 里面设置完 URL types 之后,就可以在程序中处理这类 URL 的打开请求了。

在外部程序中,如果打开了指定自定义协议的 URL,程序中 application delegate 的 application:handleOpenURL: 方法就会被调用,在这个方法里,可以获取到触发这个方法的 URL,可以通过对这个 URL 进行判断,例如根据不同的 Host,不同的 Query String 来执行不同的动作。

- (void)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    NSLog(@"%@", [url absoluteString]);

    // 在 host 等于 item.taobao.com 时,说明一个宝贝详情的 url,
    // 那么就使用本地的 TBItemDetailViewController 来显示
    if ([[url host] isEqualToString:@"item.taobao.com"]) {

        // 这里只是简单地假设 url 形式为 taobao://item.taobao.com/item.htm?id=12345678
        // 先获取要查看的宝贝详情的 itemId
        NSString *itemId = [[url query] substringFromIndex:[[url query] rangeOfString:@"id="].location+3];

        // 使用本地 ViewController 来显示淘宝商品详情
        TBItemDetailViewController *controller = [[TBItemDetailViewController alloc] initWithItemId:itemId];
        [self.navigationController pushViewController:controller animated:YES];
        [controller release];
    }
}

淘宝 for iOS

现在,淘宝 和 淘宝 HD 两个客户端都支持 taobao:// 协议,来打开特定的链接。目前已经支持的有:

例如,想要在自己的程序中,使用淘宝客户端来显示一个淘宝商品的详情,以支持用户可以直接在 iPhone 上购买,收藏等,就可以使用下面的代码:

- (void)showItemInTaobao4iOS:(NSString *)itemId {
    // 构建淘宝客户端协议的 URL
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"taobao://item.taobao.com/item.htm?id=%@", itemId]];

    // 判断当前系统是否有安装淘宝客户端
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        // 如果已经安装淘宝客户端,就使用客户端打开链接
        [[UIApplication sharedApplication] openURL:url];
    } else {
        // 否则使用 Mobile Safari 或者内嵌 WebView 来显示
        url = [NSURL URLWithString:[NSString stringWithFormat:@"http://item.taobao.com/item.htm?id=%@", itemId]];
        [[UIApplication sharedApplication] openURL:url];
    }
}

使用淘宝客户端来打开淘宝链接的好处就是可以让用户更加方便的去购买商品,而不需要再重新登录,或者把用户名密码给了第三方的网站而导致安全隐患。

扩展性

在淘宝客户端中,支持的 URL 往往是淘宝网站已经有的链接,这些链接的 QueryString 中所带的参数往往已经满足了使用本地代码显示内容的需要,但是为了扩展性考虑,就需要添加一些额外的参数,并且与原有 QueryString 中不冲突的参数名称。通过这些额外的参数,再实现客户端打开链接时更多的自定义行为。

例如,如果在打开特定 URL,进行一些操作后需要再返回原来的程序,就会需要在 URL 中添加类似于 callback 这样的参数,这样在客户端处理完用户的操作后,可以将用户操作的结果返回给原来的程序,从而实现程序间的通信。

示例:

- (void)buyItemInTaobao4iOS:(NSString *)itemId {
    // 构建淘宝客户端协议的 URL
    NSString *format = @"taobao://item.taobao.com/item.htm?id=%@&_action=buy&_callback=myapp://taobaobuysuccess";
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:format, itemId]];

    // 使用淘宝客户端打开链接
    [[UIApplication sharedApplication] openURL:url];
}

注意:当前淘宝客户端并不支持这样的调用方式,这里仅是一个示例。

当然,在使用这种方式实现程序间通信的时候,需要考虑检查一下来源 URL 的合法性,防止一些非法的调用造成用户的损失。

结语

通过自定义协议地支持,可以将 iOS 程序的一些功能和服务提供给外部程序,也可以实现 Web 和本地应用之间的互相调用。

如果你的 iOS 程序有这些需求的话,那么就可以考虑在 iOS 程序中添加自定义协议的支持了。

参考资料

  1. iOS Application Programming Guide: Implementing Custom URL Schemes

— EOF —