一. 概要
在 iOS 設(shè)備中,照片和視頻是相當(dāng)重要的一部分。最近剛好在制作一個(gè)自定義的 iOS 圖片選擇器,順便整理一下 iOS 中對(duì)照片框架的使用方法。在 iOS 8 出現(xiàn)之前,開(kāi)發(fā)者只能使用 AssetsLibrary 框架來(lái)訪(fǎng)問(wèn)設(shè)備的照片庫(kù),這是一個(gè)有點(diǎn)跟不上 iOS 應(yīng)用發(fā)展步伐以及代碼設(shè)計(jì)原則但確實(shí)強(qiáng)大的框架,考慮到 iOS7 仍占有不少的滲透率,因此 AssetsLibrary 也是本文重點(diǎn)介紹的部分。而在 iOS8 出現(xiàn)之后,蘋(píng)果提供了一個(gè)名為 PhotoKit 的框架,一個(gè)可以讓?xiě)?yīng)用更好地與設(shè)備照片庫(kù)對(duì)接的框架,文末也會(huì)介紹一下這個(gè)框架。
另外值得強(qiáng)調(diào)的是,在 iOS 中,照片庫(kù)并不只是照片的集合,同時(shí)也包含了視頻。在 AssetsLibrary 中兩者都有相同類(lèi)型的對(duì)象去描述,只是類(lèi)型不同而已。文中為了方便,大部分時(shí)候會(huì)使用「資源」代表 iOS 中的「照片和視頻」。
二. AssetsLibrary 組成介紹
AssetsLibrary 的組成比較符合照片庫(kù)本身的組成,照片庫(kù)中的完整照片庫(kù)對(duì)象、相冊(cè)、相片都能在 AssetsLibrary 中找到一一對(duì)應(yīng)的組成,這使到 AssetsLibrary 的使用變得直觀而方便。
- AssetsLibrary: 代表整個(gè)設(shè)備中的資源庫(kù)(照片庫(kù)),通過(guò) AssetsLibrary 可以獲取和包括設(shè)備中的照片和視頻
- ALAssetsGroup: 映射照片庫(kù)中的一個(gè)相冊(cè),通過(guò) ALAssetsGroup 可以獲取某個(gè)相冊(cè)的信息,相冊(cè)下的資源,同時(shí)也可以對(duì)某個(gè)相冊(cè)添加資源。
- ALAsset: 映射照片庫(kù)中的一個(gè)照片或視頻,通過(guò) ALAsset 可以獲取某個(gè)照片或視頻的詳細(xì)信息,或者保存照片和視頻。
- ALAssetRepresentation: ALAssetRepresentation 是對(duì) ALAsset 的封裝(但不是其子類(lèi)),可以更方便地獲取 ALAsset 中的資源信息,每個(gè) ALAsset 都有至少有一個(gè) ALAssetRepresentation 對(duì)象,可以通過(guò) defaultRepresentation 獲取。而例如使用系統(tǒng)相機(jī)應(yīng)用拍攝的 RAW + JPEG 照片,則會(huì)有兩個(gè) ALAssetRepresentation,一個(gè)封裝了照片的 RAW 信息,另一個(gè)則封裝了照片的 JPEG 信息。
三. AssetsLibrary 的基本使用
AssetsLibrary 的功能很多,基本可以分為對(duì)資源的獲取/保存兩個(gè)部分,保存的部分相對(duì)簡(jiǎn)單,API 也比較少,因此這里不作詳細(xì)介紹。獲取資源的 API 則比較豐富了,一個(gè)常見(jiàn)的使用大量 AssetsLibrary API 的例子就是圖片選擇器(ALAsset Picker)。要制作一個(gè)圖片選擇器,思路應(yīng)該是獲取照片庫(kù)-列出所有相冊(cè)-展示相冊(cè)中的所有圖片-預(yù)覽圖片大圖。
首先是要檢查 App 是否有照片操作授權(quán):
1 2 3 4 5 6 7 8 9 10 | NSString *tipTextWhenNoPhotosAuthorization; // 提示語(yǔ)
// 獲取當(dāng)前應(yīng)用對(duì)照片的訪(fǎng)問(wèn)授權(quán)狀態(tài)
ALAuthorizationStatus authorizationStatus = [ALAssetsLibrary authorizationStatus];
// 如果沒(méi)有獲取訪(fǎng)問(wèn)授權(quán),或者訪(fǎng)問(wèn)授權(quán)狀態(tài)已經(jīng)被明確禁止,則顯示提示語(yǔ),引導(dǎo)用戶(hù)開(kāi)啟授權(quán)
if (authorizationStatus == ALAuthorizationStatusRestricted || authorizationStatus == ALAuthorizationStatusDenied) {
NSDictionary *mainInfoDictionary = [[ NSBundle mainBundle] infoDictionary];
NSString *appName = [mainInfoDictionary objectForKey: @"CFBundleDisplayName" ];
tipTextWhenNoPhotosAuthorization = [ NSString stringWithFormat: @"請(qǐng)?jiān)谠O(shè)備的\"設(shè)置-隱私-照片\"選項(xiàng)中,允許%@訪(fǎng)問(wèn)你的手機(jī)相冊(cè)" , appName];
// 展示提示語(yǔ)
}
|
如果已經(jīng)獲取授權(quán),則可以獲取相冊(cè)列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | _assetsLibrary = [[ALAssetsLibrary alloc] init];
_albumsArray = [[ NSMutableArray alloc] init];
[_assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
if (group.numberOfAssets > 0) {
// 把相冊(cè)儲(chǔ)存到數(shù)組中,方便后面展示相冊(cè)時(shí)使用
[_albumsArray addObject:group];
}
} else {
if ([_albumsArray count] > 0) {
// 把所有的相冊(cè)儲(chǔ)存完畢,可以展示相冊(cè)列表
} else {
// 沒(méi)有任何有資源的相冊(cè),輸出提示
}
}
} failureBlock:^( NSError *error) {
NSLog ( @"Asset group not found!\n" );
}];
|
上面的代碼中,遍歷出所有的相冊(cè)列表,并把相冊(cè)中資源數(shù)不為空的相冊(cè) ALAssetGroup 對(duì)象的引用儲(chǔ)存到一個(gè)數(shù)組中。這里需要強(qiáng)調(diào)幾點(diǎn):
- iOS 中允許相冊(cè)為空,即相冊(cè)中沒(méi)有任何資源,如果不希望獲取空相冊(cè),則需要像上面的代碼中那樣手動(dòng)過(guò)濾
- ALAssetsGroup 有一個(gè) setAssetsFilter 的方法,可以傳入一個(gè)過(guò)濾器,控制只獲取相冊(cè)中的照片或只獲取視頻。一旦設(shè)置過(guò)濾,ALAssetsGroup 中資源列表和資源數(shù)量的獲取也會(huì)被自動(dòng)更新。
- 整個(gè) AssetsLibrary 中對(duì)相冊(cè)、資源的獲取和保存都是使用異步處理(Asynchronous),這是考慮到資源文件體積相當(dāng)比較大(還可能很大)。例如上面的遍歷相冊(cè)操作,相冊(cè)的結(jié)果使用 block 輸出,如果相冊(cè)遍歷完畢,則最后一次輸出的 block 中的 group 參數(shù)值為 nil。而 stop 參數(shù)則是用于手工停止遍歷,只要把 *stop 置 YES,則會(huì)停止下一次的遍歷。關(guān)于這一點(diǎn)常常會(huì)引起誤會(huì),所以需要注意。
現(xiàn)在,已經(jīng)可以獲取相冊(cè)了,接下來(lái)是獲取相冊(cè)中的資源:
1 2 3 4 5 6 7 8 | _imagesAssetArray = [[ NSMutableArray alloc] init];
[assetsGroup enumerateAssetsWithOptions: NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// result 為 nil,即遍歷相片或視頻完畢,可以展示資源列表
}
}];
|
跟遍歷相冊(cè)的過(guò)程類(lèi)似,遍歷相片也是使用一系列的異步方法,其中上面的方法所輸出的 block 中,除了 result 參數(shù)表示資源信息,stop 用于手工停止遍歷外,還提供了一個(gè) index 參數(shù),這個(gè)參數(shù)表示資源的索引。一般來(lái)說(shuō),展示資源列表都會(huì)使用縮略圖(result.thumbnail),因此即使資源很多,遍歷資源的速度也會(huì)相當(dāng)快。但如果確實(shí)需要加載資源的高清圖或者其他耗時(shí)的處理,則可以利用上面的 index 參數(shù)和 stop 參數(shù)做一個(gè)分段拉取資源。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | NSUInteger _targetIndex; // index 目標(biāo)值,拉取資源直到這個(gè)值就手工停止拉取
NSUInteger _currentIndex; // 當(dāng)前 index,每次拉取資源時(shí)從這個(gè)值開(kāi)始
_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) {
// 拉取資源的索引如果比目標(biāo)值大,則停止拉取
*stop = YES ;
} else {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// result 為 nil,即遍歷相片或視頻完畢
}
}
}];
}
// 之前拉取的數(shù)據(jù)已經(jīng)顯示完畢,需要展示新數(shù)據(jù),重新調(diào)用 loadAssetWithAssetsGroup 方法,并根據(jù)需要更新 _targetIndex 的值
|
最后一步是獲取圖片詳細(xì)信息,例如:
1 2 3 4 | // 獲取資源圖片的詳細(xì)資源信息,其中 imageAsset 是某個(gè)資源的 ALAsset 對(duì)象
ALAssetRepresentation *representation = [imageAsset defaultRepresentation];
// 獲取資源圖片的 fullScreenImage
UIImage *contentImage = [UIImage imageWithCGImage:[representation fullScreenImage]];
|
對(duì)于一個(gè) ALAssetRepresentation,里面包含了圖片的多個(gè)版本。最常用的是 fullResolutionImage 和 fullScreenImage。fullResolutionImage 是圖片的原圖,通過(guò) fullResolutionImage 獲取的圖片沒(méi)有任何處理,包括通過(guò)系統(tǒng)相冊(cè)中“編輯”功能處理后的信息也沒(méi)有被包含其中,因此需要展示“編輯”功能處理后的信息,使用 fullResolutionImage 就比較不方便,另外 fullResolutionImage 的拉取也會(huì)比較慢,在多張 fullResolutionImage 中切換時(shí)能明顯感覺(jué)到圖片的加載過(guò)程。因此這里建議獲取圖片的 fullScreenImage,它是圖片的全屏圖版本,這個(gè)版本包含了通過(guò)系統(tǒng)相冊(cè)中“編輯”功能處理后的信息,同時(shí)也是一張縮略圖,但圖片的失真很少,缺點(diǎn)是圖片的尺寸是一個(gè)適應(yīng)屏幕大小的版本,因此展示圖片時(shí)需要作出額外處理,但考慮到加載速度非??斓脑颍ㄔ诙鄰垐D片之間切換感受不到圖片加載耗時(shí)),仍建議使用 fullScreenImage。
系統(tǒng)相冊(cè)的處理過(guò)程大概也是如上,可以看出,在整個(gè)過(guò)程中并沒(méi)有使用到圖片的 fullResolutionImage,從相冊(cè)列表展示到最終查看資源,都是使用縮略圖,這也是 iOS 相冊(cè)加載快的一個(gè)重要原因。
三. AssetsLibrary 的坑點(diǎn)
作為一套老框架,AssetsLibrary 不但有坑,而且還不少,除了上面提到的資源異步拉取時(shí)需要注意的事項(xiàng),下面幾點(diǎn)也是值得注意的:
1. AssetsLibrary 實(shí)例需要強(qiáng)引用
實(shí)例一個(gè) AssetsLibrary 后,如上面所示,我們可以通過(guò)一系列枚舉方法獲取到需要的相冊(cè)和資源,并把其儲(chǔ)存到數(shù)組中,方便用于展示。但是,當(dāng)我們把這些獲取到的相冊(cè)和資源儲(chǔ)存到數(shù)組時(shí),實(shí)際上只是在數(shù)組中儲(chǔ)存了這些相冊(cè)和資源在 AssetsLibrary 中的引用(指針),因而無(wú)論把相冊(cè)和資源儲(chǔ)存數(shù)組后如何利用這些數(shù)據(jù),都首先需要確保 AssetsLibrary 沒(méi)有被 ARC 釋放,否則把數(shù)據(jù)從數(shù)組中取出來(lái)時(shí),會(huì)發(fā)現(xiàn)對(duì)應(yīng)的引用數(shù)據(jù)已經(jīng)丟失(參見(jiàn)下圖)。這一點(diǎn)較為容易被忽略,因此建議在使用 AssetsLibrary 的 viewController 中,把 AssetsLibrary 作為一個(gè)強(qiáng)持有的 property 或私有變量,避免在枚舉出 AssetsLibrary 中所需要的數(shù)據(jù)后,AssetsLibrary 就被 ARC 釋放了。
如下圖:實(shí)例化一個(gè) AssetsLibrary 的局部變量,枚舉所有相冊(cè)并儲(chǔ)存在名為 _albumsArray 的數(shù)組中,展示相冊(cè)時(shí)再次查看數(shù)組,發(fā)現(xiàn) ALAssetsGroup 中的數(shù)據(jù)已經(jīng)丟失。
2. AssetsLibrary 遵循寫(xiě)入優(yōu)先原則
寫(xiě)入優(yōu)先也就是說(shuō),在利用 AssetsLibrary 讀取資源的過(guò)程中,有任何其它的進(jìn)程(不一定是同一個(gè) App)在保存資源時(shí),就會(huì)收到 ALAssetsLibraryChangedNotification,讓用戶(hù)自行中斷讀取操作。最常見(jiàn)的就是讀取 fullResolutionImage 時(shí),用進(jìn)程在寫(xiě)入,由于讀取 fullResolutionImage 耗時(shí)較長(zhǎng),很容易就會(huì) exception。
3. 開(kāi)啟 Photo Stream 容易導(dǎo)致 exception
本質(zhì)上,這跟上面的 AssetsLibrary 遵循寫(xiě)入優(yōu)先原則是同一個(gè)問(wèn)題。如果用戶(hù)開(kāi)啟了共享照片流(Photo Stream),共享照片流會(huì)以 mstreamd 的方式“偷偷”執(zhí)行,當(dāng)有人把相片寫(xiě)入 Camera Roll 時(shí),它就會(huì)自動(dòng)保存到 Photo Stream Album 中,如果用戶(hù)剛好在讀取,那就跟上面說(shuō)的一樣產(chǎn)生 exception 了。由于共享照片流是用戶(hù)決定是否要開(kāi)啟的,所以開(kāi)發(fā)者無(wú)法改變,但是可以通過(guò)下面的接口在需要保護(hù)的時(shí)刻關(guān)閉監(jiān)聽(tīng)共享照片流產(chǎn)生的頻繁通知信息。
1 | [ALAssetsLibrary disableSharedPhotoStreamsSupport];
|
四. PhotoKit 簡(jiǎn)介
PhotoKit 是一套比 AssetsLibrary 更完整也更高效的庫(kù),對(duì)資源的處理跟 AssetsLibrary 也有很大的不同。
首先簡(jiǎn)單介紹幾個(gè)概念:
- PHAsset: 代表照片庫(kù)中的一個(gè)資源,跟 ALAsset 類(lèi)似,通過(guò) PHAsset 可以獲取和保存資源
- PHFetchOptions: 獲取資源時(shí)的參數(shù),可以傳 nil,即使用系統(tǒng)默認(rèn)值
- PHFetchResult: 表示一系列的資源集合,也可以是相冊(cè)的集合
- PHAssetCollection: 表示一個(gè)相冊(cè)或者一個(gè)時(shí)刻,或者是一個(gè)「智能相冊(cè)(系統(tǒng)提供的特定的一系列相冊(cè),例如:最近刪除,視頻列表,收藏等等,如下圖所示)
- PHImageManager: 用于處理資源的加載,加載圖片的過(guò)程帶有緩存處理,可以通過(guò)傳入一個(gè) PHImageRequestOptions 控制資源的輸出尺寸等規(guī)格
- PHImageRequestOptions: 如上面所說(shuō),控制加載圖片時(shí)的一系列參數(shù)
下圖中 UITableView 的第二個(gè) section 就是 PhotoKit 所列出的所有智能相冊(cè)
再列出幾個(gè)代碼片段,展示如何獲取相冊(cè)以及某個(gè)相冊(cè)下資源的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 列出所有相冊(cè)智能相冊(cè)
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options: nil ];
// 列出所有用戶(hù)創(chuàng)建的相冊(cè)
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions: nil ];
// 獲取所有資源的集合,并按資源的創(chuàng)建時(shí)間排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[ NSSortDescriptor sortDescriptorWithKey: @"creationDate" ascending: YES ]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 在資源的集合中獲取第一個(gè)集合,并獲取其中的圖片
PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
PHAsset *asset = assetsFetchResults[0];
[imageManager requestImageForAsset:asset
targetSize:SomeSize
contentMode:PHImageContentModeAspectFill
options: nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// 得到一張 UIImage,展示到界面上
}];
|
結(jié)合上面幾個(gè)代碼片段上看,PhotoKit 相對(duì) AssetsLibrary 主要有三點(diǎn)重要的改進(jìn):
- 從 AssetsLibrary 中獲取數(shù)據(jù),無(wú)論是相冊(cè),還是資源,本質(zhì)上都是使用枚舉的方式,遍歷照片庫(kù)取得相應(yīng)的數(shù)據(jù)。而 PhotoKit 則是通過(guò)傳入?yún)?shù),直接獲取相應(yīng)的數(shù)據(jù),因而效率會(huì)提高不少。
- 在 AssetsLibrary 中,相冊(cè)和資源是對(duì)應(yīng)不同的對(duì)象(ALAssetGroup 和 ALAsset),因此獲取相冊(cè)和獲取資源是兩個(gè)完全沒(méi)有關(guān)聯(lián)的接口。而 PhotoKit 中則有 PHFetchResult 這個(gè)可以統(tǒng)一儲(chǔ)存相冊(cè)或資源的對(duì)象,因此處理相冊(cè)和資源時(shí)也會(huì)比較方便。
- PhotoKit 返回資源結(jié)果時(shí),同時(shí)返回了資源的元數(shù)據(jù),獲取元數(shù)據(jù)在 AssetsLibrary 中是很難辦到的一件事。同時(shí)通過(guò) PHAsset,開(kāi)發(fā)者還能直接獲取資源是否被收藏(favorite)和隱藏(hidden),拍攝圖片時(shí)是否開(kāi)啟了 HDR 或全景模式,甚至能通過(guò)一張連拍圖片獲取到連拍圖片中的其他圖片。這也是文章開(kāi)頭說(shuō)的,PhotoKit 能更好地與設(shè)備照片庫(kù)接入的一個(gè)重要因素。
關(guān)于 PhotoKit,建議可以參考 Apple 的 Example app using Photos framework
系列文章:
iOS 開(kāi)發(fā)之照片框架詳解
iOS 開(kāi)發(fā)之照片框架詳解之二 —— PhotoKit 詳解(上)
iOS 開(kāi)發(fā)之照片框架詳解之二 —— PhotoKit 詳解(下)
參考資料:
objc中國(guó) - 照片框架
Example app using Photos framework
AssetsLibrary Framework Reference
本文由 Kayo Lee 發(fā)表,本文鏈接:http:///ios-development-and-detail-of-photo-framework.html
|