App Extensions學習筆記

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

一、關於App Extensions
 
extension是iOS8新開放的一種對幾個固定系統區域的擴展機制,它可以在一定程度上彌補iOS的沙盒機制對應用間通信的限制。
 
extension的出現,爲用戶提供了在其它應用中使用我們應用提供的服務的便捷方式,比如用戶可以在Today的widgets中查看應用展示的簡略信息,而不用再進到我們的應用中,這將是一種全新的用戶體驗;但是,extension的出現可能會減少用戶啓動應用的次數,同時還會增大開發者的工作量。
 
幾個關鍵詞
 
extension point
系統中支持extension的區域,extension的類別也是據此區分的,iOS上共有Today、Share、Action、Photo Editing、Storage Provider、Custom 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可以在選擇文件時出現;Share和Action可以在任何應用裏被激活,但前提是開發者需要設置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. @interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling> 
  2.  
  3. // Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request. 
  4. @property (nonatomic,readonly,retain) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0); 
  5.  
  6. @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. //通過openURL的方式啓動Containing APP 
  2. - (void)openURLContainingAPP 
  3.     [self.extensionContext openURL:[NSURL URLWithString:@"appextension://123"
  4.                  completionHandler:^(BOOL success) { 
  5.                      NSLog(@"open url result:%d",success); 
  6.                  }]; 
extension中是無法直接使用openURL的。
 
可以共享Shared resources
 
extension和containing app可以共同讀寫一個被稱爲Shared resources的存儲區域,這是通過App Groups實現的,後文將會詳述。
 
三者間的關係可以通過官網給的兩張圖片形象地說明:
 
containing app能夠控制extension的出現和隱藏
通過以下代碼,containing app可以讓extension出現或隱藏(當然extension也可以讓自己隱藏):
  1. //讓隱藏的插件重新顯示 
  2. - (void)showTodayExtension 
  3.     [[NCWidgetController widgetController] setHasContent:YES forWidgetWithBundleIdentifier:@"com.wangzz.app.extension"]; 
  4.  
  5. //隱藏插件 
  6. - (void)hiddeTodayExtension 
  7.     [[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。
 
添加成功以後如下圖所示:
在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. - (void)saveTextByNSUserDefaults 
  2.     NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"]; 
  3.     [shared setObject:_textField.text forKey:@"wangzz"]; 
  4.     [shared synchronize]; 
需要注意的是:
 
1.保存數據的時候必須指明group id;
 
2.而且要注意NSUserDefaults能夠處理的數據只能是可plist化的對象,詳情見Property List Programming Guide。
 
3.爲了防止出現數據同步問題,不要忘記調用[shared synchronize];
 
讀數據
對應的讀取數據方式:
  1. - (NSString *)readDataFromNSUserDefaults 
  2.     NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.wangzz"]; 
  3.     NSString *value = [shared valueForKey:@"wangzz"]; 
  4.  
  5.     return value; 
 
4.2 通過NSFileManager共享數據
 
NSFileManager在iOS7提供了containerURLForSecurityApplicationGroupIdentifier方法,可以用來實現app group共享數據。
 
保存數據
  1. - (BOOL)saveTextByNSFileManager 
  2.     NSError *err = nil; 
  3.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  4.     containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"]; 
  5.  
  6.     NSString *value = _textField.text; 
  7.     BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err]; 
  8.     if (!result) { 
  9.         NSLog(@"%@",err); 
  10.     } else { 
  11.         NSLog(@"save value:%@ success.",value); 
  12.     } 
  13.  
  14.     return result; 
 
讀數據
  1. - (NSString *)readTextByNSFileManager 
  2.     NSError *err = nil; 
  3.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  4.     containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"]; 
  5.     NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&err]; 
  6.  
  7.     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. - (BOOL)copyFrameworkFromMainBundleToAppGroup 
  2.     NSFileManager *manager = [NSFileManager defaultManager]; 
  3.     NSError *err = nil; 
  4.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  5.     NSString *sorPath = [NSString stringWithFormat:@"%@/Dylib.framework",[[NSBundle mainBundle] bundlePath]]; 
  6.     NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path]; 
  7.  
  8.     BOOL removeResult = [manager removeItemAtPath:desPath error:&err]; 
  9.     if (!removeResult) { 
  10.         NSLog(@"%@",err); 
  11.     } else { 
  12.         NSLog(@"remove success."); 
  13.     } 
  14.  
  15.     BOOL copyResult = [[NSFileManager defaultManager] copyItemAtPath:sorPath toPath:desPath error:&err]; 
  16.     if (!copyResult) { 
  17.         NSLog(@"%@",err); 
  18.     } else { 
  19.         NSLog(@"copy success."); 
  20.     } 
  21.  
  22.     return copyResult; 
 
5.2 使用framework:
  1. - (BOOL)loadFrameworkInAppGroup 
  2.     NSError *err = nil; 
  3.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  4.     NSString *desPath = [NSString stringWithFormat:@"%@/Library/Caches/Dylib.framework",containerURL.path]; 
  5.     NSBundle *bundle = [NSBundle bundleWithPath:desPath]; 
  6.     BOOL result = [bundle loadAndReturnError:&err]; 
  7.     if (result) { 
  8.         Class root = NSClassFromString(@"Person"); 
  9.         if (root) { 
  10.             Person *person = [[root alloc] init]; 
  11.             if (person) { 
  12.                 [person run]; 
  13.             } 
  14.         } 
  15.     } else { 
  16.         NSLog(@"%@",err); 
  17.     } 
  18.  
  19.     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. - (void)logAppPath 
  2.     //app group路徑 
  3.     NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"]; 
  4.     NSLog(@"app group:\n%@",containerURL.path); 
  5.  
  6.     //打印可執行文件路徑 
  7.     NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]); 
  8.  
  9.     //打印documents 
  10.     NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
  11.     NSString *path = [paths objectAtIndex:0]; 
  12.     NSLog(@"documents:\n%@",path); 
 
containing app執行結果
  1. 2014-06-23 19:35:03.944 AppExtensionDemo[7471:365131] app group: 
  2. /private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816 
  3. 2014-06-23 19:35:03.946 AppExtensionDemo[7471:365131] bundle: 
  4. /private/var/mobile/Containers/Bundle/Application/1AC73797-A3BB-4BDE-A647-3D083DA6871A/AppExtensionDemo.app 
  5. 2014-06-23 19:35:03.948 AppExtensionDemo[7471:365131] documents: 
  6. /var/mobile/Containers/Data/Application/E5E6E516-0163-4754-9D10-A5F6C33A6261/Documents 
 
extension執行結果
  1. Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: app group: 
  2.   /private/var/mobile/Containers/Shared/AppGroup/89CCBFB1-CA5E-4C7F-80CB-A3EB9E841816 
  3. Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: bundle: 
  4.   /private/var/mobile/Containers/Bundle/Application/596717B7-7CB8-4F53-BCD4-380F34ABD30F/AppExtensionDemo.app/PlugIns/com.foogry.AppExtensionDemo.TodayExtension.appex 
  5. Jun 23 19:37:49 autonavis-iPad com.foogry.AppExtensionDemo.TodayExtension[7638] <Warning>: documents: 
  6.   /var/mobile/Containers/Data/PluginKitPlugin/57581433-3DBD-4930-971F-78D30C150E8A/Documents 
 
由此可見,不管是extension還是containing app,他們的可執行文件和保存數據的目錄都是分開存放的,即所有app的可執行文件都放在一個大目錄下,保存數據的目錄保存在另一個大目錄下,同樣,AppGroup放在另一個大目錄下。
 
說明
 
本文用到的demo已經上傳到github上。
 
文中可能有理解有誤的地方,還請指出。
 
參考文檔
 
 
 
 
 
 
 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章