一蜀铲、關(guān)于App Extensions
extension是iOS8新開放的一種對(duì)幾個(gè)固定系統(tǒng)區(qū)域的擴(kuò)展機(jī)制边琉,它可以在一定程度上彌補(bǔ)iOS的沙盒機(jī)制對(duì)應(yīng)用間通信的限制。
extension的出現(xiàn)记劝,為用戶提供了在其它應(yīng)用中使用我們應(yīng)用提供的服務(wù)的便捷方式变姨,比如用戶可以在Today的widgets中查看應(yīng)用展示的簡(jiǎn)略信息,而不用再進(jìn)到我們的應(yīng)用中厌丑,這將是一種全新的用戶體驗(yàn)定欧;但是,extension的出現(xiàn)可能會(huì)減少用戶啟動(dòng)應(yīng)用的次數(shù)怒竿,同時(shí)還會(huì)增大開發(fā)者的工作量砍鸠。
幾個(gè)關(guān)鍵詞
extension point
系統(tǒng)中支持extension的區(qū)域,extension的類別也是據(jù)此區(qū)分的愧口,iOS上共有Today睦番、Share、Action耍属、Photo Editing托嚣、Storage Provider、Custom keyboard幾種厚骗,其中Today中的extension又被稱為widget示启。
每種extension point的使用方式和適合干的活都不一樣,因此不存在通用的extension领舰。
app extension
即為本文所說的extension夫嗓。extension并不是一個(gè)獨(dú)立的app迟螺,它有一個(gè)包含在app bundle中的獨(dú)立bundle,extension的bundle后綴名是.appex舍咖。其生命周期也和普通app不同矩父,這些后文將會(huì)詳述。
extension不能單獨(dú)存在排霉,必須有一個(gè)包含它的containing app窍株。
另外,extension需要用戶手動(dòng)激活攻柠,不同的extension激活方式也不同球订,比如: 比如Today中的widget需要在Today中激活和關(guān)閉;Custom keyboard需要在設(shè)置中進(jìn)行相關(guān)設(shè)置瑰钮;Photo Editing需要在使用照片時(shí)在照片管理器中激活或關(guān)閉冒滩;Storage Provider可以在選擇文件時(shí)出現(xiàn);Share和Action可以在任何應(yīng)用里被激活浪谴,但前提是開發(fā)者需要設(shè)置Activation Rules开睡,以確定extension需要在合適出現(xiàn)。
containing app
盡管蘋果開放了extension较店,但是在iOS中extension并不能單獨(dú)存在士八,要想提交到AppStore容燕,必須將extension包含在一個(gè)app中提交梁呈,并且app的實(shí)現(xiàn)部分不能為空,這個(gè)包含extension的app就叫containing app。
extension會(huì)隨著containing app的安裝而安裝蘸秘,同時(shí)隨著containing app的卸載而卸載官卡。
host app
能夠調(diào)起extension的app被稱為host app,比如widget的host app就是Today醋虏。
二寻咒、extension和containing app、host app
2.1 extension和host app
extension和host app之間可以通過extensionContext屬性直接通信颈嚼,該屬性是新增加的UIViewController類別:
@interface?UIViewController(NSExtensionAdditions)??
//?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?
實(shí)際上extension和host app之間是通過IPC(interprocess communication)實(shí)現(xiàn)的毛秘,只是蘋果把調(diào)用接口高度抽象了,我們并不需要關(guān)注那么底層的東西阻课。
2.2 containing app和host app
他們之間沒有任何直接關(guān)系叫挟,也從來不需要通信。
2.3 extension和containing app
這二者之間的關(guān)系最復(fù)雜限煞,糾糾纏纏扯不清關(guān)系抹恳。
不能直接通信
首先,盡管extension的bundle是放在containing app的bundle中署驻,但是他們是兩個(gè)完全獨(dú)立的進(jìn)程奋献,之間不能直接通信健霹。不過extension可以通過openURL的方式啟動(dòng)containing app(當(dāng)然也能啟動(dòng)其它app),不過必須通過extensionContext借助host app來實(shí)現(xiàn):
//通過openURL的方式啟動(dòng)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可以共同讀寫一個(gè)被稱為Shared resources的存儲(chǔ)區(qū)域糖埋,這是通過App Groups實(shí)現(xiàn)的,后文將會(huì)詳述窃这。
三者間的關(guān)系可以通過官網(wǎng)給的兩張圖片形象地說明:
containing app能夠控制extension的出現(xiàn)和隱藏
通過以下代碼阶捆,containing app可以讓extension出現(xiàn)或隱藏(當(dāng)然extension也可以讓自己隱藏):
//讓隱藏的插件重新顯示?
-?(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共享同一份讀寫空間,以實(shí)現(xiàn)數(shù)據(jù)共享朴上。
extension和containing app共同讀寫一份數(shù)據(jù)是很合理的需求垒棋,比如系統(tǒng)的股市應(yīng)用,widget和app中都需要展示幾個(gè)公司的股票數(shù)據(jù)痪宰,這就可以通過App Groups實(shí)現(xiàn)叼架。
3.1 功能開啟
為了便于后續(xù)操作,請(qǐng)先確保你的開發(fā)者賬號(hào)在Xcode上處于登錄狀態(tài)衣撬。
在app中開啟
App Groups位于:
TARGETS-->AppExtensionDemo-->Capabilities-->App?Groups?
找到以后乖订,將App Groups右上角的開關(guān)打開,然后選擇添加groups具练,比如我的是group.wangzz乍构,當(dāng)然這是為了測(cè)試隨便起得名字,正規(guī)點(diǎn)得命名規(guī)則應(yīng)該是:group.com.company.app扛点。
添加成功以后如下圖所示:
在extension中開啟
我創(chuàng)建的是widget哥遮,target名稱為TodayExtension,對(duì)應(yīng)的App Groups位于:
TARGETS-->TodayExtension-->Capabilities-->App?Groups?
開啟方式和app中一樣陵究,需要注意的是必須保證這里地App Groups名稱和app中的相同眠饮,即為group.wangzz。
四铜邮、extension和containing app數(shù)據(jù)共享
App Groups給我們提供了同一group內(nèi)app可以共同讀寫的區(qū)域仪召,可以通過以下方式實(shí)現(xiàn)數(shù)據(jù)共享:
4.1 通過NSUserDefaults共享數(shù)據(jù)
存數(shù)據(jù)
通過以下方式向NSUserDefaults中保存數(shù)據(jù):
-?(void)saveTextByNSUserDefaults?
{?
NSUserDefaults?*shared?=?[[NSUserDefaults?alloc]?initWithSuiteName:@"group.wangzz"];?
[shared?setObject:_textField.text?forKey:@"wangzz"];?
????[shared?synchronize];?
}?
需要注意的是:
1.保存數(shù)據(jù)的時(shí)候必須指明group id;
2.而且要注意NSUserDefaults能夠處理的數(shù)據(jù)只能是可plist化的對(duì)象松蒜,詳情見Property List Programming Guide扔茅。
3.為了防止出現(xiàn)數(shù)據(jù)同步問題,不要忘記調(diào)用[shared synchronize];
讀數(shù)據(jù)
對(duì)應(yīng)的讀取數(shù)據(jù)方式:
-?(NSString?*)readDataFromNSUserDefaults?
{?
NSUserDefaults?*shared?=?[[NSUserDefaults?alloc]?initWithSuiteName:@"group.wangzz"];?
NSString?*value?=?[shared?valueForKey:@"wangzz"];?
return?value;?
}?
4.2 通過NSFileManager共享數(shù)據(jù)
NSFileManager在iOS7提供了containerURLForSecurityApplicationGroupIdentifier方法牍鞠,可以用來實(shí)現(xiàn)app group共享數(shù)據(jù)咖摹。
保存數(shù)據(jù)
-?(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;?
}?
讀數(shù)據(jù)
-?(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;?
}?
在這里我試著保存和讀取的是字符串?dāng)?shù)據(jù),但讀寫SQlite我相信也是沒問題的难述。
數(shù)據(jù)同步
兩個(gè)應(yīng)用共同讀取同一份數(shù)據(jù)萤晴,就會(huì)引發(fā)數(shù)據(jù)同步問題吐句。WWDC2014的視頻中建議使用NSFileCoordination實(shí)現(xiàn)普通文件的讀寫同步,而數(shù)據(jù)庫可以使用CoreData,Sqlite也支持同步店读。
五嗦枢、extension和containing app代碼共享
和數(shù)據(jù)共享類似,extension和containing app很自然地會(huì)有一些業(yè)務(wù)邏輯上可以共用的代碼屯断,這時(shí)可以通過iOS8中剛開放使用的framework實(shí)現(xiàn)文虏。蘋果在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中實(shí)現(xiàn)代碼共享。但這樣豈不是需要分別要將framework分別copy到extension和containing app的main bundle中殖演?
參考extension和containing app數(shù)據(jù)共享氧秘,我試想能不能將framework只保存一份放在App Groups區(qū)域?
5.1 copy framework到App Groups
在app首次啟動(dòng)的時(shí)候?qū)ramework放到App Groups區(qū)域:
-?(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:
-?(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;?
}?
經(jīng)過測(cè)試趴久,竟然能夠加載成功丸相。
需要說明的是,這里只是說那么用是可以成功加載framework彼棍,但還面臨不少問題灭忠,比如如果用戶在啟動(dòng)app之前去使用extension,這時(shí)framework還沒有copy過去座硕,怎么處理弛作;另外iOS的機(jī)制或者蘋果的審核是否允許這樣使用等。
在一切確定下來之前還是乖乖按文檔中的方式使用吧华匾。
六映琳、生命周期
extension和普通app的最大區(qū)別之一是生命周期。
開始
在用戶通過host app點(diǎn)擊extension時(shí)瘦真,系統(tǒng)就會(huì)實(shí)例化extension應(yīng)用刊头,這是生命周期的開始黍瞧。
執(zhí)行任務(wù)
在extension啟動(dòng)以后诸尽,開始執(zhí)行它的使命。
終止
在用戶取消任務(wù)印颤,或者任務(wù)執(zhí)行結(jié)束您机,或者開啟了一個(gè)長(zhǎng)時(shí)后臺(tái)任務(wù)時(shí),系統(tǒng)會(huì)將其殺掉年局。
由此可見际看,extension就是為了任務(wù)而生!
下圖來自官方文檔矢否,它將生命周期劃分的更詳細(xì):
通過打印日志發(fā)現(xiàn)仲闽,Today中的widget在將Today切換到全部或者未讀通知時(shí)都會(huì)被殺掉。
七僵朗、 調(diào)試
extension和普通app的調(diào)試方式差不多赖欣,開始調(diào)試前先選中extension對(duì)應(yīng)的target屑彻,點(diǎn)擊run,就會(huì)彈出下圖所示選擇框:
需要選擇一個(gè)host app顶吮,這里選擇Today社牲。
然后即可和普通app一樣調(diào)試了,不過我在實(shí)際使用過程中悴了,發(fā)現(xiàn)有各種奇怪的事情搏恤,比如NSLog無法在控制臺(tái)輸出,應(yīng)該是bug吧湃交。
八熟空、 iOS8應(yīng)用文件系統(tǒng)
發(fā)現(xiàn)iOS8的文件系統(tǒng)發(fā)生了變化,新的文件系統(tǒng)將可執(zhí)行文件(即原來的.app文件)從沙盒中移到了另外一個(gè)地方搞莺,這樣感覺更合理痛阻。
測(cè)試代碼
下述代碼用于打印App Groups路徑、應(yīng)用的可執(zhí)行文件路徑腮敌、對(duì)應(yīng)的Documents路徑:
-?(void)logAppPath?
{?
//app?group路徑?
NSURL?*containerURL?=?[[NSFileManager?defaultManager]?containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];?
NSLog(@"app?group:\n%@",containerURL.path);?
//打印可執(zhí)行文件路徑?
NSLog(@"bundle:\n%@",[[NSBundle?mainBundle]?bundlePath]);?
//打印documents?
????NSArray?*paths?=?NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,?NSUserDomainMask,?YES);?
????NSString?*path?=?[paths?objectAtIndex:0];?
NSLog(@"documents:\n%@",path);?
}?
containing app執(zhí)行結(jié)果
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執(zhí)行結(jié)果
Jun?23?19:37:49?autonavis-iPad?com.foogry.AppExtensionDemo.TodayExtension[7638]?:?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]?:?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]?:?documents:?
/var/mobile/Containers/Data/PluginKitPlugin/57581433-3DBD-4930-971F-78D30C150E8A/Documents?
由此可見阱当,不管是extension還是containing app,他們的可執(zhí)行文件和保存數(shù)據(jù)的目錄都是分開存放的糜工,即所有app的可執(zhí)行文件都放在一個(gè)大目錄下弊添,保存數(shù)據(jù)的目錄保存在另一個(gè)大目錄下,同樣捌木,AppGroup放在另一個(gè)大目錄下油坝。