Optimization of iOS photo album Moment function

Optimization of iOS photo album Moment function

Recently, I am developing the Gallery module of my company's product Perfect365, including the Moment and Album modules sorted by date. The Moment function is similar to the system album, which is to sort the pictures according to their date information, and then display them in sections according to different dates.

The implementation idea of ​​Moment is very simple: first traverse all the albums in the system, then obtain the date information of the pictures in each album, classify and sort them according to the date, and finally put all the enumerated data on the interface for display. The sample code is as follows:

  1. NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@ "date" ascending:NO];
  2.  
  3. [objects sortUsingDescriptors:@[sort]];
  4.  
  5. MomentCollection *lastGroup = nil; NSMutableArray *ds = [[NSMutableArray alloc] init];
  6.  
  7. for (ALAsset *asset in objects)
  8. {
  9. @autoreleasepool  
  10. {
  11. NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit |
  12. NSMonthCalendarUnit |
  13. NSYearCalendarUnit
  14. fromDate:[asset date]];
  15. NSUInteger month = [components month];
  16. NSUInteger year = [components year];
  17. NSUInteger day = [components day];
  18.          
  19. if (!lastGroup || lastGroup.year!=year || lastGroup.month!=month || lastGroup.day!=day)
  20. {
  21. lastGroup = [MomentCollection new ]; [ds addObject:lastGroup];
  22.              
  23. lastGroup.month = month; lastGroup.year = year; lastGroup.day = day;
  24. }
  25.          
  26. ALAsset *lPhoto = [lastGroup.assetObjs lastObject];
  27. NSURL *lPhotoURL = [lPhoto valueForProperty:ALAssetPropertyAssetURL];
  28. NSURL *photoURL = [asset valueForProperty:ALAssetPropertyAssetURL];
  29. if (![lPhotoURL isEqual:photoURL])
  30. {
  31. [lastGroup.assetObjs addObject:asset];
  32. }
  33. }
  34. }

So far so good, next create UICollectionView, set dataSource and you can display moment pictures. I thought so at first, but everything is possible for developing an App with 65 million users. After the version was released, many users reported that the App freezes directly after opening the album, what the hell is this? Everything was OK during QA testing. Well, continue to harass users to ask what is going on, users reply: I have 30k+ pictures in my phone, which takes up 20G+ storage space. OH MY GOD!!!

Optimization plan

For the Moment function, it is necessary to traverse all the album pictures in the system, and then sort them by date and display them to the user. Then the optimization can only be squeezed in the two parts of enumeration and sorting. After two days of hard thinking, it was decided to use the solution of batch loading + tail sorting to optimize. The specific idea is: if there are many pictures in the user's device, instead of waiting for all pictures to be enumerated and sorted before displaying, we enumerate every certain number of pictures (eg 50 pictures) and throw them out (put them in NSOperationQueue) to classify and sort them by date, and then display them to the user, so that the user can see the process of our dynamic loading of pictures, let him know that our program is still alive and is constantly loading pictures. However, in general, the time taken for sorting will be longer than the enumeration of pictures, that is, after the first 50 pictures are sorted, there are already several batches of pictures enumerated and put in the Queue waiting for sorting, then we only sort the last batch of pictures (that is, tail) and clear the current Queue, because the middle batches of data already make no sense. The detailed flow chart of the solution is as follows:

Curve Flowchart

In order to minimize the abruptness of the refresh display after dynamic loading, it is necessary to determine whether the user is sliding the page before displaying, and refresh the display only when the page is still. However, the last batch of data after all pictures are enumerated must be temporarily saved (otherwise nothing will be displayed), and reloadData after the user stops sliding.

Batch loading

Moment needs to be displayed by date (the oldest is displayed at the front), so when enumerating the albums, you can start from the camera roll (the photos taken by users are usually earlier than the imported photos). After loading to a multiple of 50, it will be thrown into the queue to wait for sorting. After one album is enumerated, continue to traverse the rest of the albums...

  1. - ( void )getPhotosWithGroupTypes:(ALAssetsGroupType)types
  2. batchReturn:(BOOL)batch
  3. completion:( void (^)(BOOL ret, id obj))completion
  4. {
  5. self.batchBlock = completion;
  6. NSMutableArray *tmpArr = [[NSMutableArray alloc] init];
  7.      
  8. [self.assetLibary enumerateGroupsWithTypes:types
  9.      
  10. usingBlock:^(ALAssetsGroup *group, BOOL *stop)
  11. {
  12. if (self.stopEnumeratePhoto) {*stop = YES; return ;}
  13. NSInteger gType = [[group valueForProperty:ALAssetsGroupPropertyType] integerValue];
  14. if (group && (gType != ALAssetsGroupPhotoStream))
  15. {
  16. [group setAssetsFilter:[ALAssetsFilter allPhotos]];
  17.              
  18. [group enumerateAssetsWithOptions:NSEnumerationReverse
  19. usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop)
  20. {
  21. if (self.stopEnumeratePhoto) {*stop = YES; return ;}
  22.                  
  23. if (result) [tmpArr addObject:result];
  24.                  
  25. if (batch && !([tmpArr count]% 50 )) [self addQueueWithData:tmpArr final :NO];
  26. }];
  27. }
  28. else   if (nil == group)
  29. {
  30. [self addQueueWithData:tmpArr final :YES];
  31. }
  32. }failureBlock:nil];
  33. }

Tail sort

Each batch of images is added to a serial queue to wait for sorting. After a batch is sorted, the last one in the current queue (that is, the enumerated image that came last) is taken to continue sorting, and the current queue is cleared. That is, cleanQueueAfterRoundOperation is called in the sortMomentWithDate:final: function below.

  1. - ( void )addQueueWithData:(NSMutableArray *)data final :(BOOL) final  
  2. {
  3. NSMutableArray *rawData = [NSMutableArray arrayWithArray:data];
  4.      
  5. NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^
  6. {
  7. [self sortMomentWithDate:rawData final : final ];
  8. }];
  9.      
  10. [self.operQueue addOperation:op];
  11. }
  12.  
  13. - ( void )cleanQueueAfterRoundOperation
  14. {
  15. if (self.operQueue == nil) return ;
  16.      
  17. if (self.operQueue.operationCount > 1 )
  18. {
  19. NSArray *queueArr = self.operQueue.operations;
  20. NSMutableArray *opArr = [NSMutableArray arrayWithArray:queueArr];
  21.          
  22. [opArr removeLastObject]; [opArr removeLastObject];
  23. [opArr makeObjectsPerformSelector: @selector (cancel)];
  24. }
  25. }

Refresh CollectionView to display pictures

After the middle batch of data classified by date is ready, before reloadData, first determine whether the current user is scrolling the collectionView. If it is not in the scrolling state, refresh the display, otherwise drop it directly. However, the last batch of data needs to be stored first and judged in scrollViewDidEndDragging and scrollViewDidEndDecelerating. Once the user stops sliding, it will be refreshed to the collectionView immediately.

  1. [[ImageDataAPI sharedInstance] getMomentsWithBatchReturn:YES
  2. ascending:NO
  3. completion:^(BOOL done, id obj)
  4. {
  5. NSMutableArray *dArr = (NSMutableArray *)obj;
  6.      
  7. if (dArr != nil && [dArr count])
  8. {
  9. if (!self.momentView.dragging && !self.momentView.decelerating)
  10. {
  11. dispatch_async(dispatch_get_main_queue(), ^
  12. {
  13. [self reloadWithData:dArr];
  14. });
  15. }
  16. else  
  17. {
  18. if (done) {self.backupArr = dArr}
  19. }
  20. }
  21. }];
  22.  
  23. - ( void )scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  24. {
  25. if (!decelerate && self.backupArr)
  26. {
  27. dispatch_async(dispatch_get_main_queue(), ^{
  28. [self reloadWithData:self.backupArr];
  29. self.backupArr = nil; // done refresh  
  30. });
  31. }
  32. }
  33.  
  34. - ( void )scrollViewDidEndDecelerating:(UIScrollView *)scrollView
  35. {
  36. if (self.backupArr)
  37. {
  38. dispatch_async(dispatch_get_main_queue(), ^{
  39. [self reloadWithData:self.backupArr];
  40. self.backupArr = nil; // done refresh  
  41. });
  42. }
  43. }

Subsequent improvement ideas

According to the above solution, no matter how many pictures there are on the device, the album can be opened and loaded normally. But there are still many areas that need to be improved:

  • The data in the intermediate batch can also be stored after it is ready, and then reloaded after the user stops sliding, instead of simply dropping it.

  • The sorting needs to be optimized. Now after the first batch of 50 pictures are sorted, the second batch of 200 pictures need to be reclassified and sorted. The middle batch data is just for displaying to users first. Can the 200th picture only sort the next 150 pictures, that is, if the next 150 pictures have new dates, create a new section, and directly insert the same date to the front. This needs to be studied later...

The above are just some of my own optimization ideas. If you have a better solution, please leave a message~~

Photos.framework

iOS 8 introduces a new PhotoKit API to replace the AssetsLibrary framework. PhotoKit provides an interface for directly accessing Moment data + (PHFetchResult *)fetchMomentsWithOptions:(nullable PHFetchOptions *)options This function directly returns a collection of pictures classified by date, and the speed is very fast (I guess Apple has marked the date information and sorted it by category when the user takes or imports the picture). Therefore, the PhotoKit framework can be directly used to implement the moment function in iOS 8 and above.

  1. PHFetchOptions *options = [[PHFetchOptions alloc] init];
  2. options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@ "endDate"  
  3. ascending:ascending]];
  4. PHFetchResult *momentRes = [PHAssetCollection fetchMomentsWithOptions:options];
  5. NSMutableArray *momArray = [[NSMutableArray alloc] init];
  6. for (PHAssetCollection *collection in momentRes)
  7. {
  8. NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit |
  9. NSMonthCalendarUnit |
  10. NSYearCalendarUnit
  11. fromDate:collection.endDate];
  12. NSUInteger month = [components month];
  13. NSUInteger year = [components year];
  14. NSUInteger day = [components day];
  15. MomentCollection *moment = [MomentCollection new ];
  16. moment.month = month; moment.year = year; moment.day = day;
  17. PHFetchOptions *option = [[PHFetchOptions alloc] init];
  18. option.predicate = [NSPredicate predicateWithFormat:@ "mediaType = %d" , PHAssetMediaTypeImage];
  19. moment.assetObjs = [PHAsset fetchAssetsInAssetCollection:collection
  20. options:option];
  21. if ([moment.assetObjs count]) [momArray addObject:moment];
  22. }

So, we can unify the moment interface externally and differentiate the system implementations internally (Gallery Model class): iOS 7 system uses AssetsLibrary and uses the above optimization solution, while iOS 8 system directly calls the Moment interface of Photos.framework.

But there is a problem here. The type of AssetsLibrary is ALAssetsGroup, and the type of PhotoKit is PHFetchResult. How can we unify them when using them? Do we need to distinguish the systems when calling externally?

The solution is very simple. Define your own data class, distinguish it within the data structure, and use the data type you defined when making external calls:

For example, define MomentCollection, including year, month, and day information, and the external attribute assetObjs distinguishes the system internally and returns or sets the corresponding type:

  1. @interface MomentCollection : NSObject
  2.  
  3. @property (nonatomic, readwrite) NSUInteger month;
  4. @property (nonatomic, readwrite) NSUInteger year;
  5. @property (nonatomic, readwrite) NSUInteger day;
  6. @property (nonatomic, strong) id assetObjs;
  7.  
  8. @end  
  9.  
  10. @property (nonatomic, strong) NSMutableArray *items;
  11. @property (nonatomic, strong) PHFetchResult *assets;
  12.  
  13. - (id)assetObjs
  14. {
  15. return IS_IOS_8 ? self.assets : self.items;
  16. }
  17.  
  18. - ( void )setAssetObjs:(id)assetObjs
  19. {
  20. if (IS_IOS_8)
  21. {
  22. self.assets = (PHFetchResult *)assetObjs;
  23. }
  24. else  
  25. {
  26. self.items = (NSMutableArray *)assetObjs;
  27. }
  28. }

The same processing method is used for albums or specific pictures. AlbumObj and PhotoObj data types are defined. In this way, the outside world (caller) does not need to worry about the data type, and all the logic is handled internally...

In addition, for other functions, such as enumerating albums, obtaining album poster images, obtaining image URLs, obtaining all thumbnails in an album, etc., a unified interface can be used externally, and internally distinguish whether to use PhotoKit or AssetsLibrary.

  1. - ( void )getMomentsWithBatchReturn:(BOOL)batch // batch for iOS 7 only  
  2. ascending:(BOOL)ascending
  3. completion:( void (^)(BOOL done, id obj))completion;
  4.                         
  5. - ( void )getThumbnailForAssetObj:(id)asset
  6. withSize:(CGSize)size // size for iOS 8 only  
  7. completion:( void (^)(BOOL ret, UIImage *image))completion;
  8.                       
  9. - ( void )getURLForAssetObj:(id)asset
  10. /*usingPH:(BOOL)usingPH*/  
  11. completion:( void (^)(BOOL ret, NSURL *URL))completion;
  12.                 
  13. - ( void )getAlbumsWithCompletion:( void (^)(BOOL ret, id obj))completion;
  14.  
  15. - ( void )getPosterImageForAlbumObj:(id)album
  16. completion:( void (^)(BOOL ret, id obj))completion;
  17.                         
  18. - ( void )getPhotosWithGroup:(id)group
  19. completion:( void (^)(BOOL ret, id obj))completion;
  20.                  
  21. - ( void )getImageForPhotoObj:(id)asset
  22. withSize:(CGSize)size
  23. completion:( void (^)(BOOL ret, UIImage *image))completion;

The complete moment optimization solution and PhotoKit/AssetsLibrary integration interface implementation code (RJPhotoGallery) have been uploaded to GitHub. Interested friends can refer to it. The ImageDataAPI encapsulated in the program is the model class for image loading, which implements the Moment/Album function. If you need it, you can directly copy it and use it.

PS Welcome all the friends to comment and communicate~~

<<:  Android 6.0 will be pushed next week, with a lot of new features

>>:  The programmers with the highest average income are not from the United States

Recommend

What do we talk about when we talk about electric propulsion in aviation?

Let’s talk about what is aviation electric propul...

Here they come! What special features do these two Fengyun satellites have?

On June 1, Fengyun-3E, Fengyun-4B and their groun...

A detailed account of several shortcomings in Alibaba's future development

Since 2014, the reach of the Alibaba empire has b...

The "slowest bird" may not fly first, but it may have the longest wings

Albatross, also known as sea mandarin duck, belon...

Skipping breakfast makes you more likely to gain weight. Why is this happening?

When managing their weight, some people specifica...

Highly cute alert! We are serious about rescuing wild animals!

The Oriental White Stork is an endangered species...