tmyam's blog

time run off...

Mac App的开机自启动

前言

在出现沙盒以前,mac下设置开机自启动是很容易的,使用 LSSharedFileListRef 很容易做到开机自启动。但自从沙盒出现之后,就变得麻烦了点,这里进行一下详细的使用描述。

非沙盒下的开机自启动

简单的说只需要两个函数就可以了。而且这种开机自启动和app所在的文件夹无关,并且可以在"系统偏好设置"->“用户与群组”->“登录项"里面看到,并进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
+ (BOOL) isStartAtLogin
{
    BOOL  isHaveAdd = NO;
    NSString* appPath = [[NSBundle mainBundle] bundlePath];
    LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
    UInt32 seedValue = 0;
    NSArray* loginItemsArray = (NSArray*)LSSharedFileListCopySnapshot(loginItems, &seedValue);
    CFURLRef tempUrl = (CFURLRef)[NSURL fileURLWithPath:appPath];
    for(NSInteger i = 0 ; i< [loginItemsArray count]; i ++ )
    {
        LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)[loginItemsArray objectAtIndex:i];
        if (LSSharedFileListItemResolve(itemRef, 0,&tempUrl, NULL) == noErr)
        {
            NSString * urlPath = [(NSURL*)tempUrl path];
            if ([urlPath compare:appPath] == NSOrderedSame)
            {
                isHaveAdd = YES;
                break;
            }
        }
    }
    [loginItemsArray release];
    CFRelease(loginItems);
    return isHaveAdd;
}

+ (BOOL) setStartAtLogin:(BOOL)startAtLogin
{
    NSString* appPath = [[NSBundle mainBundle] bundlePath];
    BOOL result = NO;
    if (startAtLogin)
    {
        if (![self isStartAtLogin])
        {
            CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:appPath];
            LSSharedFileListRef newloginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
            LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(newloginItems, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL);
            if (item)
            {
                result = YES;
                CFRelease(item);
            }
            if (newloginItems)
            {
                CFRelease(newloginItems);
            }
        }
    }
    else
    {
        LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL,kLSSharedFileListSessionLoginItems, NULL);
        UInt32 seedValue = 0;
        NSArray* loginItemsArray = (NSArray*)LSSharedFileListCopySnapshot(loginItems, &seedValue);
        CFURLRef tempUrl = (CFURLRef)[NSURL fileURLWithPath:appPath];
        for(NSInteger i = 0 ; i < [loginItemsArray count]; i ++ )
        {
            LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)[loginItemsArray objectAtIndex:i];
            if (LSSharedFileListItemResolve(itemRef, 0,&tempUrl, NULL) == noErr)
            {
                NSString * urlPath = [(NSURL*)tempUrl path];
                if ([urlPath compare:appPath] == NSOrderedSame)
                {
                    OSStatus status = LSSharedFileListItemRemove(loginItems,itemRef);
                    result = (status == noErr);
                }
            }
        }
        [loginItemsArray release];
        CFRelease(loginItems);
    }
    return result;
}

沙盒下的开机自启动

沙盒下的自启动会比较麻烦,步骤比较多。这里以程序 test 为例,进行详细阐述。

1. 添加Helper程序

沙盒下,app是无法自己做到开机自启动的,需要另外一个app协助才行。这里就创建这样一个app,为了便于区分起名testHelper。 temp

2. 设置Helper

test和testHelper都需要设置,这里先进行testHelper的设置.

  • 修改build Configuration 为 Release 。 temp
  • 修改Info.plist文件,使app在运行时不显示到前端。 temp
  • 删除MainMenu.xib中的window窗口,
    并且删除 AppDelegate.h 中的@property (assign) IBOutlet NSWindow *window;
    这些都是确保app在运行时不显示到前端。 temp
  • 修改 Build Settings 的 Skip Install 为 YES。 temp
  • 设置沙盒环境,证书,描述文件。
    这里设置的发布证书,如果需要测试,可以设置开发者证书进行测试。 temp

3. 添加Helper的代码

参照下面的函数,复制到testHelper的AppDelegate中,注意修改identifier和appName为自己主app的信息。这段代码的意义是启动主app。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    [self runMainApp:@"com.tmyam.test" appName:@"test"];
}

/** 运行开机自启动的app

 @param identifier 主app的标识符
 @param appName 主app的名称(在MacOS文件夹下的名称)
 */
- (void) runMainApp:(NSString*)identifier appName:(NSString*)appName
{
    // Check if main app is already running; if yes, do nothing and terminate helper app
    BOOL alreadyRunning = NO;
    NSArray *running = [[NSWorkspace sharedWorkspace] runningApplications];
    for (NSRunningApplication *app in running)
    {
        if ([[app bundleIdentifier] isEqualToString:identifier])
        {
            alreadyRunning = YES;
        }
    }
    if (!alreadyRunning)
    {
        NSString *path = [[NSBundle mainBundle] bundlePath];
        NSArray *p = [path pathComponents];
        NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:p];
        [pathComponents removeLastObject];
        [pathComponents removeLastObject];
        [pathComponents removeLastObject];
        [pathComponents addObject:@"MacOS"];
        [pathComponents addObject:appName];
        NSString *newPath = [NSString pathWithComponents:pathComponents];
        [[NSWorkspace sharedWorkspace] launchApplication:newPath];
    }
    [NSApp terminate:nil];
}

4. 设置主程序

  • test添加Copy Files 。 temp
  • 设置 Copy Files 。Destination为"Wrapper",Subpath为"Contents/Library/LoginItems",添加"testHelper.app"如下图。
    temp
  • 设置依赖,如下图。
    temp
  • 设置 Build Settings -> “Strip Debug Symbols During Copy” 为 NO 。 temp
  • 设置沙盒环境,证书,描述文件。
    这里设置的发布证书,如果需要测试,可以设置开发者证书进行测试。 temp

5. 主程序添加代码

  • test首先导入两个文件,StartAtLoginController.h和StartAtLoginController.m,StartAtLoginController下载

  • 添加 ServiceManagement.framework temp

  • 创建一个管理文件例如 TMStartAtLogin,负责管理开机自启动状态,并在里面粘贴以下代码。
    别忘了 #import “StartAtLoginController.h”
    这里的 Identifier是testHelper的标识符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (BOOL) isStartAtLogin
{
    StartAtLoginController* loginController = [[StartAtLoginController alloc]initWithIdentifier:@"com.tmyam.testHelper"];
    BOOL startedAtLogin = [loginController startAtLogin];
    [loginController release];
    return startedAtLogin;
}

+ (BOOL) setStartAtLogin:(BOOL)isStartLogin
{
    StartAtLoginController* loginController = [[StartAtLoginController alloc]initWithIdentifier:@"com.tmyam.testHelper"];
    loginController.startAtLogin = isStartLogin;
    BOOL result = loginController.enabled;
    [loginController release];
    return result;
}

结尾

至此,所有的设置已经完成,可以通过主程序的 TMStartAtLogin 管理自启动的状态。 还有要注意的地方,如果要测试沙盒下的开机自启动,需要将编译出的app放入系统的应用程序目录下,否则不会生效。

另外,在发布app时,会遇到因为使用了testHelper证书而导致上传到app store时的错误,这时需要深入到“…/Products/Applications/test.app/Contents/Library/LoginItems/testHelper.app/Contents”目录下,删除embedded.provisionprofile文件,就可以正确上传了。

示例

StartAtLoginController下载
test示例下载

参考

http://blog.timschroeder.net/2012/07/03/the-launch-at-login-sandbox-project/ http://stackoverflow.com/questions/11230168/submit-an-osx-app-with-helper-app-in-its-bundle

评论