天天看点

App Extensions for iOS 8

近日,苹果宣布了App Extensions for iOS 8,它允许开发人员将功能和内容扩展到单个应用程序之外。 

其中,App Extensions允许修正的两个主要iOS限制是:由Open In系统创建的应用程序之间不必要的数据复制和无法替代苹果的键盘。这种状况与Android平台允许用户借助Widget和自定义键盘形成鲜明的对比。 

然而,其中有一个最可能的误解需要澄清一下,就是iOS 8 App Extensions不同于Android Indents,Ars Technica网站撰稿人Andrew Cunningham这样写道。根据Google的描述: 

Intent提供了一种机制,用于不同应用程序代码之间的后期运行时绑定。它主要用来启动Activities,因此可以将它看作是Activities之间的粘合剂。从本质上讲,它是一个无源数据结构,存放要执行动作的抽象描述。 

虽然Extension在很多情况下与Intent没有什么不同,但在iOS 8中,App Extension系统的整体设计使得它与Intent有很大的不同。 

Extension的生命周期

正如苹果文档中的描述,Extension是通过“包含应用程序(containing app)”提供的专门的二进制文件。包含应用程序只负责提供Extension,后者是独立运行的。尽管如此,一个iOS包含应用程序实际上还需要提供Extension之外的某些功能。OS X没有这样的要求,其上的包含应用程序不需要提供任何额外的功能。 

文档提到,Extension的生命周期与它的包含应用程序完全没有关系,它由4个阶段组成: 

  1. 用户选择一个App Extension 
  2. 系统启动它 
  3. App Extension运行 
  4. 系统终止App Extension

如果两个应用程序需要同样的Extension做相同的工作,那么这会发生在两个独立的Extension进程中。 

这一方法的主要动机是,通过生命周期短暂的Extension减少内存使用和能量消耗,并防止一个Extension的错误影响到使用了相同Extension的应用程序。 

Extension的类型

Extension有多种类型,每一种类型都绑定到一个称为“扩展点(Extension point)”的系统区域: 

  • “今日(Today,又称为Widget)”:可以快速获取更新或者在通知中心的今日视图中执行一项快速任务。 
  • 共享:发布到一个共享网站或者与其它应用程序共享内容。 
  • 动作:在另一个应用程序的上下文中操作或查看内容。 
  • 照片编辑(仅限于iOS):在照片应用程序中编辑照片或视频。 
  • 查找器(仅限于iOS):在查找器中直接显示文件同步的状态信息。 
  • 文档提供程序(仅限于iOS):提供对文件库的访问和管理。 
  • 自定义键盘(仅限于iOS):用自定义键盘替代iOS系统键盘,并用于所有的应用程序中。

由于每个扩展点都有与之相关的使用策略和专门的API,开发人员必须为他们想要提供的那种功能选择恰当的扩展点。例如,在默认情况下,键盘Extension“不能访问网络,而且不能与其包含应用程序共享同一容器”。通过对Extension进行恰当的配置,这样的限制可以移除,但开发人员仍然需要遵守苹果应用商店审查指南和iOS开发者计划许可协议中的具体的网络键盘指南。 

沙箱和安全

众所周知,每个iOS应用程序都有自己的沙箱。通过Mac苹果应用商店分发的OS X应用程序也有类似的要求,不过许多OS X应用程序是在Mac苹果应用商店之外分发的,并不需要遵守这一沙箱要求。 

沙箱是苹果iOS安全策略的基石之一。沙箱是为了限制应用程序对文件、首选项、网络资源、硬件等的访问,具体来讲,其目的是为了限制受损的应用程序可能对系统造成的损害。 

考虑到并不是所有可以用在应用程序中的API都可以用在Extension中,所以与通常的应用程序相比,App Extension运行在有更多限制的沙箱中。不能在Extension中使用的API标记为不可用宏,如NS_EXTENSIONS_UNAVAILABLE,它会在链接时导致失败。 

此外,对于Extension与其它应用程序之间的通信,苹果有几项强制规定: 

  1. 调用Extension的应用程序即主应用程序不能启动Extension;只有系统可以启动Extension。 
  2. 当Extension启动后,主应用程序就和它直接通信。 
  3. 主应用程序永远不和包含应用程序直接通信。 
  4. Extension不是一个应用程序,但它由系统生成,并有它自己单独的进程。 
  5. 为了在包含应用程序和它的Extension之间共享数据,包含应用程序及其Extension都必须是应用程序组的一部分。对于应用程序组的其中两个成员,部分数据可以在两者沙箱之外的第三个容器中共享。

正如Ars Technica的Andrew Cunningham总结的那样,这些规则的最终结果主要是一个应用程序不能进入另一个应用程序的沙箱。这与Android相反,在Android上,内容提供程序和解析程序仍然可以一起工作来为应用程序提供对其它应用程序中数据的访问。 

反应

App Extension已经在iOS开发人员中间引发了极大的兴趣。Cunningham说,“Extension将会对新操作系统产生最大最显著的影响”。 

MacStories的Federico Viticci收集了若干开发人员对苹果公告的反应,他说“Extension对于iOS应用程序生态系统的影响很难量化,但是……考虑到开发人员对苹果公告的反应,在今年秋天,我们将看到许多又新又酷的东西”。 

另一方面,安全专家提出警告,更强大的功能往往带来更大的风险。安全公司Symantec写到:“在iOS 8发布之前,我们无法看到攻击是上升还是下降,因此,我们无法知道这些功能效果如何”,同时他们也承认“根据目前获得的信息,少数安全功能应该会增强iOS设备的防护等级”。

================读数据==================================================

App Extensions for iOS 8

转自王中周的技术博客

  一、关于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实现的,后文将会详述。   三者间的关系可以通过官网给的两张图片形象地说明:

App Extensions for iOS 8
App Extensions for iOS 8

  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。   添加成功以后如下图所示:

App Extensions for iOS 8

在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就是为了任务而生!   下图来自官方文档,它将生命周期划分的更详细:  

App Extensions for iOS 8

  通过打印日志发现,Today中的widget在将Today切换到全部或者未读通知时都会被杀掉。   七、 调试   extension和普通app的调试方式差不多,开始调试前先选中extension对应的target,点击run,就会弹出下图所示选择框:  

App Extensions for iOS 8

  需要选择一个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上。   文中可能有理解有误的地方,还请指出。   参考文档   App Extension Programming Guide   Crash Course In iOS 8 Widgets   Notification Center Framework Reference   iOS 8 Release Notes   Entitlement Key Reference   苹果的插件生态系统,开发者的新世界   iOS 8 Extensions: Apple’s Plan for a Powerful App Ecosystem   Property List Programming Guide

继续阅读