1. Model
PHAsset 、PHAssetCollection、PHCollectionList 是Photos框架中的模型类,PHAsset类是数据模型,描述图片或者视频文件数据;PHAssetCollection描述了包括相册、moments、智能相册以及共享照片流等一系列图片或者视频文件的集合的元数据;PHCollectionList是一组资源集合,可能是一组Assets,也可能是一组collection。 它们关系如下图:
photos_model.png
1.1 PHAsset
PHAsset是iOS8平台的新接口,存储获取图片和视频文件的元数据
,这些图片和视频文件可能在手机本地,也可能在iCloud。相当于以前的 ALAsset接口,但比起ALAsset,PhotoKit 提供了额外的关于用户资源的元数据,而这些数据在以前使用 ALAssetsLibrary 框架中是没有办法访问,或者很难访问到。
它有几个要点:
- 需要用PHAsset的一些fetchAssets...系列类方法获取到,你想要拿到的照片的PHAsset对象。
- PHAsset 对象只包含元数据,image或者video数据需要用PHImageManager加载。
- PHAsset对象的元数据是不可变的,修改元数据需要借助 PHAssetChangeRequest
它有几个重要属性:
mediaType
:资源类型,图片或者音频或视频Paste_Image.png
mediaSubtypes
:图片又包含全景图(Panorama)、HDR图片、屏幕截图、livePhoto .live photo 加3Dtouch效果太赞! [视频链接].我们可以使用照片资源的 mediaSubtypes 属性验证资源库中的图像在捕捉时是否开启了 HDR,拍摄时是否使用了相机应用的全景模式.(http://www.macworld.com/article/2988585/apple-phone/how-to-get-started-with-3d-touch-live-photos-and-4k-video-on-the-iphone-6s.html)
Paste_Image.png
Creation date
Location
Favorite
布尔值,用户是否标记资源为"收藏",我们平时浏览照片或视频,在下方点就表示收藏这张图。hidden
要验证一个资源是否被用户标记为收被隐藏,只要检查 PHAsset 实例的 hidden 属性即可。sourceType
: 资源可以来源自用户相册、iCloud、iTunes同步Paste_Image.png
representsBurst
和burstSelectionTypes
: 对于一个资源,如果其 PHAsset 的 representsBurst 属性为 true,则表示这个资源是一系列连拍照片中的代表照片 (多张照片是在用户按住快门时拍摄的)。它还有一个属性是 burstIdentifier,如果想要获取连拍照片中的剩余的其他照片,可以通过将这个值传入 fetchAssetsWithBurstIdentifier(...) 方法来获取。用户可以在连拍的照片中做标记;此外,系统也会自动用各种试探来标记用户可能会选择的潜在代表照片。这个元数据是可以通过 PHAsset 的 burstSelectionTypes 属性来访问。这个属性是用三个常量组成的位掩码:.UserPick 表示用户手动标记的资源,.AutoPick 表示用户可能标记的潜在资源,.None 表示没有标记的资源。
Paste_Image.png
我模拟器相册中某张图片:
<PHAsset: 0x7f8293659e20> B84E8479-475C-4727-A4A4-B77AA9980897/L0/001 mediaType=1/0, sourceType=1, (4288x2848), creationDate=2009-10-09 21:09:20 +0000, location=0, hidden=0, favorite=0
localIdentifier
Photos 框架中的根类PHObject只有一个公开接口localIdentifier,是对象唯一唯一标志符.PHObject实现了-isEqual 和-hash方法.可以直接使用localIdentifier属性对PHObject及其子类对象进行对比是否同一个对象。
在我的模拟器中“最近删除”这个相册的localIdentifier为72053882-BF20-4D4A-B1A5-03D1DDAE1707/L0/040
1.2 PHAssetCollection
PHAssetCollection是一组有序的资源集合,包括相册、moments、智能相册以及共享照片流.
它的重要属性 ;
- assetCollectionType 资源集合类型,比如相册或者“时刻”相册
typedef NS_ENUM(NSInteger, PHAssetCollectionType) { PHAssetCollectionTypeAlbum = 1, PHAssetCollectionTypeSmartAlbum = 2, PHAssetCollectionTypeMoment = 3, } NS_ENUM_AVAILABLE_IOS(8_0);
- assetCollectionSubtype 子类型
enum PHAssetCollectionType : Int {
case Album //从 iTunes 同步来的相册,以及用户在 Photos 中自己建立的相册
case SmartAlbum //经由相机得来的相册
case Moment //Photos 为我们自动生成的时间分组的相册
}
enum PHAssetCollectionSubtype : Int {
case AlbumRegular //用户在 Photos 中创建的相册
case AlbumSyncedEvent //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步过来的事件。然而,在iTunes 12 以及iOS 9.0 beta4上,选用该类型没法获取同步的事件相册,而必须使用AlbumSyncedAlbum。
case AlbumSyncedFaces //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步的人物相册。
case AlbumSyncedAlbum //做了 AlbumSyncedEvent 应该做的事
case AlbumImported //从相机或是外部存储导入的相册,完全没有这方面的使用经验,没法验证。
case AlbumMyPhotoStream //用户的 iCloud 照片流
case AlbumCloudShared //用户使用 iCloud 共享的相册
case SmartAlbumGeneric //文档解释为非特殊类型的相册,主要包括从 iPhoto 同步过来的相册。
case SmartAlbumPanoramas //相机拍摄的全景照片
case SmartAlbumVideos //相机拍摄的视频
case SmartAlbumFavorites //收藏文件夹
case SmartAlbumTimelapses //延时视频文件夹,同时也会出现在视频文件夹中
case SmartAlbumAllHidden //包含隐藏照片或视频的文件夹
case SmartAlbumRecentlyAdded //相机近期拍摄的照片或视频
case SmartAlbumBursts //连拍模式拍摄的照片
case SmartAlbumUserLibrary //这个命名最神奇了,就是相机相册,所有相机拍摄的照片或视频都会出现在该相册中,而且使用其他应用保存的照片也会出现在这里。
case Any //包含所有类型
}
- startDate
- endDate
- estimatedAssetCount :估算的asset数量,不精确
我模拟器 智能相册里,“最近删除”这个相册的对象信息
<PHAssetCollection: 0x7f82951bd640> 72053882-BF20-4D4A-B1A5-03D1DDAE1707/L0/040 Recently Deleted assetCollectionType=2/1000000201
1.3 PHCollectionList
一组有序的资源集合的集合,暂时还没用过。几个重要属性:
- collectionListType
- collectionListSubtype
- startDate
- endDate
2. 获取模型数据
PHAsset 、PHCollection、PHCollectionList有一系列类方法可供我们访问资源的元数据
2.1 比如PHAsset提供了一系列获取PHAsset对象的方法
+ fetchAssetsInAssetCollection:options:
+ fetchAssetsWithMediaType:options:
+ fetchAssetsWithLocalIdentifiers:options:
+ fetchKeyAssetsInAssetCollection:options:
+ fetchAssetsWithOptions:
+ fetchAssetsWithBurstIdentifier:options:
+ fetchAssetsWithALAssetURLs:options:
其中fetchAssetsInAssetCollection:options:方法可以获取资源集合中的所有asset对象。每个方法中的 PHFetchOptions参数,是获取asset对象的一些配置,我们可以设置获取asset的条件,比如获取哪种资源,如何分类。获取的时候,如果该参数为空,则使用系统的默认值,当我们调用如上所示方法获取时,可以直接传nil。
例子:
PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init];
// 按图片生成时间排序
allPhotosOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
// 获取图片
PHFetchResult *allPhotos = [PHAsset fetchAssetsWithOptions:allPhotosOptions];
// 获取智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 获取用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
2.2 PHFetchOptions
- predicate : 做选择的约束条件。比如,只获取图片,不获取视频。指定 PHAssetMediaType为image.
// swift: options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.Image.rawValue)
- sortDescriptors 可指定字段用来对获取结果进行排序
- includeHiddenAssets 获取结果是否包括被隐藏的资源
- includeAllBurstAssets 获取结果是否包括连拍资源
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.wantsIncrementalChangeDetails = YES;
options.includeAllBurstAssets = YES;
options.includeHiddenAssets = YES;
// 只取图片
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeImage];
// 按时间排序
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *albums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumAllHidden options:nil];
2.3 PHFetchResult
类似数组,存储获取到asset对象集合。
- 同步快速获取结果,
- 即使结果集很大,框架也能保证获取速度. 因为它不会一次性将所有结果放进内存,而是按需批量加载
- 可以用类似 NSArray 的接口来访问PHFetchResult结果内的集合。
比如 :PHCollection *collection = smartAlbums[n];
3. 资源对象的增删改
上面PHAsset PHAssetCollection PHCollectionList对象都是不可变的。那么我们如何实现资源增删改呢?要借助 request API :
request.png
比如:这段代码用来修改一张图片的资源属性:是否被收藏。
// 创建
request = [PHAssetChangeRequest creationRequestForAssetFromImage:image]
- (void)toggleFavoriteForAsset:(PHAsset *)asset {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
// 改变
PHAssetChangeRequest *request = [PHAssetChangeRequest changeRequestForAsset:asset];
request.favorite = !asset.favorite;
} completionHandler:^(BOOL success, NSError *error) {
NSLog(@"Finished updating asset. %@", (success ? @"Success." : error));
}];
}
(1) 创建PHAssetChangeRequest对象。想要修改资源,需要创建一个 PHAssetChangeRequest 。然后你就可以修改创建创建日期,资源位置,以及是否将隐藏资源,是否将资源看做用户收藏等。此外,你还可以从用户的库里删除资源。类似地,若要修改资源集合或集合列表,需要创建一个 PHAssetCollectionChangeRequest 或 PHCollectionListChangeRequest对象。然后你就可以修改集合标题,添加或删除集合成员,或者完全删除集合。
(2) 操作的请求都要求在PHPhotoLibrary的performChanges的changeBlock中执行
(3)如果有更新UI操作,需要遵守PHPhotoLibraryChangeObserver
协议,实现photoLibraryDidChange(changeInfo: PHChange!)方法.在photoLibraryDidChange中进行UI更新操作。
比如,下面2段代码用来向册中新增一张图片:
// 为image对象生成PHAsset对象,并加入collection
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
if (self.assetCollection) {
PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:self.assetCollection];
[assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];
}
} completionHandler:^(BOOL success, NSError *error) {
if (!success) {
NSLog(@"Error creating asset: %@", error);
}
}];
创建一个新资源只要用 creationRequestForAssetFromXXX(...)
工厂方法,来创建变化请求,并传入资源图像数据 (或一个 URL)。如果你需要对新建的资源做额外的修改,你可以用创建变化请求的placeholderForCreatedAsset属性。它会返回一个可用的 placeholder 来代替“真实的” PHAsset 引用.
// PHPhotoLibraryChangeObserver协议的方法,更新相册UI
- (void)photoLibraryDidChange:(PHChange *)changeInstance {
// Check if there are changes to the assets we are showing.
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
if (collectionChanges == nil) {
return;
}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue(), ^{
// Get the new fetch result.
self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];
UICollectionView *collectionView = self.collectionView;
if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
// Reload the collection view if the incremental diffs are not available
[collectionView reloadData];
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
[collectionView performBatchUpdates:^{
NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
if ([removedIndexes count] > 0) {
[collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
if ([insertedIndexes count] > 0) {
[collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
if ([changedIndexes count] > 0) {
[collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
} completion:NULL];
}
[self resetCachedAssets];
});
}
3.1 PHPhotoLibrary
系统中PHPhotoLibrary单例对象 是用来维护用户照片库。当我们需要编辑资源对象元数据、资源内容、或者插入新的资源对象等,都可以借助通过PHPhotoLibrary单例对象执行block,block中创建我们指定的请求对象(比如PHAssetChangeRequest,PHAssetCollectionChangeRequest, PHCollectionListChangeRequest的对象)。photoLibraryDidChange(changeInfo: PHChange!)中进行.
3.2 协议PHPhotoLibraryChangeObserver
PHPhotoLibraryChangeObserver协议能让我们知道照片相册库中的改变。Photos会发送系统图片改变的消息,我们可以遵守PHPhotoLibraryChangeObserver协议,并通过 PHPhotoLibrary的registerChangeObserver方法将对象注册为观察者,时时接收照片改变的消息。
3.3 PHChange
PHphoto框架会提供给我们PHChange对象,我们可以调用changeDetailsForObject或者changeDetailsForFetchResult 方法,它返回给我们一个PHObjectChangeDetails对象,是对最新的照片实体对象的引用,可以告诉我们对象的图像数据是否曾变化过、对象是否曾被删除过。
4.请求图片和视频数据
4.1 PHImageManager
PHImageManager在框架中是个单例对象,用[PHImageManager defaultManager]
获取,它提供了加载图片和视频的方法。
比如:图像请求是通过 requestImageForAsset(...) 方法派发的。
这些方法的参数:
- 一个 PHAsset对象,
- PHImageRequestOptions 参数对象:可以设置返回图像的大小和图像的其它可选项
- 结果回调 (result handler)。
请求图片
- requestImageForAsset:targetSize:contentMode:options:resultHandler:
- requestImageDataForAsset:options:resultHandler:
请求视频
- requestPlayerItemForVideo:options:resultHandler:
- requestExportSessionForVideo:options:exportPreset:resultHandler:
- requestAVAssetForVideo:options:resultHandler:
默认情况下,这些API是异步执行,但是我们可以通过options参数中的synchronous属性,设置为同步执行,这些方法会阻塞当前调用线程直到下载完成或者发生错误。
4.2 请求图片
//加载 160X160 像素的图片
[manager requestImageForAsset:photo
targetSize:CGSizeMake(160,160)
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (result) {
[cell setImage:result];
} else {
... }
}];
targetSize
: 返回图片的尺寸
contentMode
:决定了照片应该以按比例缩放还是按比例填充的方式放到目标大小内。
options
: PHImageRequestOptions类用于定制请求。上面的方法返回指定尺寸的图像,如果你仅仅指定必要的参数而没有对 options 进行配置的话,返回的图像尺寸将会是原始图像的尺寸。或者,你指定的尺寸很小,这时候会按照你的要求来返回接近该尺寸的图像。
4.2.1 PHImageRequestOptions有以下几个重要的属性:
synchronous
: 指定请求是否同步执行。
resizeMode
: 对请求的图像怎样缩放。有三种选择:None,不缩放;Fast,尽快地提供接近或稍微大于要求的尺寸;Exact,精准提供要求的尺寸。
deliveryMode
: 图像质量。有三种值:Opportunistic,在速度与质量中均衡;HighQualityFormat,不管花费多长时间,提供高质量图像;FastFormat,以最快速度提供好的质量。
normalizedCropRect
: 用于对原始尺寸的图像进行裁剪,基于比例坐标。只在 resizeMode 为 Exact 时有效。
resizeMode 默认是 None,这也造成了返回图像尺寸与要求尺寸不符。这点需要注意。要返回一个指定尺寸的图像需要避免两层陷阱:一定要指定 options 参数,resizeMode 不能为 None。
PHImageManager.png
是否裁剪图片由 targetSize、contentMode来控制,裁剪的尺寸由targetSize、contentMode、resizeMode指定。
比如
// 下载原图:
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
options.resizeMode = PHImageRequestOptionsResizeModeExact;
options.progressHandler = (PHAssetImageProgressHandler)progress;
targetSize = PHImageManagerMaximumSize;
contentMode = PHImageContentModeDefault;
// 全屏图
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
options.resizeMode = PHImageRequestOptionsResizeModeExact;
options.progressHandler = (PHAssetImageProgressHandler)progress;
targetSize = UIScreen.mainScreen.bounds.size;
contentMode = PHImageContentModeAspectFit;
4.2.2 resultHandler可能被多次调用
见上图,
- 如果
synchronous
属性为YES,同步请求,resultHandler只会执行一次,并且返回高质量的图片。
Paste_Image.png
- 但是如果
synchronous
为NO,resultHandler是否会被多次调用取决于deliveryMode
属性:- 如果
deliveryMode
是.HighQualityFormat ,resultHandler调用一次,框架只返回高质量图。 - 如果
deliveryMode
是FastFormat,resultHandler也只被调用一次,保证速度尽可能保证图片质量。 deliveryMode
是.Opportunistic Photos 可能会先提供低质量的图像以供临时显示,随后会将指定尺寸的图像返回。如果指定尺寸的高质量的图像有缓存,那么直接提供高质量的图像。如图,第一次同步调用请求返回一张模糊的图,第二次再去下载一张高质量的图片:
- 如果
result_handler.png
resultHandler中的info 字典提供了关于当前请求状态的信息,比如:
- 图像是否必须从 iCloud 请求 (如果你初始化时将 networkAccessAllowed 设置成 false,那么就必须重新请求图像) —— PHImageResultIsInCloudKey 。
- 当前递送的 UIImage 是否是最终结果的低质量格式。当高质量图像正在下载时,这个可以让你给用户先展示一个预览图像 —— PHImageResultIsDegradedKey。
- 请求 ID (可以便捷的取消请求),以及请求是否已经被取消。 —— PHImageResultRequestIDKey 和 PHImageCancelledKey。
如果没有图像提供给 result handler,字典内还会有一个错误信息 —— PHImageErrorKey。
比如用-requestImageForAsset:targetSize:contentMode:options:resultHandler:
接口请求图片
// 入参:video对应的PHAsset对象
// info字典返回:
"PHImageResultRequestIDKey" : (int)1
"PHImageResultImageTypeKey" : (long)0
"PHImageResultDeliveredImageFormatKey" : (long)5005
"PHImageResultIsDegradedKey" : @"0"
注: 返回的image是video第一帧的截图
// 入参:图片对应的PHAsset对象
// info字典返回:
"PHImageFileOrientationKey" : (long)0
"PHImageResultWantedImageFormatKey" : (long)5005
"PHImageResultRequestIDKey" : (int)2
"PHImageResultDeliveredImageFormatKey" : (long)4031
"PHImageResultIsDegradedKey" : @"1"
4.3 请求视频
// 视频请求对象
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
// 最高质量的视频
options.deliveryMode = PHVideoRequestOptionsDeliveryModeHighQualityFormat;
// 可从iCloud中获取图片
options.networkAccessAllowed = YES;
// 如果是iCloud的视频,可以获取到下载进度
options.progressHandler = ^(double progress, NSError *error, BOOL *stop) {
[self updateUserVisibleProgress:progress error:error];
// 此回调block可能不在UI线程中,我们需要在UI线程中更新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
};
// 方法1
[manager requestPlayerItemForVideo:video options:options resultHandler:....]
// 方法2
[manager requestExportSessionForVideo:video options:options ...];
// 方法3
[manager requestAVAssetForVideo:video options:options resultHandler...];
5. 滚动性能:缓存
5.1 PHCachingImageManager
滚动一系列缩略图时,我们可以在可视区域前后维护一些数据缓存。如图:
cache.png
- 生成PHCachingImageManager实例
- 调用方法startCachingImagesForAssets:targetSize:contentMode:options:,并指定 目标尺寸target size,内容模式content mode,选项options参数
- 需要从asset对象中获取图片的时候,调用
requestImageForAsset:targetSize:contentMode:options:resultHandler
PHCachingImageManager *cim = [[PHCachingImageManager alloc] init];
NSArray *soonToBeVisibleAssets = [cim startCachingImagesForAssets:soonToBeVisibleAssets
targetSize:targetSize
contentMode:PHImageContentModeAspectFill
options:nil];
NSArray *previouslyVisibleAssets = [cim stopCachingImagesForAssets:previouslyVisibleAssets
targetSize:targetSize
contentMode:PHImageContentModeAspectFill
options:nil];
// 从缓存中读取相片
// Request an image for the asset from the PHCachingImageManager.
[self.imageManager requestImageForAsset:asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Set the cell's thumbnail image if it's still showing the same asset.
if ([cell.representedAssetIdentifier isEqualToString:asset.localIdentifier]) {
cell.thumbnailImage = result;
}
}];
官方demo中计算缓存区域的算法比较有意思
- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler {
if (CGRectIntersectsRect(newRect, oldRect)) {
CGFloat oldMaxY = CGRectGetMaxY(oldRect);
CGFloat oldMinY = CGRectGetMinY(oldRect);
CGFloat newMaxY = CGRectGetMaxY(newRect);
CGFloat newMinY = CGRectGetMinY(newRect);
if (newMaxY > oldMaxY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
addedHandler(rectToAdd);
}
if (oldMinY > newMinY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
addedHandler(rectToAdd);
}
if (newMaxY < oldMaxY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
removedHandler(rectToRemove);
}
if (oldMinY < newMinY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
removedHandler(rectToRemove);
}
} else {
addedHandler(newRect);
removedHandler(oldRect);
}
}