本文翻译自 Apple文档 On-Demand Resources Guide
按需加载资源是由App Store托管的内容,它和下载的app bundle是分开的。app请求一系列按需加载资源,而下载和存储资源是由操作系统来管理。这些资源可以是除可执行代码外,bundle支持的任何类型。支持类型的完整列表,请参阅附录a:按需加载的资源类型。
按需加载资源主要可以带来以下的几种好处:
在开发时,通过给按需加载资源分配一个或多个tag来区分。tag是一个你创建的字符串标识符。你可以使用tag的名称来区分这些资源在你的app中是如何使用的。例如,在一个游戏中,所有第五关的资源可以使用tag level-5
。
在运行时,你通过指定tag来请求访问远程资源。操作系统会下载和这个tag关联的所有资源,然后保留在存储中,直到app不再使用它们。当操作系统需要更多的存储空间,它会清理一个或多个不再使用的tag关联的资源。tag关联的资源在被清理之前可能会在设备中保存一段时间。
继续上面游戏的例子,游戏有许多关卡,用户只需要当前正在玩的关卡和可能玩的下一关卡的资源。下图的上部展示了包括全部关卡资源的app bundle。可以通过为不同关卡和其他无需放在app内的资源,创建不同tag的按需加载资源,来缩减app bundle的大小。下图的底部显示了一个更小的app,其中加了tag的资源由App Store托管。
还有一些其他的功能,比如让你指定那些在app启动之前必须加载的资源,为资源请求设置优先级,追踪资源下载的进度,为不再使用的已下载tag设置保留优先级。
当用户第一次启动app时,设备上唯一的按需加载资源是那些设置为预获取的。随着用户使用app,app会请求加了tag的资源,使用这些资源,然后告诉操作系统不再使用这些资源了。在这之后的某个时间,操作系统会清理一个或多个tag。
注意:app应该请求tag,而不是单个资源。
当你在开发按需加载资源时,可能会注意到请求下载某个tag时,其他tag也会被下载。这是因为操作系统处理的是资源包(asset pack),这些资源包会为共享资源的下载进行优化。一个tag可能对应多个资源包。资源包是在构建app时由Xcode生成的。
加了tag的资源的生命周期如下所示:
1、加了tag的资源由App Store托管。
这发生在app第一次启动和之前加载的tag被清理时。
2、加tag的资源被下载到设备上
这发生在app请求一个设备上没有的tag之后,或者发生在操作系统下载初始资源。
下图中展示的是,app请求 Level11
和 Forest
tag关联的资源,然后下载到设备上。
3、操作系统将tag保存在本地存储中,并通知app这些tag已经可以使用。
这发生在一个tag下的所有资源都已成功下载之后,或者tag对应的资源都已缓存在本地存储中。
在tag可用之后,app就可以使用tag关联的资源了。按需加载资源的使用方式和使用下载的app中资源包一样。下图展示了下载的资源就像是app的虚拟成员一样。
4、操作系统释放本地存储中的tag。
被释放的tag还会保存在设备中,直到它再次被请求,或者从本地存储中被清理掉。当一个tag没有被任何请求持有时,tag就可以被清理。在tag被清理之前,它还可能在设备上保存一段时间,包括app多次启动。请求一个tag会持有该tag,然后返回上面的步骤3。
清理tag会返回到上面的步骤1。你可以为tag设置保存优先级来影响清理的顺序。更多信息参见管理按需加载资源。
对于支持iOS 9.0及以后的app,按需加载资源是默认开启的。你也可以在target的build settings中手动更改。
1、在project navigator中选择工程文件。
2、在project editor中选择对应的target。
3、选择Build Settings选项卡。
4、展开Assets分类。
提示:可以在Build Settings选项卡右上角的搜索框中输入“Assets”,来快速定位到Assets分类。
5、设置Enable On-Demand Resources的值。
tag用来区分和管理按需加载资源。给一个资源添加一个或多个tag就能将其标记为按需加载资源。在运行时,所有管理按需加载资源的调用都是和tag相关,而不是单个资源。在运行时管理按需加载资源在管理按需加载资源中有讲到。
Xcode提供了管理tag和相关资源的工具。包括创建和编辑tag,添加和删除tag下的资源,指定操作系统在什么时刻下载tag关联的资源。
Resource Tags选项卡
Resource Tags选项卡在project editor中,用来浏览和编辑工程中全部的tag和相关资源。选项卡包括:
按需加载资源tag框
在按需加载资源tag框中,你可以快速浏览一个文件夹或资源,并为其添加删除tag。在Asset Catalog中,如果文件夹或者某个类型的资源可以加tag的话,这个框显示在Attributes inspector中,如下图所示。
创建tag的第一步是检查你的app在运行时如何使用资源。看看你的app是否有以下几种资源:
上面说的后两类的就有可能是按需加载资源。在app第一次启动后马上就要用到的资源可以设置为在安装后自动预获取。操作方法见预获取tag。
每个tag下的资源总计大小不能超过2GB。按需加载资源的总计大小不能超过20GB。按需加载资源的大小是不算在app bundle的大小中的。更多信息,参见附录B:按需加载资源的大小限制。
一个tag的理想大小是不超过64MB。这能很好地平衡下载速度和本地存储大小,当设备的存储空间不足时可以进行清理。
tag的名称最好能区分出资源是如何使用的。例如,所有和森林场景有关的资源就可以起名叫 forest-scenery
。
创建好tag后就可以分配给资源了。一个资源可以添加多个tag。例如在下图中,游戏的资源是按关卡和地点来分割的。
1、在project navigator中, 选择工程文件。
2、为需要新tag的target打开project editor 。
3、选择 Resource Tags 选项卡。
4、点击选项卡左上角的添加按钮(+)。
如下图所示,会显示一个新的tag输入框。
5、输入tag的名称来替换占位符。
可以为任何有效的按需加载资源类型添加tag。具体的有效类型,参见附录A:
1、在project navigator中, 选择Asset Catalog。
2、在列表中选择一项 。
3、为选择的项目打开Attributes inspector。
4、在按需加载资源tag框,输入tag的名称。
Xcode会根据输入的字符进行自动补全提示。
5、按下Return键来确认输入的tag名称。
注意:输入一个新的名称,并按下Return键,会创建一个新的tag。
1、在project navigator中, 选择一个文件。
2、打开实用工具区,点击File inspector 按钮。
3、在按需加载资源tag框,输入tag的名称。
Xcode会根据输入的字符进行自动补全提示。
4、按下Return键来确认输入的tag名称。
注意:输入一个新的名称,并按下Return键,会创建一个新的tag。
1、在project navigator中, 选择工程文件。
2、为target打开project editor 。
3、选择 Resource Tags 选项卡。
4、在搜索框中,输入tag的名称。列表会根据搜索的文字进行过滤。
5、点击添加按钮(+),会弹出一个对话框。
6、在输入框中,输入资源文件的名称。
7、选中想要的资源文件,点击Add。这个资源文件就加上tag了。
注意: Asset Catalogs中的资源必须使用按需加载资源tag框来添加tag。
可以通过在按需加载资源tag框删除tag,来将资源从tag中删除。也可以在Resource Tags 选项卡中删除资源。
通常来说,操作系统是不会下载一个tag下的资源,直到app请求一个tag并且该tag的资源没有在设备上缓存。一些tag中会包含启动时需要或者启动后马上要用到的资源。
在Resource Tags选项卡的Prefetched界面下,可以把tag分配给三个预获取优先级分类的其中一个。界面展示了按预获取分类分组的tag。tag可以在分类间拖动。
NSBundleResourceRequest
对象获取过,就有可能被清理掉。 下载和管理按需加载资源是由操作系统完成的。app使用 NSBundleResourceRequest 来:
当已下载的某些资源不再使用时,可以用NSBundle中的方法来设置保存优先级。
按需加载资源使用下面的4个方法来管理。
NSBundleResourceRequest
对象。需要管理的tag必须在初始化时指定,不能更改。 在tag下载到设备后的任何时间都可以设置tag的保存优先级。
注意:每个 NSBundleResourceRequest
对象都只能用于一个请求访问/结束访问循环。
app必须在使用tag的资源之前先请求访问这些tag。请求访问的第一步是为tag创建一个 NSBundleResourceRequest
对象。一个tag可以由多个 NSBundleResourceRequest
对象来管理。
每个 NSBundleResourceRequest
实例管理同一个bundle下的加了tag的资源。使用下面的两个方法来在初始化时设置被管理的tag和bundle:
initWithTags:
。 initWithTags:bundle:
。 注意: bundle
可以设置为main bundle。
列表4-1展示了一个初始化资源管理器的一个例子,所有加tag的资源都在main bundle中。
列表4-1 初始化一个NSBundleResourceRequest实例
// Initialize an NSBundleResourceRequest with the desired tags NSSet *tags = [NSSet setWithArray: @[@"birds", @"bridge", @"city"]]; // All the resources are in the main bundle so use the shorter initialization method resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
注意:tag和bundle只能在初始化时设置。
请求访问资源
在初始化NSBundleResourceRequest实例之后,就是请求访问了。当请求的所有tag下的所有资源都在本地存储中时,操作系统会持有这些资源,并使用回调通知app这些资源已经可以使用了。更多信息参见第三步按需加载资源的生命周期。
有两个方法来请求访问。当资源已在设备上时,这两个方法都可以允许访问。不同的是当资源不在设备上时会做什么。
beginAccessingResourcesWithCompletionHandler:
会从 App Store下载这些资源。 conditionallyBeginAccessingResourcesWithCompletionHandler:
不会下载资源。 两个方法都会在回调block中返回结果。所有的资源都必须已经在设备上才能使用。列表4-2展示了方法 beginAccessingResourcesWithCompletionHandler:
。
列表4-2 使用 beginAccessingResourcesWithCompletionHandler:
// Request access to the tags for this resource request [resourceRequest beginAccessingResourcesWithCompletionHandler: ^(NSError * __nullable error) { // Check if there is an error if (error) { // There is a problem, update app state (should inform user if appropriate) self.resourcesLoaded = NO; return; } // the resources associated with the the tags are loaded, start using them self.resourcesAvailable = YES; } ];
注意:在允许访问之后,不要使用同一个 NSBundleResourceRequest
实例再次请求访问。
检查tag是否已在设备上
有时当tag不在设备上时,你并不想开始下载它们。例如,当设备使用低带宽网络,并且高质量的图片和声音不在设备上时,你可以使用低质量资源。
当tag已在设备上, conditionallyBeginAccessingResourcesWithCompletionHandler:
会允许访问。如果tag不在设备上,app需要调用 beginAccessingResourcesWithCompletionHandler:
来下载它们。列表4-3展示了一个检查tag是否在设备上的例子。
注意:如果 conditionallyBeginAccessingResourcesWithCompletionHandler:
返回 YES
,就不要调用 beginAccessingResourcesWithCompletionHandler:
了。
列表4-3 使用 conditionallyBeginAccessingResourcesWithCompletionHandler:
// Request access to tags that may already be on the device [resourceRequest conditionallyBeginAccessingResourcesWithCompletionHandler: ^(BOOL resourcesAvailable) { // Check if the resources are available if (resourcesAvailable) { // the resources associated with the the tags are loaded, start using them self.highQualityResourcesAvailable = YES; } else { // The resources are not on the device and need to be loaded // Queue up a call to custom method for loading the tags using // beginAccessingResourcesWithCompletionHandler: dispatch_async(dispatch_get_main_queue(), ^{ [self loadLowerQualityTags]; } } } ];
何时请求tag
因为从App Store下载tag会花一些时间,你可以在需要使用tag之前请求tag。下载时间取决于总共要下载的大小,网络连接的速度,以及操作系统能分配多少资源来处理下载。
在没有带宽限制的理想情况,在300Mbps的 802.11n或者LTE网络(299.6Mbps)上,下载一个64MB的tag至少要用1.7秒。但实际情况是连接到因特网的速度要远低于300Mbps。
下载优先级
资源请求有一个默认的优先级,这可以随时更改,包括下载时。低优先级使用更少的操作系统资源,为其他任务腾出资源。这也会降低下载速度。低优先级有利于最大化app执行效率。列表4-4展示了一个更改请求优先级的例子。
列表4-4 更改下载优先级
// The priority is a between 0.0 and 1.0 self.resourceRequest.loadingPriority = 0.1;
提高下载优先级会使用更多的操作系统资源,相应地提高下载速度,降低app效率。如果下载很紧急,app可以将下载优先级设置为 NSBundleResourceRequestLoadingPriorityUrgent
。这会告诉操作系统尽可能多地分配资源来处理下载。一个使用场景就是用户在下载完成之前什么也做不了。列表4-5展示了一个当用户需要等待时,设置紧急优先级的例子
列表4-5 提高请求的优先级
// Raise the priority based on the urgency if (self.userWaiting) { // The user is waiting, request the maximum download time self.resourceRequest.loadingPriority = NSBundleResourceRequestLoadingPriorityUrgent; } else { // Set a higher priority self.resourceRequest.loadingPriority = 0.8; }
上面的代码直接使用了一个固定浮点数设置优先级。你也可以根据app的效率来更新下载优先级。
追踪下载进度
在下载开始之后,请求会开始更新 progress
,这是一个 NSProgress
类型的属性。app通过对 progress.fractionCompleted
进行KVO来追踪下载进度。这需要开始和结束观察,以及添加当值改变时执行的代码。列表4-6展示了如何开始和结束观察进度。列表4-7展示了当值改变时执行的代码。
列表4-6 开始和结束追踪下载进度
// Start observing fractionCompleted for the progress [self.resourceRequest.progress addObserver:self forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:NULL]; // Stop observing fractionCompleted for the progress [self.resourceRequest.progress removeObserver:self forKeyPath:@"fractionCompleted"];
列表4-7 当 fractionCompleted
的值改变时执行的代码
// -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // Check for the progress object and key if ((object == self.resourceRequest.progress) && ([keyPath isEqualToString:@"fractionCompleted])) { double progressSoFar = self.resourceRequest.progress.fractionCompleted; // do something with the value } }
追踪下载的两个重要用途是:
fractionCompleted
的值。 暂停和取消下载
正在进行的下载可以被暂停、恢复、取消。着通过 progress
属性,以及 NSProgress
提供的方法来完成。更多信息参见NSProgress类参考。
列表4-8 暂停、恢复、取消当前的下载
// Pause the current download [self.resourceRequest.progress pause]; // Resume the current download [self.resourceRequest.progress resume]; // Cancel the current download [self.resourceRequest.progress cancel];
结束访问
当app不再使用资源时,结束访问能让操作系统可以回收存储空间。这也就是按需加载资源基础中按需加载资源的生命周期的第4步。有2种方法结束访问:
endAccessingResources
,如列表4-9所示。 列表4-9 结束对tag的访问
// End access by calling this method or deallocating the NSBUndleResourceRequest instance [self.resourceRequest endAccessingResources];
在 endAccessingResources
调用之后,这个请求就不能再用于请求访问了。如果app还需要访问同一个tag,需要再重新创建一个 NSBundleResourceRequest
实例。
设置保留优先级
某些tag中的资源可能比其他的更重要。例如,应用内购买或者基本功能的资源就会被更频繁地用到。app可以为这些tag设置一个高保留优先级。当操作系统开始清理tag时,会从最低保留优先级开始。
可以使用 NSBundle
的方法来设置和检查保留优先级。
列表4-10 为tag检查和设置保留优先级
// Check the preservation priority for the llama in-app purchase module double currentPriority = [[NSBundle mainBundle] preservationPriorityForTag:@"iap-llamas"]; // Set the priority to the maximum of 1.0 (the default is 0.0) // The call to set the priority takes a set of tags NSSet *tags = [NSSet setWithArray: @[@"iap-llamas"]]; [[NSBundle mainBundle] setPreservationPriority:1.0 forTags:tags];
低存储空间警告
当操作系统没有办法为当前正在请求的资源释放出足够的空间时,系统会发出一个通知。你的app应该停止访问所有不再使用的tag,如上面 结束访问 描述的。如果操作系统不能释放出足够空间,app会被终止。
例如,在一个有多个关卡的游戏中,用户正在第4关,app请求第3、5、6关的tag。当低存储空间警告发生时,app可以释放第3、6关的tag。列表4-11展示了注册低存储空间通知的代码。列表4-12展示了释放不需要tag的方法。
列表4-11 注册 NSBundleResourceRequestLowDiskSpaceNotification
通知
// End access by calling this method or deallocating the NSBUndleResourceRequest instance [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lowDiskSpace:) name:NSBundleResourceRequestLowDiskSpaceNotification object:nil];
注册通知一般由app delegate或者主view来完成。
列表4-12 低存储空间通知的处理
// Notification handler for low disk space warning -(void)lowDiskSpace:(NSNotification*)theNotification { // Free the lower priority resource requests for (NSBundleResourceRequest *atRequest in self.lowPriorityRequests) { // End accessing the resources [atRequest endAccessingResources]; } // clear lowPriorityRequests preventing multiple calls to endAccesingResource [self.lowPriorityRequests removeAllObjects]; }
lowPriorityRequests
不是由操作系统提供的。它是一个需要由app创建和维持的mutable set 。
在你开发的过程中可能会遇到几类问题。不同类别的问题需要使用不同的工具来调试。主要有以下几类问题:
调试意外状态最有用的工具就是Xcode中的 磁盘仪表 了。磁盘仪表中会显示tag的当前状态,如下图所示。
磁盘仪表会显示每个tag的大小和状态。大小是针对当前设备裁剪后的。
表5-1描述了磁盘仪表中tag可能处于的状态。
表5-1 tag的状态
状态 | 描述 |
未下载 | 在此调试会话中,tag还没有被下载到设备上。 |
部分已下载 | 可能由于连接中断造成tag的一部分已下载。 |
下载中 | tag正在下载中。可能会显示一个进度条。 |
已下载 | tag已在设备上,且没有被任何 NSBundleResourceRequest 对象使用。 |
使用中 | tag已在设备上,且正在被app使用。 |
被清理 | tag已在设备商,且已经被清理出本地存储。在被使用前,必须先下载。 |
在模拟器或真机上运行app都可以使用磁盘仪表。
下表列出了可以加tag的资源类型。
表A-1 资源类型
类型 | Asset catalog | 文件 |
图像文件 | - | √ |
多媒体文件 | - | √ |
Asset catalog image set | √ | - |
Asset catalog folder | √ | - |
SpriteKit scene | - | √ |
SpriteKit texture atlas | √ | √ |
SpriteKit particle | - | √ |
WatchKit Complication | √ | - |
OpenGL shader | - | √ |
Data file | √ | √ |
数据文件可以包括除了可执行的Swift、Objective-C、C、或者C++二进制包以外的任何类型数据。由脚本语言生成的文件可以用作资源。
在App Store 提交时和app运行时,资源的使用的内存大小没有限制。
表B-1 资源大小
项目 | 大小 | Slicing |
app二进制包 | 2GB | √ |
Initial install tags | 2GB | √ |
Initial install and prefetched tags | 4GB | √ |
In use on-demand resources | 2GB | √ |
Hosted on-demand resources | 20GB | - |
NSBundleResourceRequest
对象访问tag,tag就算是在使用中。