WWDC2014之App Extensions學習筆記

好文,原博文:http://foggry.com/blog/2014/06/23/wwdc2014zhi-app-extensionsxue-xi-bi-ji/

一、關於App Extensions

extension是iOS8新開放的一種對幾個固定系統區域的擴展機制,它可以在一定程度上彌補iOS的沙盒機制對應用間通信的限制。

extension的出現,爲用戶提供了在其它應用中使用我們應用提供的服務的便捷方式,比如用戶可以在Todaywidgets中查看應用展示的簡略信息,而不用再進到我們的應用中,這將是一種全新的用戶體驗;但是,extension的出現可能會減少用戶啓動應用的次數,同時還會增大開發者的工作量。

幾個關鍵詞

  • extension point

系統中支持extension的區域,extension的類別也是據此區分的,iOS上共有TodayShareActionPhoto EditingStorage ProviderCustom keyboard幾種,其中Today中的extension又被稱爲widget

每種extension point的使用方式和適合乾的活都不一樣,因此不存在通用的extension。

  • app extension

即爲本文所說的extension。extension並不是一個獨立的app,它有一個包含在app bundle中的獨立bundle,extension的bundle後綴名是.appex。其生命週期也和普通app不同,這些後文將會詳述。

extension不能單獨存在,必須有一個包含它的containing app。

另外,extension需要用戶手動激活,不同的extension激活方式也不同,比如: 比如Today中的widget需要在Today中激活和關閉;Custom keyboard需要在設置中進行相關設置;Photo Editing需要在使用照片時在照片管理器中激活或關閉;Storage Provider可以在選擇文件時出現;ShareAction可以在任何應用裏被激活,但前提是開發者需要設置Activation Rules,以確定extension需要在合適出現。

  • containing app

儘管蘋果開放了extension,但是在iOS中extension並不能單獨存在,要想提交到AppStore,必須將extension包含在一個app中提交,並且app的實現部分不能爲空,這個包含extension的app就叫containing app。

extension會隨着containing app的安裝而安裝,同時隨着containing app的卸載而卸載。

  • host app

能夠調起extension的app被稱爲host app,比如widget的host app就是Today

二、extension和containing app、host app

2.1 extension和host app

extension和host app之間可以通過extensionContext屬性直接通信,該屬性是新增加的UIViewController類別:

1
2
3
4
5
6
@interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling>

// Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request.
@property (nonatomic,readonly,retain) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0);

@end

實際上extension和host app之間是通過IPC(interprocess communication)實現的,只是蘋果把調用接口高度抽象了,我們並不需要關注那麼底層的東西。

2.2 containing app和host app

他們之間沒有任何直接關係,也從來不需要通信。

2.3 extension和containing app

這二者之間的關係最複雜,糾糾纏纏扯不清關係。

  • 不能直接通信

首先,儘管extension的bundle是放在containing app的bundle中,但是他們是兩個完全獨立的進程,之間不能直接通信。不過extension可以通過openURL的方式啓動containing app(當然也能啓動其它app),不過必須通過extensionContext藉助host app來實現:

1
2
3
4
5
6
7
8
//通過openURL的方式啓動Containing APP
- (void)openURLContainingAPP
{
    [self.extensionContext openURL:[NSURL URLWithString:@"appextension://123"]
                 completionHandler:^(BOOL success) {
                     NSLog(@"open url result:%d",success);
                 }];
}

extension中是無法直接使用openURL的。

  • 可以共享Shared resources

extension和containing app可以共同讀寫一個被稱爲Shared resources的存儲區域,這是通過App Groups實現的,後文將會詳述。

三者間的關係可以通過官網給的兩張圖片形象地說明:

detailed_communication

app_extensions_container_restrictions

  • containing app能夠控制extension的出現和隱藏

通過以下代碼,containing app可以讓extension出現或隱藏(當然extension也可以讓自己隱藏):

1
2
3
4
5
6
7
8
9
10
11
//讓隱藏的插件重新顯示
- (void)showTodayExtension
{
    [[NCWidgetController widgetController] setHasContent:YES forWidgetWithBundleIdentifier:@"com.wangzz.app.extension"];
}

//隱藏插件
- (void)hiddeTodayExtension
{
    [[NCWidgetController widgetController] setHasContent:NO forWidgetWithBundleIdentifier:@"com.wangzz.app.extension"];
}

三、App Groups

這是iOS8新開放的功能,在OS X上早就可用了。它主要用於同一group下的app共享同一份讀寫空間,以實現數據共享。

extension和containing app共同讀寫一份數據是很合理的需求,比如系統的股市應用,widget和app中都需要展示幾個公司的股票數據,這就可以通過App Groups實現。

3.1 功能開啓

爲了便於後續操作,請先確保你的開發者賬號在Xcode上處於登錄狀態。

  • 在app中開啓

App Groups位於:

1
TARGETS-->AppExtensionDemo-->Capabilities-->App Groups

找到以後,將App Groups右上角的開關打開,然後選擇添加groups,比如我的是group.wangzz,當然這是爲了測試隨便起得名字,正規點得命名規則應該是:group.com.company.app。

添加成功以後如下圖所示:

app_group

  • 在extension中開啓

我創建的是widget,target名稱爲TodayExtension,對應的App Groups位於:

1
TARGETS-->TodayExtension-->Capabilities-->App Groups

開啓方式和app中一樣,需要注意的是必須保證這裏地App Groups名稱和app中的相同,即爲group.wangzz。

四、extension和containing app數據共享

App Groups給我們提供了同一group內app可以共同讀寫的區域,可以通過以下方式實現數據共享:

4.1 通過NSUserDefaults共享數據

  • 存數據

通過以下方式向NSUserDefaults中保存數據:

1
2
3
4
5
6
7
- (void)saveTextByNSUserDefaults
{
    NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"];
    [shared setObject:_textField.text forKey:@"wangzz"];
    [shared synchronize];
}

需要注意的是:

1.保存數據的時候必須指明group id;

2.而且要注意NSUserDefaults能夠處理的數據只能是可plist化的對象,詳情見Property List Programming Guide

3.爲了防止出現數據同步問題,不要忘記調用[shared synchronize];

  • 讀數據

對應的讀取數據方式:

1
2
3
4
5
6
7
- (NSString *)readDataFromNSUserDefaults
{
    NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"];
    NSString *value = [shared valueForKey:@"wangzz"];

    return value;
}

4.2 通過NSFileManager共享數據

NSFileManager在iOS7提供了containerURLForSecurityApplicationGroupIdentifier方法,可以用來實現app group共享數據。

  • 保存數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)saveTextByNSFileManager
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];

    NSString *value = _textField.text;
    BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
    if (!result) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"save value:%@ success.",value);
    }

    return result;
}
  • 讀數據
1
2
3
4
5
6
7
8
9
- (NSString *)readTextByNSFileManager
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
    NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&err];

    return value;
}

在這裏我試着保存和讀取的是字符串數據,但讀寫SQlite我相信也是沒問題的。

  • 數據同步

兩個應用共同讀取同一份數據,就會引發數據同步問題。WWDC2014的視頻中建議使用NSFileCoordination實現普通文件的讀寫同步,而數據庫可以使用CoreData,Sqlite也支持同步。

五、extension和containing app代碼共享

和數據共享類似,extension和containing app很自然地會有一些業務邏輯上可以共用的代碼,這時可以通過iOS8中剛開放使用的framework實現。蘋果在App Extension Programming Guide中是這樣描述的:

In iOS 8.0 and later, you can use an embedded framework to share code between your extension and its containing app. For example, if you develop image-processing code that you want both your Photo Editing extension and its containing app to share, you can put the code into a framework and embed it in both targets.

即將framework分別嵌入到extension和containing app的target中實現代碼共享。但這樣豈不是需要分別要將framework分別copy到extension和containing app的main bundle中?

參考extension和containing app數據共享,我試想能不能將framework只保存一份放在App Groups區域?

5.1 copy framework到App Groups

在app首次啓動的時候將framework放到App Groups區域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)copyFrameworkFromMainBundleToAppGroup
{
    NSFileManager *manager = [NSFileManager defaultManager];
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSString *sorPath = [NSString stringWithFormat:@"%@/Dylib.framework",[[NSBundle mainBundle] bundlePath]];
    NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path];

    BOOL removeResult = [manager removeItemAtPath:desPath error:&err];
    if (!removeResult) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"remove success.");
    }

    BOOL copyResult = [[NSFileManager defaultManager] copyItemAtPath:sorPath toPath:desPath error:&err];
    if (!copyResult) {
        NSLog(@"%@",err);
    } else {
        NSLog(@"copy success.");
    }

    return copyResult;
}

5.2 使用framework:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (BOOL)loadFrameworkInAppGroup
{
    NSError *err = nil;
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path];
    NSBundle *bundle = [NSBundle bundleWithPath:desPath];
    BOOL result = [bundle loadAndReturnError:&err];
    if (result) {
        Class root = NSClassFromString(@"Person");
        if (root) {
            Person *person = [[root alloc] init];
            if (person) {
                [person run];
            }
        }
    } else {
        NSLog(@"%@",err);
    }

    return result;
}

經過測試,竟然能夠加載成功。

需要說明的是,這裏只是說那麼用是可以成功加載framework,但還面臨不少問題,比如如果用戶在啓動app之前去使用extension,這時framework還沒有copy過去,怎麼處理;另外iOS的機制或者蘋果的審覈是否允許這樣使用等。

在一切確定下來之前還是乖乖按文檔中的方式使用吧。

六、生命週期

extension和普通app的最大區別之一是生命週期。

  • 開始

在用戶通過host app點擊extension時,系統就會實例化extension應用,這是生命週期的開始。

  • 執行任務

在extension啓動以後,開始執行它的使命。

  • 終止

在用戶取消任務,或者任務執行結束,或者開啓了一個長時後臺任務時,系統會將其殺掉。

由此可見,extension就是爲了任務而生!

下圖來自官方文檔,它將生命週期劃分的更詳細:

app_extensions_lifecycle

通過打印日誌發現,Today中的widget在將Today切換到全部或者未讀通知時都會被殺掉。

七、 調試

extension和普通app的調試方式差不多,開始調試前先選中extension對應的target,點擊run,就會彈出下圖所示選擇框:

extension_debug

需要選擇一個host app,這裏選擇Today

然後即可和普通app一樣調試了,不過我在實際使用過程中,發現有各種奇怪的事情,比如NSLog無法在控制檯輸出,應該是bug吧。

八、 iOS8應用文件系統

發現iOS8的文件系統發生了變化,新的文件系統將可執行文件(即原來的.app文件)從沙盒中移到了另外一個地方,這樣感覺更合理。

  • 測試代碼

下述代碼用於打印App Groups路徑、應用的可執行文件路徑、對應的Documents路徑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)logAppPath
{
    //app group路徑
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];
    NSLog(@"app group:\n%@",containerURL.path);

    //打印可執行文件路徑
    NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]);

    //打印documents
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [paths objectAtIndex:0];
    NSLog(@"documents:\n%@",path);
}
  • containing app執行結果
1
2
3
4
5
6
2014-06-23 19:35:03.944 AppExtensionDemo[7471:365131] app group:
/private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816
2014-06-23 19:35:03.946 AppExtensionDemo[7471:365131] bundle:
/private/var/mobile/Containers/Bundle/Application/1AC73797-A3BB-4BDE-A647-3D083DA6871A/AppExtensionDemo.app
2014-06-23 19:35:03.948 AppExtensionDemo[7471:365131] documents:
/var/mobile/Containers/Data/Application/E5E6E516-0163-4754-9D10-A5F6C33A6261/Documents
  • extension執行結果
1
2
3
4
5
6
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: app group:
  /private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: bundle:
  /private/var/mobile/Containers/Bundle/Application/596717B7-7CB8-4F53-BCD4-380F34ABD30F/AppExtensionDemo.app/PlugIns/com.foogry.AppExtensionDemo.TodayExtension.appex
Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: documents:
  /var/mobile/Containers/Data/PluginKitPlugin/57581433-3DBD-4930-971F-78D30C150E8A/Documents

由此可見,不管是extension還是containing app,他們的可執行文件和保存數據的目錄都是分開存放的,即所有app的可執行文件都放在一個大目錄下,保存數據的目錄保存在另一個大目錄下,同樣,AppGroup放在另一個大目錄下。

說明

  • 本文用到的demo已經上傳到github上。

  • 文中可能有理解有誤的地方,還請指出。

參考文檔

發佈了62 篇原創文章 · 獲贊 65 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章