一、關於App Extensions
extension是iOS8新開放的一種對幾個固定系統區域的擴展機制,它可以在一定程度上彌補iOS的沙盒機制對應用間通信的限制。
extension的出現,爲用戶提供了在其它應用中使用我們應用提供的服務的便捷方式,比如用戶可以在Today
的widgets
中查看應用展示的簡略信息,而不用再進到我們的應用中,這將是一種全新的用戶體驗;但是,extension的出現可能會減少用戶啓動應用的次數,同時還會增大開發者的工作量。
幾個關鍵詞
系統中支持extension的區域,extension的類別也是據此區分的,iOS上共有Today
、Share
、Action
、Photo
Editing
、Storage
Provider
、Custom
keyboard
幾種,其中Today
中的extension又被稱爲widget
。
每種extension point的使用方式和適合乾的活都不一樣,因此不存在通用的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
可以在選擇文件時出現;Share
和Action
可以在任何應用裏被激活,但前提是開發者需要設置Activation
Rules
,以確定extension需要在合適出現。
儘管蘋果開放了extension,但是在iOS中extension並不能單獨存在,要想提交到AppStore,必須將extension包含在一個app中提交,並且app的實現部分不能爲空,這個包含extension的app就叫containing app。
extension會隨着containing app的安裝而安裝,同時隨着containing 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的。
extension和containing app可以共同讀寫一個被稱爲Shared
resources
的存儲區域,這是通過App Groups實現的,後文將會詳述。
三者間的關係可以通過官網給的兩張圖片形象地說明:
-
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 Groups位於:
1
|
TARGETS-->AppExtensionDemo-->Capabilities-->App Groups
|
找到以後,將App Groups右上角的開關打開,然後選擇添加groups,比如我的是group.wangzz,當然這是爲了測試隨便起得名字,正規點得命名規則應該是:group.com.company.app。
添加成功以後如下圖所示:
我創建的是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就是爲了任務而生!
下圖來自官方文檔,它將生命週期劃分的更詳細:
通過打印日誌發現,Today
中的widget
在將Today
切換到全部
或者未讀通知時
都會被殺掉。
七、 調試
extension和普通app的調試方式差不多,開始調試前先選中extension對應的target,點擊run,就會彈出下圖所示選擇框:
需要選擇一個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);
}
|
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
|
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上。
-
文中可能有理解有誤的地方,還請指出。
參考文檔