一. 概況
本文側重介紹在前文中簡單介紹過的 PhotoKit 及其與 ALAssetLibrary 的差異,以及如何基於 PhotoKit 與 AlAssetLibrary 封裝出通用的方法。
這裡引用一下前文中對 PhotoKit 基本構成的介紹:
PHAsset: 代表照片庫中的一個資源,跟 ALAsset 類似,通過 PHAsset 可以獲取和保存資源
PHFetchOptions: 獲取資源時的參數,可以傳 nil,即使用系統默認值
PHAssetCollection: PHCollection 的子類,表示一個相冊或者一個時刻,或者是一個「智能相冊(系統提供的特定的一系列相冊,例如:最近刪除,視頻列表,收藏等等,如下圖所示)
PHFetchResult: 表示一系列的資源結果集合,也可以是相冊的集合,從 PHCollection 的類方法中獲得
PHImageManager: 用於處理資源的加載,加載圖片的過程帶有緩存處理,可以通過傳入一個 PHImageRequestOptions 控制資源的輸出尺寸等規格
PHImageRequestOptions: 如上面所說,控制加載圖片時的一系列參數
這裡還有一個額外的概念 PHCollectionList,表示一組 PHCollection,它本身也是一個 PHCollection,因此 PHCollection 作為一個集合,可以包含其他集合,這使到 PhotoKit 的組成比 ALAssetLibrary 要復雜一些。另外與 ALAssetLibrary 相似,一個 PHAsset 可以同時屬於多個不同的 PHAssetCollection,最常見的例子就是剛剛拍攝的照片,至少同時屬於“最近添加”、“相機膠卷”以及“照片 - 精選”這三個 PHAssetCollection。關於這幾個概念的關系如下圖:
二. PhotoKit 的機制
獲取資源
// 列出所有相冊智能相冊
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];
如前面提到過的那樣,從 PHAssetCollection 獲取中獲取到的可以是相冊也可以是資源,但無論是哪種內容,都統一使用 PHFetchResult 對象封裝起來,因此雖然 PHAssetCollection 獲取到的結果可能是多樣的,但通過 PHFetchResult 就可以使用統一的方法去處理這些內容(即遍歷 PHFetchResult)。例如擴展上面的例子:
// 列出所有相冊智能相冊
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 這時 smartAlbums 中保存的應該是各個智能相冊對應的 PHAssetCollection
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 獲取一個相冊(PHAssetCollection)
PHCollection *collection = fetchResult[i];
if ([collection isKindOfClass:[PHAssetCollection class]]) {
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
// 從每一個智能相冊中獲取到的 PHFetchResult 中包含的才是真正的資源(PHAsset)
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];
else {
NSAssert(NO, @"Fetch collection not PHCollection: %@", collection);
}
}
// 獲取所有資源的集合,並按資源的創建時間排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 這時 assetsFetchResults 中包含的,應該就是各個資源(PHAsset)
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 獲取一個資源(PHAsset)
PHAsset *asset = fetchResult[i];
}
獲取圖像的方式與坑點
- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset
targetSize:(CGSize)targetSize
contentMode:(PHImageContentMode)contentMode
options:(nullable PHImageRequestOptions *)options
resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler;
這個方法中的參數坑點不少,下面逐個參數列舉一下其作用及坑點:(1)PHImageRequestOptions 與 iCloud 照片庫
PHImageRequestOptions 中包含了一系列控制請求圖像的屬性。
resizeMode 屬性控制圖像的剪裁,不知道為什麼 PhotoKit 會在請求圖像方法(requestImageForAsset)中已經有控制圖像剪裁的參數後(contentMode),還在 options 中加入控制剪裁的屬性,但如果兩個地方所控制的剪裁結果有所沖突,PhotoKit 會以 resizeMode 的結果為准。另外,resizeMode 也有控制圖像質量的作用。如 resizeMode 設置為 PHImageRequestOptionsResizeModeExact 則返回圖像必須和目標大小相匹配,並且圖像質量也為高質量圖像,而設置為 PHImageRequestOptionsResizeModeFast 則請求的效率更高,但返回的圖像可能和目標大小不一樣並且質量較低。
在 PhotoKit 中,對 iCloud 照片庫有很好的支持,如果用戶開啟了 iCloud 照片庫,並且選擇了“優化 iPhone/iPad 儲存空間”,或者選擇了“下載並保留原件”但原件還沒有加載好的時候,PhotoKit 也會預先拿到這些非本地圖像的 PHAsset,但是由於本地並沒有原圖,所以如果產生了請求高清圖的請求,PHotoKit 會嘗試從 iCloud 下載圖片,而這個行為最終的表現,會被 PHImageRequestOptions 中的值所影響。PHImageRequestOptions 中常常會用的幾個屬性如下:
networkAccessAllowed 參數控制是否允許網絡請求,默認為 NO,如果不允許網絡請求,那麼就沒有然後了,當然也拉取不到 iCloud 的圖像原件。deliveryMode 則用於控制請求的圖片質量。synchronous 控制是否為同步請求,默認為 NO,如果 synchronous 為 YES,即同步請求時,deliveryMode 會被視為 PHImageRequestOptionsDeliveryModeHighQualityFormat,即自動返回高質量的圖片,因此不建議使用同步請求,否則如果界面需要等待返回的圖像才能進一步作出反應,則反應時長會很長。
還有一個與 iCloud 密切相關的屬性 progressHandler,當圖像需要從 iCloud 下載時,這個 block 會被自動調用,block 中會返回圖像下載的進度,圖像的信息,出錯信息。開發者可以利用這些信息反饋給用戶當前圖像的下載進度以及狀況,但需要注意 progressHandler 不在主線程上執行,因此在其中需要操作 UI,則需要手工放到主線程執行。
上面有提到,requestImageForAsset 中的參數 resultHandler 可能會被多次調用,這種情況就是圖像需要從 iCloud 中下載的情況。在 requestImageForAsset 返回的內容中,一開始的那一次請求中會返回一個小尺寸的圖像版本,當高清圖像還在下載時,開發者可以首先給用戶展示這個低清的圖像版本,然後 block 在多次調用後,最終會返回高清的原圖。至於當前返回的圖像是哪個版本的圖像,可以通過 block 返回的 NSDictionary info 中獲知,PHImageResultIsDegradedKey 表示當前返回的 UIImage 是低清圖。如果需要判斷是否已經獲得高清圖,可以這樣判斷:
// 排除取消,錯誤,低清圖三種情況,即已經獲取到了高清圖
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
另外,當我們使用 requestImageForAsset 發出對圖像的請求時,如果在同一個 PHImageManager 中同時對同一個資源發出圖像請求,請求的進度是可以共享的,因此我們可以利用這個特性,把 PHImageManager 以單例的形式使用,這樣在切換界面時也不用擔心無法傳遞圖像的下載進度。例如,在圖像的列表頁面觸發了下載圖像,當我們離開列表頁面進入預覽大圖界面時,並不用擔心會重新圖像會重新下載,只要沒有手工取消圖像下載,進入預覽大圖界面下載圖像會自動繼續從上次的進度下載圖像。
如果希望取消下載圖像,則可以使用 PHImageManager 的 cancelImageRequest 方法,它傳入的是請求圖像的請求 ID,這個 ID 可以從 requestImageForAsset 的返回值中獲得,也可以從前面提到的包含圖像信息的 NSDictionary info 中獲得,當然前提是這個這個接收取消請求的 PHImageManager 與剛剛發出請求的 PHImageManager 是同一個實例,如上面所述使用單例是最為簡單有效的方式。
最後,還要介紹一個 PHImageRequestOptions 的屬性 versions,這個屬性是指獲取的圖像是否需要包含系統相冊“編輯”功能處理過的信息(如濾鏡,旋轉等),這一點比 ALAssetLibrary 要靈活很多,ALAssetLibrary 中並不能靈活地控制獲取的圖像是否帶有“編輯”處理過的效果,例如在 ALAsset 中獲取原圖的接口 fullResolutionImage 獲取到的是不帶“編輯”效果的圖像,要想獲取帶有“編輯”效果的圖像,只能自行處理獲取這些濾鏡效果,並手工疊加上去。在我們的 UI 框架 QMUI 中就有對獲取原圖作出這樣的封裝,整個過程也較為繁瑣,而框架中處理 PhotoKit 的部分則靈活很多,這也體現了 PhotoKit 相比 ALAssetLibrary 的最主要特點——復雜但靈活。文章的第三部分也會詳細列出如何處理這個問題。
(2)獲取圖像的優化
PHImageManager 提供了一個子類 PHImageCachingManager 用於處理圖像的緩存,但是這個子類並不只是圖像本身的緩存,而是更加實用——處理圖像的整個加載過程的緩存。例如要在一個 collectionView 上展示圖像列表這類大量的資源圖像的縮略圖時,可以利用 PHImageCachingManager 預先將一些圖像加載到內存中,這對優化 collectionView 滾動時的表現很有幫助。然而,這只是官方說法,實際上由於加載圖像的過程並不確定,每個業務加載圖像的實際需求都可能不一樣,因此 PHImageCachingManager 也采用比較松散的方法去控制這些緩存,其中的關鍵方法:
- (void)startCachingImagesForAssets:(NSArray *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;
需要傳入一組 PHAsset,以及 targetSize,contentMode,以及一個 PHImageRequestOptions,如上面所述,這些參數之間的有著互相影響的作用,因此實際上不同的場景對於每個參數要求都不一樣,而這些參數的最佳取值也只能通過實際在場景中測試所得。因此,比起使用 PHImageCachingManager,我總結了一些更為簡易可行的緩存方法:
獲取圖片時盡量獲取預覽圖,不要直接顯示原件,建議獲取與設備屏幕同樣大小的圖像即可,實際上系統相冊預覽大圖時使用的也是預覽圖,這也是系統相冊加載速度快的原因。
獲取圖片使用異步請求,如上面所述,當請求為異步時返回圖像的 block 會被多次調用,先返回低清圖,再返回高清圖,這樣一來可以大大減少 UI 的等待時間。
獲取到高清圖後可以緩存下來,簡單地使用變量緩存即可,盡量在獲取到高清圖後避免再次發起請求獲取圖像。因為即使圖像原件已經下載下來,重新請求高清圖時因為圖片的尺寸比較大,因此系統生成圖像和剪裁圖像也會花費一些時間。
預先加載圖像,如像預覽大圖這類情景中,用戶同時只會看到一張大圖,因此在觀看某一張圖片時,預先請求其鄰近兩張圖片,對於加快 UI 的響應很有幫助。
經過實際測試,如果請求的是縮略圖(即尺寸小的圖像),那麼即使請求的圖像很多,仍不會產生任何不流暢的表現,但如果請求的是高清大圖,那麼即使只是同時請求幾張圖都會產生不流暢的狀況。如上面提到過的那樣,這些的狀況的出現很可能是請求大圖時由圖片元數據產生圖像,以及剪裁圖像的過程耗時較多。所以按實際表現來看,即使 PhotoKit 有自己的緩存策略,仍然很難避免這部分耗時。因此上面幾點優化獲取圖像的策略重點也是放在減少圖像大小,異步請求以及做緩存幾個方面。