上一篇博客讲解了UIImagePickerController的使用,这篇博客简单说一下AssetsLibrary。在 iOS 8 出现之前,开发者只能使用 AssetsLibrary 框架来访问设备的照片库,而在 iOS8 出现之后,苹果提供了一个名为 PhotoKit 的框架。这里主要说AssetsLibrary。

另外值得强调的是,在 iOS 中,照片库并不只是照片的集合,同时也包含了视频。在 AssetsLibrary 中两者都有相同类型的对象去描述,只是类型不同而已。文中为了方便,大部分时候会使用「资源」代表 iOS 中的「照片和视频」。

AssetsLibrary 组成介绍

AssetsLibrary 的组成比较符合照片库本身的组成,照片库中的完整照片库对象、相册、相片都能在 AssetsLibrary 中找到一一对应的组成,这使到 AssetsLibrary 的使用变得直观而方便。

  • AssetsLibrary: 代表整个设备中的资源库(照片库),通过 AssetsLibrary 可以获取和包括设备中的照片和视频
  • ALAssetsGroup: 映射照片库中的一个相册,通过 ALAssetsGroup 可以获取某个相册的信息,相册下的资源,同时也可以对某个相册添加资源。
  • ALAsset: 映射照片库中的一个照片或视频,通过 ALAsset 可以获取某个照片或视频的详细信息,或者保存照片和视频。
  • ALAssetRepresentation: ALAssetRepresentation 是对 ALAsset 的封装(但不是其子类),可以更方便地获取 ALAsset 中的资源信息,每个 ALAsset 都有至少有一个 ALAssetRepresentation 对象,可以通过 defaultRepresentation 获取。而例如使用系统相机应用拍摄的 RAW + JPEG 照片,则会有两个 ALAssetRepresentation,一个封装了照片的 RAW 信息,另一个则封装了照片的 JPEG 信息。

AssetsLibrary 的基本使用

AssetsLibrary 的功能很多,基本可以分为对资源的获取/保存两个部分,保存的部分相对简单,API 也比较少,因此这里不作详细介绍。获取资源的 API 则比较丰富了,一个常见的使用大量 AssetsLibrary API 的例子就是图片选择器(ALAsset Picker)。要制作一个图片选择器,思路应该是获取照片库-列出所有相册-展示相册中的所有图片-预览图片大图。

首先是要检查 App 是否有照片操作授权:

NSString *tipTextWhenNoPhotosAuthorization; // 提示语
// 获取当前应用对照片的访问授权状态
ALAuthorizationStatus authorizationStatus = [ALAssetsLibrary authorizationStatus];
// 如果没有获取访问授权,或者访问授权状态已经被明确禁止,则显示提示语,引导用户开启授权
if (authorizationStatus == ALAuthorizationStatusRestricted || authorizationStatus == ALAuthorizationStatusDenied) {
    NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"];
    tipTextWhenNoPhotosAuthorization = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName];
    // 展示提示语
}

照片操作权限枚举类型:

typedef NS_ENUM(NSInteger, ALAuthorizationStatus) {
ALAuthorizationStatusNotDetermined = 0, 用户尚未做出了选择这个应用程序的问候
ALAuthorizationStatusRestricted,        此应用程序没有被授权访问的照片数据。可能是家长控制权限。
ALAuthorizationStatusDenied,            用户已经明确否认了这一照片数据的应用程序访问.
ALAuthorizationStatusAuthorized         用户已授权应用访问照片数据.
}

如果已经获取授权,则可以获取相册列表:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    _assetLibrary = [[ALAssetsLibrary alloc] init];
    _albumsArray = [NSMutableArray array];
    [_assetLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
        NSLog(@"%@",group);
        //查看相册的名字
        NSLog(@"ALAssetsGroupPropertyName:%@",[group valueForProperty:ALAssetsGroupPropertyName]);
        //查看相册的类型
        NSLog(@"ALAssetsGroupPropertyType:%@",[group valueForProperty:ALAssetsGroupPropertyType]);
        //查看相册的存储id
        NSLog(@"ALAssetsGroupPropertyPersistentID:%@",[group valueForProperty:ALAssetsGroupPropertyPersistentID]);
        //查看相册存储的位置地址
        NSLog(@"ALAssetsGroupPropertyURL:%@",[group valueForProperty:ALAssetsGroupPropertyURL]);
        //获取相册中一共的资源数量
        int count = [group numberOfAssets];
        //过滤器 设置相册组的筛选条件,ALAssetsFilter类表示筛选条件,allPhotos代表相册只包含相片,allVideos代表只包含视频,allAssets代表包含所有资源
        [group setAssetsFilter:[ALAssetsFilter allPhotos]];

        NSLog(@"count:%d",count);
        //获取封面图片
        CGImageRef poster = [group posterImage];
        NSLog(@"%@的封面照片是:%@",[group valueForProperty:ALAssetsGroupPropertyName],[UIImage imageWithCGImage:poster]);
        NSLog(@"====================");
        if (group) {
            if (group.numberOfAssets > 0) {
                [_albumsArray addObject:group];
            }
        }else {
            if ([_albumsArray count] > 0) {
                // 把所有的相册储存完毕,可以展示相册列表
            } else {
                // 没有任何有资源的相册,输出提示
            }
        }
    } failureBlock:^(NSError *error) {
        NSLog(@"读取失败");
    }];

}

通过上面的代码 我们可以获取到所有的相册组 ALAssetGroup 对象,打印所有的ALAssetGroup对象,得到以下信息:



首先看一下本地模拟器我创建的相册:



通过对比可以知道 ALAssetsLibrary 在遍历的时候会返回所有的ALAssetGroup对象,一些ALAssetGroup对象下其实并没有照片,所有,在这里会判断一下count是否为0。我们再来看看通过ALAssetsLibrary我们可以获取到相册的哪些信息。通多打印,可以知道,我们能获取相册组的组名,Type,count,PersistentID,URL,封面照片
这些信息足够在我们开发中使用了。

上面的代码中,遍历出所有的相册列表,并把相册中资源数不为空的相册 ALAssetGroup 对象的引用储存到一个数组中。总结成一下几点:

  • iOS 中允许相册为空,即相册中没有任何资源,如果不希望获取空相册,则需要像上面的代码中那样手动过滤
  • ALAssetsGroup 有一个 setAssetsFilter 的方法,可以传入一个过滤器,控制只获取相册中的照片或只获取视频。一旦设置过滤,ALAssetsGroup 中资源列表和资源数量的获取也会被自动更新。
  • 整个 AssetsLibrary 中对相册、资源的获取和保存都是使用异步处理(Asynchronous),这是考虑到资源文件体积相当比较大(还可能很大)。例如上面的遍历相册操作,相册的结果使用 block 输出,如果相册遍历完毕,则最后一次输出的 block 中的 group 参数值为 nil。而 stop 参数则是用于手工停止遍历,只要把 *stop 置 YES,则会停止下一次的遍历。关于这一点常常会引起误会,所以需要注意。

ALAsset介绍

现在,已经可以获取相册了,接下来是获取相册中的资源

_imagesAssetArray = [[NSMutableArray alloc] init];
[assetsGroup enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
    if (result) {
        //资源的类型
        NSLog(@"资源的类型:%@",[result valueForProperty:ALAssetPropertyType]);
        //资源地理位置(无位置信息返回null)
        NSLog(@"资源地理位置:%@",[result valueForProperty:ALAssetPropertyLocation]);
        //播放时长(照片返回ALErorInvalidProperty)
        NSLog(@"播放时长:%@",[result valueForProperty:ALAssetPropertyDuration]);
        //方向 共有8个方向,参见:ALAssetOrientation
        NSLog(@"方向:%@",[result valueForProperty:ALAssetPropertyOrientation]);
        //拍摄时间 包含了年与日时分秒
        NSLog(@"拍摄时间:%@",[result valueForProperty:ALAssetPropertyDate]);
        //描述(打印看了下,只有带后缀的名称)
        NSLog(@"描述:%@",[result valueForProperty:ALAssetPropertyRepresentations]);
        //返回一个字典,键值分别是文件名和文件的url
        NSLog(@"urlinfo:%@",[result valueForProperty:ALAssetPropertyURLs]);
        //文件的url
        NSLog(@"url:%@",[result valueForProperty:ALAssetPropertyAssetURL]);

        //原始资源。若没有保存修改后资源,则原始资源为nil
        NSLog(@"原始资源:%@",result.originalAsset);
        //指示资源是否可以编辑,只读属性
        NSLog(@"原始资源:%@",result.editable);
        //获取小正方形的缩略图
        NSLog(@"小正方形的缩略图:%@",[result thumbnail]);
        //按原始资源长宽比例的缩略图
        NSLog(@"按原始资源长宽比例的缩略图:%@",[result aspectRatioThumbnail]);
        NSLog(@"=========================");
        NSLog(@" ");


        [_imagesAssetArray addObject:result];
    } else {
        // result 为 nil,即遍历相片或视频完毕,可以展示资源列表
    }
}];

控制台输出信息:



跟遍历相册的过程类似,遍历相片也是使用一系列的异步方法,其中上面的方法所输出的 block 中,除了 result 参数表示资源信息,stop 用于手工停止遍历外,还提供了一个 index 参数,这个参数表示资源的索引。一般来说,展示资源列表都会使用缩略图(result.thumbnail),因此即使资源很多,遍历资源的速度也会相当快。但如果确实需要加载资源的高清图或者其他耗时的处理,则可以利用上面的 index 参数和 stop 参数做一个分段拉取资源。例如:

NSUInteger _targetIndex; // index 目标值,拉取资源直到这个值就手工停止拉取
NSUInteger _currentIndex; // 当前 index,每次拉取资源时从这个值开始

_targetIndex = 50;
_currentIndex = 0;

- (void)loadAssetWithAssetsGroup:(assetsGroup *)assetsGroup {
    [assetsGroup enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:_currentIndex] options:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
        _currentIndex = index;
        if (index > _targetIndex) {
            // 拉取资源的索引如果比目标值大,则停止拉取
            *stop = YES;
        } else {
            if (result) {
                [_imagesAssetArray addObject:result];
            } else {
                // result 为 nil,即遍历相片或视频完毕
            }
        }
    }];
}

// 之前拉取的数据已经显示完毕,需要展示新数据,重新调用 loadAssetWithAssetsGroup 方法,并根据需要更新 _targetIndex 的值

以上两种方法都能获取到ALAssetsGroup中的ALAsset,ALAsset代表一个资源对象 通多ALAsset就可以获取到资源的详细信息。

ALAssetRepresentation介绍

最后一步是获取图片详细信息,例如:

- (UIImage *)getImageWithAsset:(ALAsset *)asset {
    // 获取资源图片的详细资源信息,其中 imageAsset 是某个资源的 ALAsset 对象
    ALAssetRepresentation *representation = [asset defaultRepresentation];
    // 获取资源图片的 fullScreenImage
    UIImage *contentImage = [UIImage imageWithCGImage:[representation fullScreenImage]];

    return contentImage;
}

对于一个 ALAssetRepresentation,里面包含了图片的多个版本。最常用的是 fullResolutionImage 和 fullScreenImage。fullResolutionImage 是图片的原图,通过 fullResolutionImage 获取的图片没有任何处理,包括通过系统相册中“编辑”功能处理后的信息也没有被包含其中,因此需要展示“编辑”功能处理后的信息,使用 fullResolutionImage 就比较不方便,另外 fullResolutionImage 的拉取也会比较慢,在多张 fullResolutionImage 中切换时能明显感觉到图片的加载过程。因此这里建议获取图片的 fullScreenImage,它是图片的全屏图版本,这个版本包含了通过系统相册中“编辑”功能处理后的信息,同时也是一张缩略图,但图片的失真很少,缺点是图片的尺寸是一个适应屏幕大小的版本,因此展示图片时需要作出额外处理,但考虑到加载速度非常快的原因(在多张图片之间切换感受不到图片加载耗时),仍建议使用 fullScreenImage。另外ALAssetRepresentation还包含的其他的资源属性,例如:acale(长宽比例),filename(文件名字)等。

系统相册的处理过程大概也是如上,可以看出,在整个过程中并没有使用到图片的 fullResolutionImage,从相册列表展示到最终查看资源,都是使用缩略图,这也是 iOS 相册加载快的一个重要原因。

AssetsLibrary的写

创建相册组:

- (IBAction)createFileName:(id)sender {
    [self.assetLibrary addAssetsGroupAlbumWithName:@"rrrrr" resultBlock:^(ALAssetsGroup *group) {
        if (group) {
            //表明本地没有这个相册组
            NSLog(@"本地没有这个相册组");
        }else {
            //表明本地有这个相册组
            NSLog(@"本地有这个相册组");
        }
        NSLog(@"%@",group);
    } failureBlock:^(NSError *error) {

    }];
}

保存图片到指定相册组

- (IBAction)addImage:(id)sender {
    UIImage *image = [UIImage imageNamed:@"aaa"];
    //保存到系统照片 并保存到指定相册组
    __weak typeof (self)weakSelf = self;
    [self.assetLibrary writeImageToSavedPhotosAlbum:[image CGImage] orientation:ALAssetOrientationUp completionBlock:^(NSURL *assetURL, NSError *error) {

        [weakSelf.assetLibrary addAssetsGroupAlbumWithName:@"Www" resultBlock:^(ALAssetsGroup *group) {
            if (group) {
                //如果group不为空 表明新创建一个 ALAssetsGroup


                [weakSelf.assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) {
                    //添加资源
                    [group addAsset:asset];
                } failureBlock:^(NSError *error) {
                    NSLog(@"查询照片失败");
                }];
            }else {
                //如果group为空 表明系统内已经有一个 ALAssetsGroup
                [weakSelf.assetLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
                    NSString *groupName = [group valueForProperty:ALAssetsGroupPropertyName];
                    if ([groupName isEqualToString:@"Www"]) {
                        [weakSelf.assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) {
                            [group addAsset:asset];
                        } failureBlock:^(NSError *error) {
                            NSLog(@"查询照片失败");
                        }];
                    }
                } failureBlock:^(NSError *error) {

                }];
            }
            NSLog(@"%@",group);
        } failureBlock:^(NSError *error) {

        }];   
    }];
}

AssetsLibrary 的坑点

作为一套老框架,AssetsLibrary 不但有坑,而且还不少,除了上面提到的资源异步拉取时需要注意的事项,下面几点也是值得注意的:

1. AssetsLibrary 实例需要强引用

实例一个 AssetsLibrary 后,如上面所示,我们可以通过一系列枚举方法获取到需要的相册和资源,并把其储存到数组中,方便用于展示。但是,当我们把这些获取到的相册和资源储存到数组时,实际上只是在数组中储存了这些相册和资源在 AssetsLibrary 中的引用(指针),因而无论把相册和资源储存数组后如何利用这些数据,都首先需要确保 AssetsLibrary 没有被 ARC 释放,否则把数据从数组中取出来时,会发现对应的引用数据已经丢失(参见下图)。这一点较为容易被忽略,因此建议在使用 AssetsLibrary 的 viewController 中,把 AssetsLibrary 作为一个强持有的 property 或私有变量,避免在枚举出 AssetsLibrary 中所需要的数据后,AssetsLibrary 就被 ARC 释放了。

如下图:实例化一个 AssetsLibrary 的局部变量,枚举所有相册并储存在名为 _albumsArray 的数组中,展示相册时再次查看数组,发现 ALAssetsGroup 中的数据已经丢失。



2. AssetsLibrary 遵循写入优先原则

写入优先也就是說,在利用 AssetsLibrary 读取资源的过程中,有任何其它的进程(不一定是同一个 App)在保存资源时,就会收到 ALAssetsLibraryChangedNotification,让用户自行中断读取操作。最常见的就是读取 fullResolutionImage 时,用进程在写入,由于读取 fullResolutionImage 耗时较长,很容易就会 exception。

开启 Photo Stream 容易导致 exception

本质上,这跟上面的 AssetsLibrary 遵循写入优先原则是同一个问题。如果用户开启了共享照片流(Photo Stream),共享照片流会以 mstreamd 的方式“偷偷”执行,当有人把相片写入 Camera Roll 时,它就会自动保存到 Photo Stream Album 中,如果用户刚好在读取,那就跟上面说的一样产生 exception 了。由于共享照片流是用户决定是否要开启的,所以开发者无法改变,但是可以通过下面的接口在需要保护的时刻关闭监听共享照片流产生的频繁通知信息。

[ALAssetsLibrary disableSharedPhotoStreamsSupport];

下一篇博客会介绍iOS8以后出现的框架 PhotoKit 敬请关注!