你好,歡迎來到IOS教程網

 Ios教程網 >> IOS訊息 >> 關於IOS >> 【投稿】iOS相冊Moment功能的優化方案

【投稿】iOS相冊Moment功能的優化方案

編輯:關於IOS

本文是投稿文章,作者:RylanJIN


最近在開發公司產品Perfect365的Gallery模塊, 包括按日期排序的Moment以及Album這兩個模塊. Moment功能和系統相冊類似, 就是根據圖片的日期信息進行排序, 然後按照不同日期分section顯示.

【投稿】iOS相冊Moment功能的優化方案

Moment的實現思路很簡單: 先遍歷系統的所有相冊, 然後獲取每個相冊內圖片的日期信息, 根據日期進行分類和排序, 最後把枚舉完的所有數據放到界面上來顯示。示例代碼如下:

NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"date" ascending:NO]; 

 [objects sortUsingDescriptors:@[sort]]; 
 MomentCollection *lastGroup = nil;
 NSMutableArray *ds = [[NSMutableArray alloc] init]; 
 for (ALAsset *asset in objects) {    
 @autoreleasepool     {        
 NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit   |                                                                                 NSMonthCalendarUnit |                                                                                 NSYearCalendarUnitromDate:[asset date]];         
NSUInteger month = [components month];        
 NSUInteger year  = [components year];        
 NSUInteger day   = [components day];                  
if (!lastGroup || lastGroup.year!=year || lastGroup.month!=month || lastGroup.day!=day)         {             
lastGroup = [MomentCollection new]; 
[ds addObject:lastGroup];                         
 lastGroup.month = month; 
lastGroup.year = year;
 lastGroup.day = day;         
}                 
 ALAsset *lPhoto    = [lastGroup.assetObjs lastObject];        
 NSURL   *lPhotoURL = [lPhoto valueForProperty:ALAssetPropertyAssetURL];         
NSURL   *photoURL  = [asset  valueForProperty:ALAssetPropertyAssetURL];         
if (![lPhotoURL isEqual:photoURL])       
  {             
[lastGroup.assetObjs addObject:asset];        
 }    
 } 
}

So far so good, 接下來創建UICollectionView, 設置好dataSource就可以顯示moment圖片了. 起初我也是這麼認為的, 但是對於開發一款擁有6500萬用戶的App來說everything is possible. 版本發布之後, 很多用戶反饋打開相冊後App直接freeze掉, what the hell is this? QA測試時一切OK的呀. 好吧, 繼續騷擾用戶詢問到底是神馬情況, 用戶回復: 我手機裡面放了30k+圖片, 占了20G+的存儲空間. OH MY GOD!!!

優化方案

對於Moment功能, 肯定需要遍歷完系統內的所有相冊圖片, 然後再按日期排序後顯示給用戶, 那優化就只能在枚舉和排序這兩部分來壓搾了. 經過2天的苦思冥想決定采用分批加載+取尾排序的方案來優化. 具體思路為: 如果用戶設備內的圖片比較多, 不是等所有圖片都枚舉排序完了再顯示, 而是枚舉每隔一定數量的圖片(e.g. 50張)後就拋出去(放到NSOperationQueue裡)按日期分類並排序, 再顯示給用戶, 這樣讓用戶看到我們動態加載圖片的過程, 讓他知道我們的程序still alive, 並且在不斷的加載圖片. 但是一般情況下排序的耗時會大於圖片的枚舉, 也就是第一個50張排完序後, 前面枚舉放到Queue裡面等待排序的已經有好幾批了, 那麼我們只對最後一批的圖片再排序(也就是取尾)並清空當前的Queue, 因為中間的幾批數據已經makes no sense了. 方案詳細流程圖如下:

【投稿】iOS相冊Moment功能的優化方案

曲線流程圖

為了最大程度的減輕動態加載後刷新顯示對用戶造成的突兀感, 在顯示之前需要判斷用戶是否在滑動頁面, 只有頁面靜止的時候刷新顯示. 但對於全部圖片枚舉完成後的最後一批數據則要暫時保存住(否則就木有東東顯示了), 待用戶停止滑動後reloadData.

分批加載

Moment需要按日期分類顯示(最新的顯示在最前面), 所以在枚舉相冊的時候可以先從camera roll開始(一般用戶拍攝的照片相對導入的圖片會早一點). 加載到50的倍數張後就拋到queue裡面等待排序, 一個相冊枚舉完後再繼續遍歷其余的相冊...

- (void)getPhotosWithGroupTypes:(ALAssetsGroupType)types                     batchReturn:(BOOL)batch                      completion:(void (^)(BOOL ret, id obj))completion {     
self.batchBlock        = completion;    
 NSMutableArray *tmpArr = [[NSMutableArray alloc] init];       
  
 [self.assetLibary enumerateGroupsWithTypes:types   usingBlock:^(ALAssetsGroup *group, BOOL *stop)     {       
  if (self.stopEnumeratePhoto) {*stop = YES; return;
}        
 NSInteger gType = [[group valueForProperty:ALAssetsGroupPropertyType] integerValue];  
       if (group && (gType != ALAssetsGroupPhotoStream))         
{            
 [group setAssetsFilter:[ALAssetsFilter allPhotos]];                         
 [group enumerateAssetsWithOptions:NSEnumerationReverse  usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop)             {  
               if (self.stopEnumeratePhoto) {*stop = YES; return;
}                                  
if (result) [tmpArr addObject:result];                                  
if (batch && !([tmpArr count]%50)) 
[self addQueueWithData:tmpArr final:NO];            
 }];         
}        
else if (nil == group)         {
             [self addQueueWithData:tmpArr final:YES];        

 }     
}
failureBlock:nil]; }

取尾排序

每組批次的圖片都加到一個串行queue隊列裡面等待排序, 某個批次的排序完成之後取當前queue最後一個(也就是最新過來的枚舉圖片)繼續執行排序, 並清空當前的queue. 也就是在下面的sortMomentWithDate:final:函數裡面調用cleanQueueAfterRoundOperation.

- (void)addQueueWithData:(NSMutableArray *)data final:(BOOL)final {    
 NSMutableArray *rawData = [NSMutableArray arrayWithArray:data];         
 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^     {         
[self sortMomentWithDate:rawData final:final];     
}];        
  [self.operQueue addOperation:op]; 
} 
 - (void)cleanQueueAfterRoundOperation {   
  if (self.operQueue == nil) return;        
  if (self.operQueue.operationCount > 1)    
 {        
 NSArray *queueArr = self.operQueue.operations;       
  NSMutableArray *opArr = [NSMutableArray arrayWithArray:queueArr];  
                [opArr removeLastObject];
 [opArr removeLastObject];     
    [opArr makeObjectsPerformSelector:@selector(cancel)];   
  } }

刷新CollectionView顯示圖片

中間批次按日期分類過的數據ready後, 在reloadData之前先判斷一下當前用戶是否在滑動collectionView, 如果是非scroll狀態則刷新顯示, 否則直接drop掉, 但是對於最後一批數據需要先存儲著, 並在scrollViewDidEndDragging和scrollViewDidEndDecelerating裡面判斷, 一旦用戶停止滑動了就立即刷新到collectionView上.

[[ImageDataAPI sharedInstance] getMomentsWithBatchReturn:YES                                                ascending:NO                                               completion:^(BOOL done, id obj) {    
 NSMutableArray *dArr = (NSMutableArray *)obj;          
if (dArr != nil && [dArr count])     {        
 if (!self.momentView.dragging && !self.momentView.decelerating)         {            
 dispatch_async(dispatch_get_main_queue(), ^             {                 
[self reloadWithData:dArr];            
 });       
  }        
 else         {            
 if (done) {self.backupArr = dArr}        
 }    
 }
 }
]; 
 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     if (!decelerate && self.backupArr)     {
         dispatch_async(dispatch_get_main_queue(), ^{            
 [self reloadWithData:self.backupArr];            
 self.backupArr = nil; // done refresh         
}
);
     }
 } 
 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {    
 if (self.backupArr)     {        
 dispatch_async(dispatch_get_main_queue(), ^{            
 [self reloadWithData:self.backupArr];             
self.backupArr = nil; // done refresh        
 }
);    
 }
}

後續改進思路

按上述方案不管設備圖片有多少, 基本可以正常打開相冊並加載圖片. 但還是有很多需要繼續改進的地方: e.g.

  • 中間批次的數據ready後也可以先存儲著, 待用戶停止滑動後在reload上去, 而不是簡單的drop掉.
  • 排序還需要再優化. 現在第一批50張圖片排序後, 第二批進入排序的200張圖片又需要重新分類排序, 中間批次數據只是為了先顯示給用戶看. 是不是第200張圖片可以只對後面的150張進行排序, 也就是後面150張有新的日期, 則新建section, 相同日期直接insert到前面去. 這個還需要後面再研究...

以上只是自己的一些優化思路, 如果有更好的方案, 歡迎留言交流~~

Photos.framework

iOS 8新引入了全新的PhotoKit API, 用來替代AssetsLibrary框架, PhotoKit提供了直接訪問Moment數據的接口+ (PHFetchResult *)fetchMomentsWithOptions:(nullable PHFetchOptions *)options該函數直接返回按日期分類的圖片集合數據, 且速度非常快(猜想是不是Apple在用戶拍攝圖片或者導入圖片時已mark日期信息並分類排序). 因此在iOS 8以上的系統可以直接采用PhotoKit框架來實現moment功能.

 

PHFetchOptions *options  = [[PHFetchOptions alloc] init]; 
options.sortDescriptors  = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate"                                                            ascending:ascending]];
 PHFetchResult  *momentRes = [PHAssetCollection fetchMomentsWithOptions:options]; NSMutableArray *momArray  = [[NSMutableArray alloc] init]; for (PHAssetCollection *collection in momentRes) {     NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit   |                                                                             NSMonthCalendarUnit |                                                                             NSYearCalendarUnit                                                                    fromDate:collection.endDate];  
   NSUInteger month = [components month];    
 NSUInteger year  = [components year];    
 NSUInteger day   = [components day];    
 MomentCollection *moment = [MomentCollection new];   
  moment.month = month; moment.year = year; moment.day = day;    
 PHFetchOptions *option  = [[PHFetchOptions alloc] init];  
   option.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeImage];    
 moment.assetObjs = [PHAsset fetchAssetsInAssetCollection:collection                                                      options:option];   
  if ([moment.assetObjs count]) [momArray addObject:moment];
 }

So, 我們可以對外統一moment接口, 在內部(Gallery Model類)區分系統實現: iOS 7系統采用AssetsLibrary並使用上文的優化方案, iOS 8系統則直接調用Photos.framework的Moment接口.

但是這裡面有個問題, AssetsLibrary的類型是ALAssetsGroup, 而PhotoKit的類型是PHFetchResult, 怎麼在使用的時候統一呢? 難道還需要在外部調用的時候再區分一下系統麼?

解決方法很簡單, 定義自己的數據類, 在數據結構內部再區分, 外部調用時使用的都是自己定義的數據類型:

e.g. 定義MomentCollection, 包括年月日信息, 對外的屬性assetObjs則在內部區分系統並返回或設定相應的類型:

 

@interface MomentCollection : NSObject  
@property (nonatomic, readwrite) NSUInteger     month; 
@property (nonatomic, readwrite) NSUInteger     year;
 @property (nonatomic, readwrite) NSUInteger     day; 
@property (nonatomic, strong) id assetObjs;  
@end 
 @property (nonatomic, strong) NSMutableArray *items;
 @property (nonatomic, strong) PHFetchResult  *assets;  
- (id)assetObjs {   
  return IS_IOS_8 ? self.assets : self.items; 
}  
- (void)setAssetObjs:(id)assetObjs {    
 if (IS_IOS_8)     {         
self.assets = (PHFetchResult *)assetObjs;     
}    
 else     
{       
  self.items  = (NSMutableArray *)assetObjs;     
} 
}

對於相冊或者某個具體的圖片也是類似的處理方法, 定義AlbumObj和PhotoObj數據類型. 這樣外界(調用者)就不用管數據類型了, 所有的邏輯都在內部handle了...

另外, 對於對於其他功能, 比如相冊的枚舉, 相冊Poster圖片的獲取, 圖片URL的獲取, 某個相冊內所有thumbnail的獲取等等都可以對外統一接口, 內部再區分是使用PhotoKit還是AssetsLibrary.

 

- (void)getMomentsWithBatchReturn:(BOOL)batch // batch for iOS 7 only                         ascending:(BOOL)ascending                        completion:(void (^)(BOOL done, id obj))completion;                        
 - (void)getThumbnailForAssetObj:(id)asset                        withSize:(CGSize)size  // size for iOS 8 only                      completion:(void (^)(BOOL ret, UIImage *image))completion;                       
- (void)getURLForAssetObj:(id)asset                 /*usingPH:(BOOL)usingPH*/                completion:(void (^)(BOOL ret, NSURL *URL))completion;                
 - (void)getAlbumsWithCompletion:(void (^)(BOOL ret, id obj))completion; 
 - (void)getPosterImageForAlbumObj:(id)album                        completion:(void (^)(BOOL ret, id obj))completion;                        
 - (void)getPhotosWithGroup:(id)group                 completion:(void (^)(BOOL ret, id obj))completion;                 
 - (void)getImageForPhotoObj:(id)asset                    withSize:(CGSize)size                  completion:(void (^)(BOOL ret, UIImage *image))completion;

 

完整的moment優化方案和PhotoKit/AssetsLibrary集成接口實現代碼(RJPhotoGallery)已經上傳到GitHub, 有興趣的童鞋可以參考一下. 程序內封裝的ImageDataAPI是圖片加載的model類, 實現了Moment/Album功能, 有需要的可以直接copy過去使用.

P.S. 歡迎各路童鞋大神吐槽和交流~~

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved