iOS 開發之照片框架詳解

weixin_33896726發表於2016-06-19

 

一. 概要

在 iOS 裝置中,照片和視訊是相當重要的一部分。最近剛好在製作一個自定義的 iOS 圖片選擇器,順便整理一下 iOS 中對照片框架的使用方法。在 iOS 8 出現之前,開發者只能使用 AssetsLibrary 框架來訪問裝置的照片庫,這是一個有點跟不上 iOS 應用發展步伐以及程式碼設計原則但確實強大的框架,考慮到 iOS7 仍佔有不少的滲透率,因此 AssetsLibrary 也是本文重點介紹的部分。而在 iOS8 出現之後,蘋果提供了一個名為 PhotoKit 的框架,一個可以讓應用更好地與裝置照片庫對接的框架,文末也會介紹一下這個框架。

另外值得強調的是,在 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 是否有照片操作授權:

1
2
3
4
5
6
7
8
9
10
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];
    // 展示提示語
}

如果已經獲取授權,則可以獲取相簿列表:

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) {
            // 把相簿儲存到陣列中,方便後面展示相簿時使用
            [_albumsArray addObject:group];
        }
    } else {
        if ([_albumsArray count] > 0) {
            // 把所有的相簿儲存完畢,可以展示相簿列表
        } else {
            // 沒有任何有資源的相簿,輸出提示
        }
    }
} failureBlock:^(NSError *error) {
    NSLog(@"Asset group not found!\n");
}];

上面的程式碼中,遍歷出所有的相簿列表,並把相簿中資源數不為空的相簿 ALAssetGroup 物件的引用儲存到一個陣列中。這裡需要強調幾點:

  • iOS 中允許相簿為空,即相簿中沒有任何資源,如果不希望獲取空相簿,則需要像上面的程式碼中那樣手動過濾
  • ALAssetsGroup 有一個 setAssetsFilter 的方法,可以傳入一個過濾器,控制只獲取相簿中的照片或只獲取視訊。一旦設定過濾,ALAssetsGroup 中資源列表和資源數量的獲取也會被自動更新。
  • 整個 AssetsLibrary 中對相簿、資源的獲取和儲存都是使用非同步處理(Asynchronous),這是考慮到資原始檔體積相當比較大(還可能很大)。例如上面的遍歷相簿操作,相簿的結果使用 block 輸出,如果相簿遍歷完畢,則最後一次輸出的 block 中的 group 引數值為 nil。而 stop 引數則是用於手工停止遍歷,只要把 *stop 置 YES,則會停止下一次的遍歷。關於這一點常常會引起誤會,所以需要注意。

現在,已經可以獲取相簿了,接下來是獲取相簿中的資源:

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 參數列示資源資訊,stop 用於手工停止遍歷外,還提供了一個 index 引數,這個參數列示資源的索引。一般來說,展示資源列表都會使用縮圖(result.thumbnail),因此即使資源很多,遍歷資源的速度也會相當快。但如果確實需要載入資源的高清圖或者其他耗時的處理,則可以利用上面的 index 引數和 stop 引數做一個分段拉取資源。例如:

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 目標值,拉取資源直到這個值就手工停止拉取
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 的值

最後一步是獲取圖片詳細資訊,例如:

1
2
3
4
// 獲取資源圖片的詳細資源資訊,其中 imageAsset 是某個資源的 ALAsset 物件
ALAssetRepresentation *representation = [imageAsset defaultRepresentation];
// 獲取資源圖片的 fullScreenImage
UIImage *contentImage = [UIImage imageWithCGImage:[representation fullScreenImage]];

對於一個 ALAssetRepresentation,裡面包含了圖片的多個版本。最常用的是 fullResolutionImage 和 fullScreenImage。fullResolutionImage 是圖片的原圖,通過 fullResolutionImage 獲取的圖片沒有任何處理,包括通過系統相簿中“編輯”功能處理後的資訊也沒有被包含其中,因此需要展示“編輯”功能處理後的資訊,使用 fullResolutionImage 就比較不方便,另外 fullResolutionImage 的拉取也會比較慢,在多張 fullResolutionImage 中切換時能明顯感覺到圖片的載入過程。因此這裡建議獲取圖片的 fullScreenImage,它是圖片的全屏圖版本,這個版本包含了通過系統相簿中“編輯”功能處理後的資訊,同時也是一張縮圖,但圖片的失真很少,缺點是圖片的尺寸是一個適應螢幕大小的版本,因此展示圖片時需要作出額外處理,但考慮到載入速度非常快的原因(在多張圖片之間切換感受不到圖片載入耗時),仍建議使用 fullScreenImage。

系統相簿的處理過程大概也是如上,可以看出,在整個過程中並沒有使用到圖片的 fullResolutionImage,從相簿列表展示到最終檢視資源,都是使用縮圖,這也是 iOS 相簿載入快的一個重要原因。

三. AssetsLibrary 的坑點

作為一套老框架,AssetsLibrary 不但有坑,而且還不少,除了上面提到的資源非同步拉取時需要注意的事項,下面幾點也是值得注意的:

1. AssetsLibrary 例項需要強引用

例項一個 AssetsLibrary 後,如上面所示,我們可以通過一系列列舉方法獲取到需要的相簿和資源,並把其儲存到陣列中,方便用於展示。但是,當我們把這些獲取到的相簿和資源儲存到陣列時,實際上只是在陣列中儲存了這些相簿和資源在 AssetsLibrary 中的引用(指標),因而無論把相簿和資源儲存陣列後如何利用這些資料,都首先需要確保 AssetsLibrary 沒有被 ARC 釋放,否則把資料從陣列中取出來時,會發現對應的引用資料已經丟失(參見下圖)。這一點較為容易被忽略,因此建議在使用 AssetsLibrary 的 viewController 中,把 AssetsLibrary 作為一個強持有的 property 或私有變數,避免在列舉出 AssetsLibrary 中所需要的資料後,AssetsLibrary 就被 ARC 釋放了。

如下圖:例項化一個 AssetsLibrary 的區域性變數,列舉所有相簿並儲存在名為 _albumsArray 的陣列中,展示相簿時再次檢視陣列,發現 ALAssetsGroup 中的資料已經丟失。

ALAssetsLibrary_release

2. AssetsLibrary 遵循寫入優先原則

寫入優先也就是說,在利用 AssetsLibrary 讀取資源的過程中,有任何其它的程式(不一定是同一個 App)在儲存資源時,就會收到 ALAssetsLibraryChangedNotification,讓使用者自行中斷讀取操作。最常見的就是讀取 fullResolutionImage 時,用程式在寫入,由於讀取 fullResolutionImage 耗時較長,很容易就會 exception。

3. 開啟 Photo Stream 容易導致 exception

本質上,這跟上面的 AssetsLibrary 遵循寫入優先原則是同一個問題。如果使用者開啟了共享照片流(Photo Stream),共享照片流會以 mstreamd 的方式“偷偷”執行,當有人把相片寫入 Camera Roll 時,它就會自動儲存到 Photo Stream Album 中,如果使用者剛好在讀取,那就跟上面說的一樣產生 exception 了。由於共享照片流是使用者決定是否要開啟的,所以開發者無法改變,但是可以通過下面的介面在需要保護的時刻關閉監聽共享照片流產生的頻繁通知資訊。

1
[ALAssetsLibrary disableSharedPhotoStreamsSupport];

四. PhotoKit 簡介

PhotoKit 是一套比 AssetsLibrary 更完整也更高效的庫,對資源的處理跟 AssetsLibrary 也有很大的不同。

首先簡單介紹幾個概念:

  • PHAsset: 代表照片庫中的一個資源,跟 ALAsset 類似,通過 PHAsset 可以獲取和儲存資源
  • PHFetchOptions: 獲取資源時的引數,可以傳 nil,即使用系統預設值
  • PHFetchResult: 表示一系列的資源集合,也可以是相簿的集合
  • PHAssetCollection: 表示一個相簿或者一個時刻,或者是一個「智慧相簿(系統提供的特定的一系列相簿,例如:最近刪除,視訊列表,收藏等等,如下圖所示)
  • PHImageManager: 用於處理資源的載入,載入圖片的過程帶有快取處理,可以通過傳入一個 PHImageRequestOptions 控制資源的輸出尺寸等規格
  • PHImageRequestOptions: 如上面所說,控制載入圖片時的一系列引數

下圖中 UITableView 的第二個 section 就是 PhotoKit 所列出的所有智慧相簿

photokit-album-list

再列出幾個程式碼片段,展示如何獲取相簿以及某個相簿下資源的程式碼:

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];
 
// 列出所有使用者建立的相簿
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
 
// 獲取所有資源的集合,並按資源的建立時間排序
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,展示到介面上
                           
                      }];

結合上面幾個程式碼片段上看,PhotoKit 相對 AssetsLibrary 主要有三點重要的改進:

  • 從 AssetsLibrary 中獲取資料,無論是相簿,還是資源,本質上都是使用列舉的方式,遍歷照片庫取得相應的資料。而 PhotoKit 則是通過傳入引數,直接獲取相應的資料,因而效率會提高不少。
  • 在 AssetsLibrary 中,相簿和資源是對應不同的物件(ALAssetGroup 和 ALAsset),因此獲取相簿和獲取資源是兩個完全沒有關聯的介面。而 PhotoKit 中則有 PHFetchResult 這個可以統一儲存相簿或資源的物件,因此處理相簿和資源時也會比較方便。
  • PhotoKit 返回資源結果時,同時返回了資源的後設資料,獲取後設資料在 AssetsLibrary 中是很難辦到的一件事。同時通過 PHAsset,開發者還能直接獲取資源是否被收藏(favorite)和隱藏(hidden),拍攝圖片時是否開啟了 HDR 或全景模式,甚至能通過一張連拍圖片獲取到連拍圖片中的其他圖片。這也是文章開頭說的,PhotoKit 能更好地與裝置照片庫接入的一個重要因素。

相關文章