1. 程式人生 > >WWDC2014之App Extensions學習筆記

WWDC2014之App Extensions學習筆記

一、關於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. // Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request.
  3. @property (nonatomic,readonly,retain) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0); 
  4. @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. - (void)hiddeTodayExtension 
  6.     [[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.     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.     NSString *value = _textField.text; 
  6.     BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err]; 
  7.     if (!result) { 
  8.         NSLog(@"%@",err); 
  9.     } else { 
  10.         NSLog(@"save value:%@ success.",value); 
  11.     } 
  12.     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.     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.     BOOL removeResult = [manager removeItemAtPath:desPath error:&err]; 
  8.     if (!removeResult) { 
  9.         NSLog(@"%@",err); 
  10.     } else { 
  11.         NSLog(@"remove success."); 
  12.     } 
  13.     BOOL copyResult = [[NSFileManager defaultManager] copyItemAtPath:sorPath toPath:desPath error:&err]; 
  14.     if (!copyResult) { 
  15.         NSLog(@"%@",err); 
  16.     } else { 
  17.         NSLog(@"copy success."); 
  18.     } 
  19.     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.     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.     NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]); 
  7.     //列印documents
  8.     NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
  9.     NSString *path = [paths objectAtIndex:0]; 
  10.     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上。 文中可能有理解有誤的地方,還請指出。 參考文件