iOS 开发之照片框架PhotoKit 详解 上

本文转载自Kayo Lee发表的文章,本文链接:http://kayosite.com/ios-development-and-detail-of-photo-framework-part-two.html

一. 概况

本文接着?iOS 开发之照片框架详解,侧重介绍在前文中简单介绍过的 PhotoKit 及其与 ALAssetLibrary 的差异,以及如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法。

这里引用一下前文中对 PhotoKit 基本构成的介绍:

PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源

PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值

PHAssetCollection: PHCollection 的子类,表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)

PHFetchResult: 表示一系列的资源结果集合,也可以是相册的集合,从?PHCollection 的类方法中获得

PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格

PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

这里还有一个额外的概念?PHCollectionList,表示一组?PHCollection,它本身也是一个?PHCollection,因此?PHCollection 作为一个集合,可以包含其他集合,这使到 PhotoKit 的组成比 ALAssetLibrary 要复杂一些。另外与 ALAssetLibrary 相似,一个 PHAsset 可以同时属于多个不同的 PHAssetCollection,最常见的例子就是刚刚拍摄的照片,至少同时属于“最近添加”、“相机胶卷”以及“照片 - 精选”这三个 PHAssetCollection。关于这几个概念的关系如下图:

二. ?PhotoKit 的机制

1. 获取资源

在 ALAssetLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据,并且数据是从

ALAssetLibrary(照片库) - ALAssetGroup(相册)- ALAsset(资源)这一路径逐层获取,即使有直接从

ALAssetLibrary 这一层获取 ALAsset 的接口,本质上也是枚举 ALAssetLibrary

所得,并不是直接获取,这样的好处很明显,就是非常符合实际应用中资源的显示路径:照片库 - 相册 -

图片或视频,但由于采用枚举的方式获取资源,效率低而且不灵活。

而在 PhotoKit 中,则是采用“获取”的方式拉取资源,这些获取的手段,都是一系列形如 class func

fetchXXX(..., options: PHFetchOptions) -> PHFetchResult

的类方法,具体使用哪个类方法,则视乎需要获取的是相册、时刻还是资源,这类方法中的 option

充当了过滤器的作用,可以过滤相册的类型,日期,名称等,从而直接获取对应的资源而不需要枚举。例如在前文中列举个的几个小例子:

// 列出所有相册智能相册

10// 列出所有相册智能相册

PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];

// 列出所有用户创建的相册

PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];

// 获取所有资源的集合,并按资源的创建时间排序

PHFetchOptions *options = [[PHFetchOptions alloc] init];

options.sortDescriptors = @[[NSSortDescriptorsortDescriptorWithKey:@"creationDate"ascending:YES]];

PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

如前面提到过的那样,从?PHAssetCollection

获取中获取到的可以是相册也可以是资源,但无论是哪种内容,都统一使用?PHFetchResult 对象封装起来,因此虽然

PHAssetCollection 获取到的结果可能是多样的,但通过?PHFetchResult 就可以使用统一的方法去处理这些内容(即遍历

PHFetchResult)。例如扩展上面的例子:

// 列出所有相册智能相册

24// 列出所有相册智能相册

PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];

// 这时 smartAlbums 中保存的应该是各个智能相册对应的 PHAssetCollection

for(NSIntegeri = 0; i < fetchResult.count; i++) {

// 获取一个相册(PHAssetCollection)

PHCollection *collection = fetchResult[i];

if([collection isKindOfClass:[PHAssetCollectionclass]]) {

PHAssetCollection *assetCollection = (PHAssetCollection *)collection;

// 从每一个智能相册中获取到的 PHFetchResult 中包含的才是真正的资源(PHAsset)

PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];

else{

NSAssert(NO,@"Fetch collection not PHCollection: %@", collection);

}

}

// 获取所有资源的集合,并按资源的创建时间排序

PHFetchOptions *options = [[PHFetchOptions alloc] init];

options.sortDescriptors = @[[NSSortDescriptorsortDescriptorWithKey:@"creationDate"ascending:YES]];

PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

// 这时 assetsFetchResults 中包含的,应该就是各个资源(PHAsset)

for(NSIntegeri = 0; i < fetchResult.count; i++) {

// 获取一个资源(PHAsset)

PHAsset *asset = fetchResult[i];

}

?2. 获取图像的方式与坑点

经过了上面的步骤,已经可以了解到如何在 PhotoKit 中获取到代表资源的 PHAsset 了,但与 ALAssetLibrary 中从

ALAsset 中直接获取图像的方式不同,PhotoKit 无法直接从 PHAsset

的实例中获取图像,而是引入了一个管理器?PHImageManager 获取图像。PHImageManager

是通过请求的方式拉取图像,并可以控制请求得到的图像的尺寸、剪裁方式、质量,缓存以及请求本身的管理(发出请求、取消请求)等。而请求图像的方法是

PHImageManager 的一个实例方法: -(PHImageRequestID)requestImageForAsset:(PHAsset *)asset

- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset

targetSize:(CGSize)targetSize

contentMode:(PHImageContentMode)contentMode

options:(nullable PHImageRequestOptions *)options

resultHandler:(void(^)(UIImage *__nullable result,NSDictionary*__nullable info))resultHandler;

这个方法中的参数坑点不少,下面逐个参数列举一下其作用及坑点:

asset,图像对应的 PHAsset。

targetSize,需要获取的图像的尺寸,如果输入的尺寸大于资源原图的尺寸,则只返回原图。需要注意在 PHImageManager 中,所有的尺寸都是用 Pixel 作为单位(Note that all sizes are in pixels),因此这里想要获得正确大小的图像,需要把输入的尺寸转换为 Pixel如果需要返回原图尺寸,可以传入 PhotoKit 中预先定义好的常量?PHImageManagerMaximumSize,表示返回可选范围内的最大的尺寸,即原图尺寸。

contentMode,图像的剪裁方式,与?UIView 的 contentMode 参数相似,控制照片应该以按比例缩放还是按比例填充的方式放到最终展示的容器内。注意如果 targetSize 传入?PHImageManagerMaximumSize,则 contentMode 无论传入什么值都会被视为?PHImageContentModeDefault

options,一个?PHImageRequestOptions 的实例,可以控制的内容相当丰富,包括图像的质量、版本,也会有参数控制图像的剪裁,下面再展开说明。

resultHandler,请求结束后被调用的 block,返回一个包含资源对于图像的 UIImage 和包含图像信息的一个 NSDictionary,在整个请求的周期中,这个 block 可能会被多次调用,关于这点连同options 参数在下面展开说明。

(1)PHImageRequestOptions 与 iCloud 照片库

PHImageRequestOptions 中包含了一系列控制请求图像的属性。

resizeMode 属性控制图像的剪裁,不知道为什么 PhotoKit 会在请求图像方法(requestImageForAsset)中已经有控制图像剪裁的参数后(contentMode),还在 options 中加入控制剪裁的属性,但如果两个地方所控制的剪裁结果有所冲突,PhotoKit 会以resizeMode 的结果为准。另外,resizeMode 也有控制图像质量的作用。如?resizeMode?设置为?PHImageRequestOptionsResizeModeExact 则返回图像必须和目标大小相匹配,并且图像质量也为高质量图像,而设置为 PHImageRequestOptionsResizeModeFast 则请求的效率更高,但返回的图像可能和目标大小不一样并且质量较低。

在 PhotoKit 中,对 iCloud 照片库有很好的支持,如果用户开启了 iCloud 照片库,并且选择了“优化 iPhone/iPad 储存空间”,或者选择了“下载并保留原件”但原件还没有加载好的时候,PhotoKit 也会预先拿到这些非本地图像的 PHAsset,但是由于本地并没有原图,所以如果产生了请求高清图的请求,PHotoKit 会尝试从 iCloud 下载图片,而这个行为最终的表现,会被?PHImageRequestOptions 中的值所影响。PHImageRequestOptions 中常常会用的几个属性如下:

networkAccessAllowed 参数控制是否允许网络请求,默认为 NO,如果不允许网络请求,那么就没有然后了,当然也拉取不到 iCloud 的图像原件。deliveryMode 则用于控制请求的图片质量。synchronous 控制是否为同步请求,默认为 NO,如果?synchronous

为 YES,即同步请求时,deliveryMode 会被视为

PHImageRequestOptionsDeliveryModeHighQualityFormat,即自动返回高质量的图片,因此不建议使用同步请求,否则如果界面需要等待返回的图像才能进一步作出反应,则反应时长会很长。

还有一个与 iCloud 密切相关的属性?progressHandler,当图像需要从 iCloud 下载时,这个 block 会被自动调用,block 中会返回图像下载的进度,图像的信息,出错信息。开发者可以利用这些信息反馈给用户当前图像的下载进度以及状况,但需要注意?progressHandler 不在主线程上执行,因此在其中需要操作 UI,则需要手工放到主线程执行。

上面有提到,requestImageForAsset 中的参数?resultHandler 可能会被多次调用,这种情况就是图像需要从

iCloud 中下载的情况。在?requestImageForAsset

返回的内容中,一开始的那一次请求中会返回一个小尺寸的图像版本,当高清图像还在下载时,开发者可以首先给用户展示这个低清的图像版本,然后 block

在多次调用后,最终会返回高清的原图。至于当前返回的图像是哪个版本的图像,可以通过 block 返回的 NSDictionary info

中获知,PHImageResultIsDegradedKey 表示当前返回的 UIImage

是低清图。如果需要判断是否已经获得高清图,可以这样判断:

1

2// 排除取消,错误,低清图三种情况,即已经获取到了高清图

BOOLdownloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];

另外,当我们使用?requestImageForAsset 发出对图像的请求时,如果在同一个 PHImageManager

中同时对同一个资源发出图像请求,请求的进度是可以共享的,因此我们可以利用这个特性,把 PHImageManager

以单例的形式使用,这样在切换界面时也不用担心无法传递图像的下载进度。例如,在图像的列表页面触发了下载图像,当我们离开列表页面进入预览大图界面时,并不用担心会重新图像会重新下载,只要没有手工取消图像下载,进入预览大图界面下载图像会自动继续从上次的进度下载图像。

如果希望取消下载图像,则可以使用?PHImageManager 的 ?cancelImageRequest 方法,它传入的是请求图像的请求

ID,这个 ID 可以从?requestImageForAsset 的返回值中获得,也可以从前面提到的包含图像信息的?NSDictionary

info 中获得,当然前提是这个这个接收取消请求的 PHImageManager 与刚刚发出请求的 PHImageManager

是同一个实例,如上面所述使用单例是最为简单有效的方式。

最后,还要介绍一个?PHImageRequestOptions 的属性

versions,这个属性是指获取的图像是否需要包含系统相册“编辑”功能处理过的信息(如滤镜,旋转等),这一点比 ALAssetLibrary

要灵活很多,ALAssetLibrary 中并不能灵活地控制获取的图像是否带有“编辑”处理过的效果,例如在 ALAsset 中获取原图的接口

fullResolutionImage

获取到的是不带“编辑”效果的图像,要想获取带有“编辑”效果的图像,只能自行处理获取这些滤镜效果,并手工叠加上去。在我们的 UI 框架 QMUI

中就有对获取原图作出这样的封装,整个过程也较为繁琐,而框架中处理 PhotoKit 的部分则灵活很多,这也体现了 PhotoKit 相比

ALAssetLibrary 的最主要特点——复杂但灵活。文章的第三部分也会详细列出如何处理这个问题。

(2)获取图像的优化

PHImageManager 提供了一个子类?PHImageCachingManager

用于处理图像的缓存,但是这个子类并不只是图像本身的缓存,而是更加实用——处理图像的整个加载过程的缓存。例如要在一个?collectionView

上展示图像列表这类大量的资源图像的缩略图时,可以利用 PHImageCachingManager?预先将一些图像加载到内存中,这对优化

collectionView

滚动时的表现很有帮助。然而,这只是官方说法,实际上由于加载图像的过程并不确定,每个业务加载图像的实际需求都可能不一样,因此?PHImageCachingManager

也采用比较松散的方法去控制这些缓存,其中的关键方法:

1

- (void)startCachingImagesForAssets:(NSArray *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;

需要传入一组 PHAsset,以及 targetSize,contentMode,以及一个?PHImageRequestOptions,如上面所述,这些参数之间的有着互相影响的作用,因此实际上不同的场景对于每个参数要求都不一样,而这些参数的最佳取值也只能通过实际在场景中测试所得。因此,比起使用?PHImageCachingManager,我总结了一些更为简易可行的缓存方法:

获取图片时尽量获取预览图,不要直接显示原件,建议获取与设备屏幕同样大小的图像即可,实际上系统相册预览大图时使用的也是预览图,这也是系统相册加载速度快的原因。

获取图片使用异步请求,如上面所述,当请求为异步时返回图像的 block 会被多次调用,先返回低清图,再返回高清图,这样一来可以大大减少 UI 的等待时间。

获取到高清图后可以缓存下来,简单地使用变量缓存即可,尽量在获取到高清图后避免再次发起请求获取图像。因为即使图像原件已经下载下来,重新请求高清图时因为图片的尺寸比较大,因此系统生成图像和剪裁图像也会花费一些时间。

预先加载图像,如像预览大图这类情景中,用户同时只会看到一张大图,因此在观看某一张图片时,预先请求其邻近两张图片,对于加快 UI 的响应很有帮助。

经过实际测试,如果请求的是缩略图(即尺寸小的图像),那么即使请求的图像很多,仍不会产生任何不流畅的表现,但如果请求的是高清大图,那么即使只是同时请求几张图都会产生不流畅的状况。如上面提到过的那样,这些的状况的出现很可能是请求大图时由图片元数据产生图像,以及剪裁图像的过程耗时较多。所以按实际表现来看,即使

PhotoKit

有自己的缓存策略,仍然很难避免这部分耗时。因此上面几点优化获取图像的策略重点也是放在减少图像大小,异步请求以及做缓存几个方面。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容