最近在开发公司产品Perfect365的图库模块,包括按日期排序的朋友圈和相册两个模块。朋友圈的功能和系统相册类似,都是按照图片的日期信息进行排序,然后按照不同的日期分段展示。Moment的实现思路很简单:首先遍历系统中所有的相册,然后获取每个相册中图片的日期信息,根据日期进行分类排序,最后将所有枚举的数据显示在界面上。示例代码如下:NSSortDescriptor*sort=[NSSortDescriptorsortDescriptorWithKey:@"date"ascending:NO];[objectssortUsingDescriptors:@[sort]];MomentCollection*lastGroup=nil;NSMutableArray*ds=[[NSMutableArrayalloc]init];for(ALAsset*assetinobjects){@autoreleasepool{NSDateComponents*components=[[NSCalendarcurrentCalendar]components:NSDayCalendarUnit|NSMonthCalendarUnit|NSYearCalendarUnitfromDate:[assetdate]];NSUIntegermonth=[componentsmonth];NSUIntegeryear=[componentsyear];[Group|componentdays][Group|componentdaysla]|lastGroup.year!=year||lastGroup.month!=month||lastGroup.day!=day){lastGroup=[MomentCollectionnew];[dsaddObject:lastGroup];lastGroup.month=month;lastGroup.year=year;lastGroup.day=day;}ALAsset*lPhoto=[lastGroup.assetObjslastObject];NSURL*lPhotoURL=[lPhotovalueForProperty:ALAssetPropertyAssetURL];NSURL*photoURL=[assetvalueForProperty:ALAssetPropertyAssetURL];if(![lPhotoURLisEqual:photoURL]){[lastGroup.assetObjsaddObject:asset];}}}到目前为止一切顺利,然后创建一个UICollectionView,设置dataSource可以显示瞬间图片。起初我也这么认为,但开发一个拥有6500万用户的应用程序一切皆有可能。版本发布后,不少用户反映打开相册后直接卡顿,这是什么鬼?QA测试期间一切正常。好吧,继续骚扰用户问什么情况。用户回复:我手机里放了30k+张图片,占用了20G+的存储空间。我的天啊!!!优化方案是针对MomentFunction,肯定是要遍历系统中所有的相册图片,然后按日期排序展示给用户。那么优化只能挤在枚举和排序这两个部分。经过2天的苦思冥想,我们决定采用batchloading+Optimizebytakingsorting的方案。具体思路是:如果用户设备中的图片很多,不是等所有的图片都被枚举排序后才显示,而是每隔一定数量的图片(比如50张)枚举出来就扔掉(放到NSOperationQueue)并按日期排序,然后显示给用户,让用户看到我们动态加载图片的过程,让他知道我们的程序还活着,还在不断加载图片。但是总的来说,排序比图片的枚举耗时更长,也就是前50张图片排序完成后,Queue中已经有好几批之前的枚举放在等待排序的,所以我们只有一批图片了重新排序(即去掉尾部),清空当前Queue,因为中间批次的数据已经没有意义了。方案详细流程图如下:动态加载后刷新曲线流程图,尽量减少显示会给用户带来的突兀感。需要在页面显示前判断用户是否在滑动页面,页面静止时才刷新显示。但是所有图片枚举后的最后一批数据必须暂时保存(否则会显示木有东东),等待用户使用停止滑动后重新加载数据。批量加载朋友圈需要按日期排序显示(***显示在最上面),所以枚举相册时可以先从相册开始(一般用户拍摄的照片会早于导入的照片))一点)。加载50的倍数后,丢入队列等待排序。枚举完一张相册后,继续遍历其余相册...-(void)getPhotosWithGroupTypes:(ALAssetsGroupType)typesbatchReturn:(BOOL)batchcompletion:(void(^)(BOOLret,idobj))completion{self.batchBlock=完成;NSMutableArray*tmpArr=[[NSMutableArrayalloc]init];[self.assetLibaryenumerateGroupsWithTypes:typesusingBlock:^(ALAssetsGroup*group,BOOL*stop){if(self.stopEnumeratePhoto){*stop=YES;return;}NSIntegergType=[[groupvalueForProperty:ALAssetsGroupPropertyType]integerValue];if(group&&(gType!=ALAssetsGroupPhotoStream)){[groupsetAssetsFilter:[ALAssetsFilterallPhotos]];*结果,NSUIntegerindex,BOOL*stop){if(self.stopEnumeratePhoto){*stop=YES;return;}if(result)[tmpArraddObject:result];if(batch&&!([tmpArrcount]%50))[selfaddQueueWithData:tmpArrfinal:NO];}];}elseif(nil==group){[selfaddQueueWithData:tmpArrfinal:YES];}}failureBlock:nil];}取尾排序将每组batch的图片添加到一个串行队列中等待排序,当一个batch排序完成后,当前队列***一张(即来自***的枚举图片)继续执行排序,清空当前队列。也就是说,在下面的sortMomentWithDate:final:function.-(void)addQueueWithData:(NSMutableArray*)datafinal:(BOOL)final{NSMutableArray*rawData=[NSMutableArrayarrayWithArray:data];NSBlockOperation*op=[NSBlockOperationblockOperationWithBlock:^{调用cleanQueueAfterRoundOperation[selfsortMomentWithDate:rawDatafinal:final];}];[self.operQueueaddOperation:op];}-(void)cleanQueueAfterRoundOperation{if(self.operQueue==nil)return;if(self.operQueue.operationCount>1){NSArray*queueArr=self.operQueue.operations;NSMutableArray*opArr=[NSMutableArrayarrayWithArray:queueArr];[opArrremoveLastObject];[opArrremoveLastObject];[opArrmakeObjectsPerformSelector:@selector(cancel)];}}刷新CollectionView显示中间批数据分类图片中的日期。数据准备好后,在reloadData前先判断当前用户是否在滑动collectionView。如果不是滚动状态,则刷新显示,否则直接drop,但是需要先存入最后一批数据,在scrollViewDidEndDragging和scrollVi中在ewDidEndDecelerating中判断,一旦用户停止滑动,就会立即刷新到collectionView中。[[ImageDataAPIsharedInstance]getMomentsWithBatchReturn:YESascending:NOcompletion:^(BOOLdone,idobj){NSMutableArray*dArr=(NSMutableArray*)obj;if(dArr!=nil&&[dArrcount]){if(!self.momentView.dragging&&!self.momentView.decelerating){dispatch_async(dispatch_get_main_queue(),^{[selfreloadWithData:dArr];});}else{if(done){self.backupArr=dArr}}}}];-(void)scrollViewDidEndDragging:(UIScrollView*)scrollViewwillDecelerate:(BOOL)decelerate{if(!decelerate&&self.backupArr){dispatch_async(dispatch_get_main_queue(),^{[selfreloadWithData:self.backupArr];self.backupArr=nil;//donerefresh});}}-(void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView{if(self.backupArr){dispatch_async(dispatch_get_main_queue(),^{[selfreloadWithData:self.backupArr];self.backupArr=nil;//donerefresh});}}后续改进思路根据以上方案,无论设备图片数量多少,都可以正常打开相册,加载图片。但仍有许多地方需要改进:例如可以先存储,等用户停止滑动后再重新加载向上,而不是简单地下降。排序需要进一步优化。现在第一批50张图片排序后,第二批进入排序的200张图片需要重新排序排序。中间批次的数据只是展示给用户看的。是否可以把第200张图片只排序到后面的150张图片,也就是说,如果最后150张图片有新的日期,那么就新建一个section,把相同的日期直接插入到最前面。这个有待后续研究。。。以上只是我自己的一些优化思路,如果大家有更好的解决方案欢迎留言交流~~Photos.frameworkiOS8引入了新的PhotoKitAPI来替代AssetsLibrary框架,而PhotoKit提供接口直接访问Momentdata+(PHFetchResult*)fetchMomentsWithOptions:(nullablePHFetchOptions*)options这个函数直接返回按日期分类的图片集数据,速度很快(猜猜苹果有没有标注日期信息和用户拍照或导入图片时排序)。因此,在iOS8以上系统可以直接使用PhotoKit框架实现矩函数。PHFetchResult*momentRes=[PHAssetCollectionfetchMomentsWithOptions:选项];NSMutableArray*momArray=[[NSMutableArrayalloc]init];for(PHAssetCollection*collectioninmomentRes){NSDateComponents*components=[[NSCalendarcurrentCalendar]components:NSDayCalendarUnit|NSMonthCalendarUnit|UnitfromCalendar:collection.endDate];NSUIntegermonth=[componentsmonth];NSUIntegeryear=[componentsyear];NSUIntegerday=[componentsday];MomentCollection*moment=[MomentCollectionnew];moment.month=month;moment.year=year;moment.day=day;PHFetchOptions*option=[[PHFetchOptionsalloc]init];option.predicate=[NSPredicatepredicateWithFormat:@"mediaType=%d",PHAssetMediaTypeImage];moment.assetObjs=[PHAssetfetchAssetsInAssetCollection:collectionoptions:option];if([moment.assetObjscount])[momArrayaddObject:moment];}所以,我们可以对外统一moment接口,对内区分系统实现(GalleryModel类):iOS7系统使用AssetsLibrary,使用上面的优化方案,iOS8系统直接调用Photos的Moment接口.framework.但是这里有个问题。AssetsLibrary的类型是ALAssetsGroup,PhotoKit的类型是PHFetchResult。在使用的时候怎么统一呢?对外调用是否需要区分系统?解决方法很简单,定义自己的数据类,在数据结构内部进一步区分,自己定义的数据类型用于外部调用:例如定义MomentCollection,包括年月日信息,外部属性assetObjs内部区分系统,返回或设置对应的Type:@interfaceMomentCollection:NSObject@property(nonatomic,readwrite)NSUIntegermonth;@property(非原子,读写)NSUIntegeryear;@property(nonatomic,readwrite)NSUIntegerday;@property(nonatomic,strong)idassetObjs;@end@property(nonatomic,strong)NSMutableArray*items;@property(nonatomic,strong)PHFetchResult*assets;-(id)assetObjs{returnIS_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的数据类型。这样,外界(调用者)就不需要关心数据类型了,所有的逻辑都在内部处理了……另外,对于其他的功能,比如专辑的枚举,专辑海报的获取图片、图片url的获取、某个相册中所有缩略图的获取等,对外可以统一接口,内部区分是使用PhotoKit还是AssetsLibrary。-(void)getMomentsWithBatchReturn:(BOOL)batch//batchforiOS7onlyascending:(BOOL)ascendingcompletion:(void(^)(BOOLdone,idobj))completion;-(void)getThumbnailForAssetObj:(id)assetwithSize:(CGSize)size//sizeforiOS8onlycompletion:(void(^)(BOOLret,UIImage*image))完成;-(void)getURLForAssetObj:(id)asset/*usingPH:(BOOL)usingPH*/completion:(void(^)(BOOLret,NSURL*URL))completion;-(void)getAlbumsWithCompletion:(void(^)(BOOLret,idobj))completion;-(void)getPosterImageForAlbumObj:(id)albumcompletion:(void(^)(BOOLret,idobj))completion;-(void)getPhotosWithGroup:(id)groupcompletion:(void(^)(BOOLret,idobj))完成;-(void)getImageForPhotoObj:(id)assetwithSize:(CGSize)sizecompletion:(void(^)(BOOLret,UIImage*image))完成;完整的矩优化方案和PhotoKit/AssetsLibrary集成接口实现代码(RJPhotoGallery)已上传至GitHub。有兴趣的童鞋可以参考一下。程序中封装的ImageDataAPI是图片加载的模型类,实现了Moment/Album功能。如有需要,可直接复制使用。P.S.欢迎各位童鞋大神投诉交流~~
