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