iOS Photos Framework高性能獲取系統(tǒng)相冊圖片

coverImage

PhotoKit簡介

PhotoKit 是iOS8之后新出一套比 AssetsLibrary 更完整也更高效的庫,對資源的處理跟 AssetsLibrary 也有很大的不同户矢。

先簡單介紹幾個Photos的類:

  • PHAsset: 代表照片庫中的一個資源失晴,跟 ALAsset 類似,通過 PHAsset 可以獲取和保存資源
  • PHFetchOptions: 獲取資源時的參數(shù),可以傳 nil模闲,即使用系統(tǒng)默認值
  • PHFetchResult: 表示一系列的資源集合腾节,也可以是相冊的集合
  • PHAssetCollection: 表示一個相冊或者一個時刻忘嫉,或者是一個「智能相冊(系統(tǒng)提供的特定的一系列相冊,例如:最近刪除案腺,視頻列表庆冕,收藏等等,如下圖所示)
  • PHImageManager: 用于處理資源的加載劈榨,加載圖片的過程帶有緩存處理访递,可以通過傳入一個 PHImageRequestOptions 控制資源的輸出尺寸,同異步獲取同辣,是否獲取iCloud圖片等
  • PHCachingImageManager: 繼承 PHImageManager 拷姿,對Photos的圖片或視頻資源提供了加載或生成預(yù)覽縮略圖和全尺寸圖片的方法,針對預(yù)處理巨量的資源進行了優(yōu)化旱函。
  • PHImageRequestOptions: 如上面所說响巢,控制加載圖片時的一系列參數(shù)

PhotoKit使用

  1. 導(dǎo)入#import <Photos/Photos.h>
  2. Info.plist添加key:
    <key>NSPhotoLibraryUsageDescription</key>
    <string>需要訪問您的相冊,請點擊允許</string>
  1. 如需要系統(tǒng)相冊名稱跟隨系統(tǒng)語言本地化陡舅,則在Info.plist添加key:
    <key>CFBundleAllowMixedLocalizations</key>
    <true/>
  1. 獲取相冊列表
- (void)fetchAssetCollection
{
    PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init];
    // 按創(chuàng)建時間升序
    allPhotosOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
    // 獲取所有照片(按創(chuàng)建時間升序)
    _allPhotos = [PHAsset fetchAssetsWithOptions:allPhotosOptions];
    // 獲取所有智能相冊
    _smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
    // 獲取所有用戶創(chuàng)建相冊
    _userCollections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];
    //_userCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
}

效果如下:

相冊列表
  1. 獲取具體某個相冊的所有PHAsset
PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
  1. PHAsset請求圖片抵乓,得到UIImage對象
    cell.representedAssetIdentifier = asset.localIdentifier;
    
    // targetSize 是以像素計量的,所以需要實際的 size * UIScreen.mainScreen.scale
    [_imageManager requestImageForAsset:asset targetSize:thumbnailSize contentMode:PHImageContentModeAspectFill options:nil resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        // 當(dāng) resultHandler 被調(diào)用時靶衍,cell可能已被回收灾炭,所以此處加個判斷條件
        if ([cell.representedAssetIdentifier isEqualToString:asset.localIdentifier]) {
            cell.thumbnailImage = result;
        }
    }];

此處有人肯定會有疑惑,- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler這個方法放在<UICollectionViewDataSource>的- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath中執(zhí)行颅眶,不會很卡么蜈出?(也許,相冊中照片比較少的時候可能感覺不太明顯涛酗,可以試試超過1000張的滑動效果)當(dāng)然铡原,如果僅僅是這樣偷厦,那用戶體驗太差了。

最重要的核心代碼是下面要說的燕刻,合理使用PHCachingImageManager只泼,在滾動一系列縮略圖時,我們可以在可視區(qū)域前后維護一些數(shù)據(jù)緩存卵洗。借圖表達下:

向下滑動時请唱,預(yù)先Start Caching

何時執(zhí)行Start CachingStop Caching當(dāng)然得分向上滑動和向下滑動時2種情況各做討論;
所以需要在<UIScrollViewDelegate>的協(xié)議方法中根據(jù)collectionView.contentOffset.y判斷什么時候Start CachingStop Caching代碼如下:

#pragma mark - UIScrollViewDelegate -
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [self updateCachedAssets];
}

- (void)updateCachedAssets
{
    if (!self.isViewLoaded || self.view.window == nil) {
        return;
    }
    
    // 預(yù)熱區(qū)域 preheatRect 是 可見區(qū)域 visibleRect 的兩倍高
    CGRect visibleRect = CGRectMake(0.f, self.collectionView.contentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    CGRect preheatRect = CGRectInset(visibleRect, 0, -0.5*visibleRect.size.height);
    
    // 只有當(dāng)可見區(qū)域與最后一個預(yù)熱區(qū)域顯著不同時才更新
    CGFloat delta = fabs(CGRectGetMidY(preheatRect) - CGRectGetMidY(previousPreheatRect));
    if (delta > self.view.bounds.size.height / 3.f) {
        // 計算開始緩存和停止緩存的區(qū)域
        [self computeDifferenceBetweenRect:previousPreheatRect andRect:preheatRect removedHandler:^(CGRect removedRect) {
            [self imageManagerStopCachingImagesWithRect:removedRect];
        } addedHandler:^(CGRect addedRect) {
            [self imageManagerStartCachingImagesWithRect:addedRect];
        }];
        previousPreheatRect = preheatRect;
    }
}

計算開始緩存和停止緩存的區(qū)域

- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler
{
    if (CGRectIntersectsRect(newRect, oldRect)) {
        CGFloat oldMaxY = CGRectGetMaxY(oldRect);
        CGFloat oldMinY = CGRectGetMinY(oldRect);
        CGFloat newMaxY = CGRectGetMaxY(newRect);
        CGFloat newMinY = CGRectGetMinY(newRect);
        //添加 向下滑動時 newRect 除去與 oldRect 相交部分的區(qū)域(即:屏幕外底部的預(yù)熱區(qū)域)
        if (newMaxY > oldMaxY) {
            CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
            addedHandler(rectToAdd);
        }
        //添加 向上滑動時 newRect 除去與 oldRect 相交部分的區(qū)域(即:屏幕外底部的預(yù)熱區(qū)域)
        if (oldMinY > newMinY) {
            CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
            addedHandler(rectToAdd);
        }
        //移除 向上滑動時 oldRect 除去與 newRect 相交部分的區(qū)域(即:屏幕外底部的預(yù)熱區(qū)域)
        if (newMaxY < oldMaxY) {
            CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
            removedHandler(rectToRemove);
        }
        //移除 向下滑動時 oldRect 除去與 newRect 相交部分的區(qū)域(即:屏幕外頂部的預(yù)熱區(qū)域)
        if (oldMinY < newMinY) {
            CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
            removedHandler(rectToRemove);
        }
    }
    else {
        //當(dāng) oldRect 與 newRect 沒有相交區(qū)域時
        addedHandler(newRect);
        removedHandler(oldRect);
    }
}

執(zhí)行startCachingImagesForAssetsstopCachingImagesForAssets

- (void)imageManagerStartCachingImagesWithRect:(CGRect)rect
{
    NSMutableArray<PHAsset *> *addAssets = [self indexPathsForElementsWithRect:rect];
    [_imageManager startCachingImagesForAssets:addAssets targetSize:thumbnailSize contentMode:PHImageContentModeAspectFill options: nil];
}

- (void)imageManagerStopCachingImagesWithRect:(CGRect)rect
{
    NSMutableArray<PHAsset *> *removeAssets = [self indexPathsForElementsWithRect:rect];
    [_imageManager stopCachingImagesForAssets:removeAssets targetSize:thumbnailSize contentMode:PHImageContentModeAspectFill options:nil];
}

- (NSMutableArray<PHAsset *> *)indexPathsForElementsWithRect:(CGRect)rect
{
    UICollectionViewLayout *layout = self.collectionView.collectionViewLayout;
    NSArray<__kindof UICollectionViewLayoutAttributes *> *layoutAttributes = [layout layoutAttributesForElementsInRect:rect];
    NSMutableArray<PHAsset *> *assets = [NSMutableArray array];
    for (__kindof UICollectionViewLayoutAttributes *layoutAttr in layoutAttributes) {
        NSIndexPath *indexPath = layoutAttr.indexPath;
        PHAsset *asset = [_fetchResult objectAtIndex:indexPath.item];
        [assets addObject:asset];
    }
    return assets;
}

切記:此處執(zhí)行 startCachingImagesForAssets 和 stopCachingImagesForAssets 時的參數(shù) targetSize过蹂,contentMode十绑,options 都要必須和 requestImageForAsset方法里的保持一致

本人手機里有3924張照片,跑了個demo酷勺,占用內(nèi)存最高不超過30M本橙,快速滑動也不會出現(xiàn)掉幀現(xiàn)象。Demo地址??SamplePhotosDemo

參考:

  1. 官方文檔Demo(Swift語言)
  2. iOS8 Photos Framework
  3. Photos 框架實踐以及坑
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脆诉,一起剝皮案震驚了整個濱河市甚亭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌击胜,老刑警劉巖狂鞋,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潜的,居然都是意外死亡,警方通過查閱死者的電腦和手機字管,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門啰挪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嘲叔,你說我怎么就攤上這事亡呵。” “怎么了硫戈?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵锰什,是天一觀的道長。 經(jīng)常有香客問我丁逝,道長汁胆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任霜幼,我火速辦了婚禮嫩码,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘罪既。我一直安慰自己铸题,他們只是感情好铡恕,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丢间,像睡著了一般探熔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烘挫,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天诀艰,我揣著相機與錄音,去河邊找鬼墙牌。 笑死涡驮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喜滨。 我是一名探鬼主播捉捅,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虽风!你這毒婦竟也來了棒口?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤辜膝,失蹤者是張志新(化名)和其女友劉穎无牵,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厂抖,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡茎毁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忱辅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片七蜘。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖墙懂,靈堂內(nèi)的尸體忽然破棺而出橡卤,到底是詐尸還是另有隱情,我是刑警寧澤损搬,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布碧库,位于F島的核電站,受9級特大地震影響巧勤,放射性物質(zhì)發(fā)生泄漏嵌灰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一踢关、第九天 我趴在偏房一處隱蔽的房頂上張望伞鲫。 院中可真熱鬧,春花似錦签舞、人聲如沸秕脓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吠架。三九已至芙贫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間傍药,已是汗流浹背磺平。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拐辽,地道東北人拣挪。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像俱诸,于是被迫代替她去往敵國和親菠劝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容