標籤:
一. 概況
本文側重介紹在前文中簡單介紹過的 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 的機制
- 擷取資源
在 ALAssetLibrary 中擷取資料,無論是相簿,還是資源,本質上都是使用枚舉的方式,遍曆照片庫取得相應的資料,並且資料是從 ALAssetLibrary(照片庫) - ALAssetGroup(相簿)- ALAsset(資源)這一路徑逐層擷取,即使有直接從 ALAssetLibrary 這一層擷取 ALAsset 的介面,本質上也是枚舉 ALAssetLibrary 所得,並不是直接擷取,這樣的好處很明顯,就是非常符合實際應用中資源的顯示路徑:照片庫 - 相簿 - 圖片或視頻,但由於採用枚舉的方式擷取資源,效率低而且不靈活。
而在 PhotoKit 中,則是採用“擷取”的方式拉取資源,這些擷取的手段,都是一系列形如 class func fetchXXX(..., options: PHFetchOptions) -> PHFetchResult 的類方法,具體使用哪個類方法,則視乎需要擷取的是相簿、時刻還是資源,這類方法中的 option 充當了過濾器的作用,可以過濾相簿的類型,日期,名稱等,從而直接擷取對應的資源而不需要枚舉。例如在前文中列舉個的幾個小例子:
// 列出所有相簿智能相簿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 中儲存的應該是各個智能相簿對應的 PHAssetCollectionfor (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];}
- 擷取映像的方式與坑點
經過了上面的步驟,已經可以瞭解到如何在 PhotoKit 中擷取到代表資源的 PHAsset 了,但與 ALAssetLibrary 中從 ALAsset 中直接擷取映像的方式不同,PhotoKit 無法直接從 PHAsset 的執行個體中擷取映像,而是引入了一個管理器 PHImageManager 擷取映像。PHImageManager 是通過請求的方式拉取映像,並可以控制請求得到的映像的尺寸、剪裁方式、品質,緩衝以及請求本身的管理(發出請求、取消請求)等。而請求映像的方法是 PHImageManager 的一個執行個體方法:- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler;
這個方法中的參數坑點不少,下面逐個參數列舉一下其作用及坑點:
asset,映像對應的 PHAsset。
targetSize,需要擷取的映像的尺寸,如果輸入的尺寸大於資源原圖的尺寸,則只返回原圖。需要注意在 PHImageManager 中,所有的尺寸都是用 Pixel 作為單位(Note that all sizes are in pixels),因此這裡想要獲得正確大小的映像,需要把輸入的尺寸轉換為 Pixel。如果需要返回原圖尺寸,可以傳入 PhotoKit 中預先定義好的常量 PHImageManagerMaximumSize,表示返回可選範圍內的最大的尺寸,即原圖尺寸。
contentMode,映像的剪裁方式,與 UIView 的 contentMode 參數相似,控制照片應該以按比例縮放還是按比例填充的方式放到最終展示的容器內。注意如果 targetSize 傳入 PHImageManagerMaximumSize,則 contentMode 無論傳入什麼值都會被視為 PHImageContentModeDefault。
options,一個 PHImageRequestOptions 的執行個體,可以控制的內容相當豐富,包括映像的品質、版本,也會有參數控製圖像的剪裁,下面再展開說明。
resultHandler,請求結束後被調用的 block,返回一個包含資源對於映像的 UIImage 和包含映像資訊的一個 NSDictionary,在整個請求的周期中,這個 block 可能會被多次調用,關於這點連同options 參數在下面展開說明。
(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 擷取到的是不帶“編輯”效果的映像,要想擷取帶有“編輯”效果的映像,只能自行處理擷取這些濾鏡效果,並手工疊加上去。在我們的 使用者介面架構 QMUI 中就有對擷取原圖作出這樣的封裝,整個過程也較為繁瑣,而架構中處理 PhotoKit 的部分則靈活很多,這也體現了 PhotoKit 相比 ALAssetLibrary 的最主要特點——複雜但靈活。文章的第三部分也會詳細列出如何處理這個問題。
(2)擷取映像的最佳化
PHImageManager 提供了一個子類 PHImageCachingManager 用於處理映像的緩衝,但是這個子類並不只是映像本身的緩衝,而是更加實用——處理映像的整個載入過程的緩衝。例如要在一個 collectionView 上展示映像列表這類大量的資源映像的縮圖時,可以利用 PHImageCachingManager 預先將一些映像載入到記憶體中,這對最佳化 collectionView 滾動時的表現很有協助。然而,這隻是官方說法,實際上由於載入映像的過程並不確定,每個業務載入映像的實際需求都可能不一樣,因此 PHImageCachingManager 也採用比較鬆散的方法去控制這些緩衝,其中的關鍵方法:
- (void)startCachingImagesForAssets:(NSArray<PHAsset *> *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;
需要傳入一組 PHAsset,以及 targetSize,contentMode,以及一個 PHImageRequestOptions,如上面所述,這些參數之間的有著互相影響的作用,因此實際上不同的情境對於每個參數要求都不一樣,而這些參數的最佳取值也只能通過實際在情境中測試所得。因此,比起使用 PHImageCachingManager,我總結了一些更為簡易可行的緩衝方法:
擷取圖片時盡量擷取預覽圖,不要直接顯示原件,建議擷取與裝置螢幕同樣大小的映像即可,實際上系統相簿預覽大圖時使用的也是預覽圖,這也是系統相簿載入速度快的原因。
擷取圖片使用非同步請求,如上面所述,當請求為非同步時返回映像的 block 會被多次調用,先返回低清圖,再返回高清圖,這樣一來可以大大減少 UI 的等待時間。
擷取到高清圖後可以緩衝下來,簡單地使用變數緩衝即可,盡量在擷取到高清圖後避免再次發起請求擷取映像。因為即使映像原件已經下載下來,重新請求高清圖時因為圖片的尺寸比較大,因此系統產生映像和剪裁映像也會花費一些時間。
積極式載入映像,如像預覽大圖這類情景中,使用者同時只會看到一張大圖,因此在觀看某一張圖片時,預先請求其鄰近兩張圖片,對於加快 UI 的響應很有協助。
經過實際測試,如果請求的是縮圖(即尺寸小的映像),那麼即使請求的映像很多,仍不會產生任何不流暢的表現,但如果請求的是高清大圖,那麼即使只是同時請求幾張圖都會產生不流暢的狀況。如上面提到過的那樣,這些的狀況的出現很可能是請求大圖時由圖片中繼資料產生映像,以及剪裁映像的過程耗時較多。所以按實際表現來看,即使 PhotoKit 有自己的緩衝策略,仍然很難避免這部分耗時。因此上面幾點最佳化擷取映像的策略重點也是放在減少映像大小,非同步請求以及做緩衝幾個方面。
iOS PhotoKit架構 詳解