@newpan tech iOS rookie engineer

Hello everyone, I’m NewPan. I wrote an article about an instant startup time optimization for iOS – Jane Book, as can be seen from the title, that article was about startup time optimization, which got a good response from everyone. This time we will talk about how to optimize the rendering time of the home page.

01. Introduction of Baychat home page

Above, bei about parents edition homepage design, can be seen from the above, the home page is very complicated, Guo Yaoyuan in his deep understanding of RunLoop | Garan no dou mentioned:

Heavy tasks in the UI thread will cause the interface to stall. These tasks are usually divided into three categories: typesetting, drawing, and UI object manipulation.

  1. Typesetting usually involves calculating the view size, calculating the text height, recalculating the layout of a subgraph, and so on.
  2. Drawing generally has text drawing (e.gCoreText), image rendering (such as pre-decompression), element rendering (Quartz) and other operations. 3.UI object operations usually includeUIView/CALayerCreate, set properties, and destroy UI objects.

The homepage of Beitchat has covered all the time-consuming operations in the three aspects of “typesetting, drawing and UI object operation”. If you write directly based on UIKit, you need to spend a lot of time to do performance tuning. Therefore, The home page of Beitchat directly adopts AsyncDisplayKit. Although we need to learn the Boxing layout rules of AsyncDisplayKit again, the effect is obvious, and our list will not appear obvious lag when scrolling quickly on the old iPhone 5.

02. Analysis of the rendering time of baychat home page

Let’s take a look at the home page time before optimization. This time refers to the time spent in the process of data loading from the background to the device, parsing and finally rendering into UI. The test device was my iPhone 6S Plus (Gb 64GB), and I tested ten sets of data in total.

// Group 1. 2018-08-14 16:20:38.831014+0800 Beiliao [2429:991848] 1.172843 // Group 2. 2018-08-14 16:21:15.409550+0800 Beiliao [2431:992484] Time from the completion of data loading to the beginning of the home page rendering: 1.199685 // Group 3. 2018-08-14 16:21:50.329775+0800 Beiliao [2433:993092] Time from the completion of data loading to the beginning of the home page rendering: 1.203976 // Group 4. 2018-08-14 16:22:30.805793+0800 Beiliao [2435:993740] Time from the completion of data loading to the beginning of the home page rendering: 2018-08-14 16:23:10.874299+0800 Beiliao [2437:994402] Time from the completion of data loading to the beginning of the home page rendering: 1.127660 // Group 6. 2018-08-14 16:23:43.988901+0800 Beiliao [2439:994997] Time from the completion of data loading to the beginning of the home page rendering: Beiliao [2441:995581] Takes time from the completion of data loading to the beginning of the home page rendering: Beiliao [2444:996330] Takes time from the completion of data loading to the start of home page rendering: 2018-08-14 16:25:30.564408+0800 Beiliao [2446:996948] Time from the completion of data loading to the start of the home page rendering: Beiliao [2452:997656] takes 0.978076 rendering time from the completion of data loading to the start of home pageCopy the code

As can be seen, the data range is 0.550910-1.339828, with an average of 1.05563. And one of the things about this is that it takes the same time on iPhone X or older devices, because you’re blocking the UI thread.

According to the previous section, the homepage of Beitchat uses AsyncDisplayKit. For typesetting, drawing and UI object operation, the first two items have been thrown into the background thread by AsyncDisplayKit, and the results of the last two items will be synchronized to the UI thread for view rendering. Therefore, it should be “UI object manipulation” that affects the rendering of the homepage.

If you have any problems with using a TimeProfiler, please refer to my previous article on iOS to find out which functions take Time.

The figure above has a -fetchAnimationImages method that looks like this. Is to load a sequence of frames, this method takes 0.284 seconds.

+ (NSArray<NSString *> *)fetchAnimationImageNames {
    NSMutableArray<NSString *> *names = @[].mutableCopy;
    for (int i = 2; i <= 23; i++) {
        [names addObject:[NSString stringWithFormat:@"BLDKLoadMoreAnimation-000%02d", i]];
    }
    return names.copy;
}

+ (NSArray<UIImage *> *)fetchAnimationImages {
    NSMutableArray<UIImage *> *images = @[].mutableCopy;
    for (NSString *imageName in [self fetchAnimationImageNames]) {
        [images addObject:[UIImage imageNamed:imageName]];
    }
    return images.copy;
}
Copy the code

Similarly, I analyzed the other methods, and finally, they all called a systematic method -imageNamed:. So I’m going to comment out the call to -Imagenamed in -fetchanimationimages.

+ (NSArray<UIImage *> *)fetchAnimationImages {
    NSMutableArray<UIImage *> *images = @[].mutableCopy;
    for (NSString *imageName in [self fetchAnimationImageNames]) {
//        [images addObject:[UIImage imageNamed:imageName]];
    }
    return images.copy;
}
Copy the code

Open the “Time Profiler” again and see that the -fetchAnimationimages method is no longer in the elapsed function.

So far, we have verified that the biggest killer of homepage rendering time is the -Imagenamed: method.

03. Optimization strategy

We load UI elements this way every day, but never think of it as the straw that breaks the camel’s back.

According to the system documentation, this method loads the image resource from the bundle, decodes the data, and renders it to the screen depending on the user’s device resolution. We know that this process can be time-consuming, especially if the image file is large, so SDWebImage, AFNetworking, and YYWebImage put the decoding of images into child threads.

In iOS 9 and later, this method is thread safe. In iOS 9 and later, this method is thread safe. This means that after iOS 9 this method is thread-safe. Inspired by these third-party libraries, I tried to run -Imagenamed: on child threads and tested it on various models without any problems.

As we all know, -imagenamed: This method has a cache, so once it’s loaded, it’s optimized for caching when it’s loaded again. So I started trying to pre-load the local resource images.

So why does this preload work? Because the timing is very important, from – application: didFinishLaunchingWithOptions: Back to the home page request at this time, just CPU and IO is free (or you can through other means to set aside this paragraph of time the CPU and IO, specific please refer to a quick start time optimization iOS – Jane books), and this time you can load the good pictures of local resources, while waiting for the request to come back, The home page needs to call -imageNamed: method has been preloaded again, again load will enjoy the cache optimization, so that the optimization effect can be achieved.

04. Concrete implementation

The implementation idea is roughly as follows:

    1. The hooks on its own-imageNamed:Method to a custom implementation in which image names are cached locally.
    1. When starting again, in-application:didFinishLaunchingWithOptions:Method starts to preload the cached images from the last APP startup. Instead, it should be loaded concurrently in child threads using GCD.

The specific implementation code is as follows:

The blimagePreloadManager.h file is as follows:

#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface BLImagePreloadManager : NSObject /** * Manually add image names that need to be preloaded (array of image names). ** @warning in load method to be executed. */ + (void)preloadImagesWithImageNames:(NSArray<NSString *> *)imageNames; / * * * manually add to preload images. * * @ warning is added in the load method to perform. * / + (void) preloadImageWithImageName (imageName nsstrings *); /** * Try to preload the image of '-imagename:' (method will automatically switch to child thread). */ + (void)preloadImagesIfNeed; / storage preload image name. * * * * / + (void) storeImageNameForPreload (imageName nsstrings *); @end NS_ASSUME_NONNULL_ENDCopy the code

The blimagePreloadManager. m file is as follows:

#import "BLImagePreloadManager.h" #import "UIImage+ImageDetect.h" #import "BLGCDExtensions.h" static NSString *const kBLImagePreloadManagerStoreKey = @"com.ibeiliao.preload.images.store.key.www"; static BOOL _isStoreTimeTick = NO; static NSTimeInterval const kBLImagePreloadManagerStoreImageTimeInterval = 10; static NSMutableSet<NSString *> *_kImageNameCollectSetM = nil; static dispatch_queue_t _ioQueue; static NSMutableArray<NSString *> *_manualPreloadImageNames = nil; @implementation BLImagePreloadManager + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _ioQueue = dispatch_queue_create("com.ibeiliao.image.preload.queue", DISPATCH_QUEUE_SERIAL); }); } + (void)preloadImagesWithImageNames:(NSArray<NSString *> *)imageNames { NSParameterAssert(imageNames.count); BLAssertMainThread; if (! imageNames.count) { return; } [self manualPreloadArrayInitIfNeed]; NSAssert(_manualPreloadImageNames, @" Too late to add preload behavior, preload is complete, please perform add preload image behavior in load method "); if (! _manualPreloadImageNames) { return; } [_manualPreloadImageNames addObjectsFromArray:imageNames]; } + (void)preloadImageWithImageName:(NSString *)imageName { NSParameterAssert(imageName); if (! imageName.length) { return; } if (! [imageName isKindOfClass:[NSString class]]) { return; } [self preloadImagesWithImageNames:@[imageName]]; PreloadImagesIfNeed {if} + (void) (@ the available (iOS 9.0, *)) {[self manualPreloadArrayInitIfNeed]; NSArray<NSString *> *imageNames = [[NSUserDefaults standardUserDefaults] valueForKey:kBLImagePreloadManagerStoreKey]; if (imageNames.count) { [_manualPreloadImageNames addObjectsFromArray:imageNames]; } if (! _manualPreloadImageNames || ! _manualPreloadImageNames.count) { return; } BOOL bl_imageWithNameEnable = [UIImage respondsToSelector:@selector(bl_imageNamed:)]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSString *imageName in _manualPreloadImageNames) { if ([imageName isKindOfClass:[NSString class]]) { bl_imageWithNameEnable ? [UIImage bl_imageNamed:imageName] : [UIImage imageNamed:imageName]; }}}); } } + (void)storeImageNameForPreload:(NSString *)imageName { NSParameterAssert(imageName); if (! [imageName isKindOfClass:[NSString class]]) { return; } if (_isStoreTimeTick) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _kImageNameCollectSetM = [NSMutableSet set]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBLImagePreloadManagerStoreImageTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ _isStoreTimeTick = YES; [self internalFinishCollectImageName]; }); }); dispatch_async(_ioQueue, ^{ if (_kImageNameCollectSetM && imageName.length) { [_kImageNameCollectSetM addObject:imageName]; }}); } + (void)internalFinishCollectImageName { if (! _kImageNameCollectSetM || ! _kImageNameCollectSetM.count) { [self releaseResources]; return; } dispatch_async(_ioQueue, ^{ [[NSUserDefaults standardUserDefaults] setObject:[_kImageNameCollectSetM allObjects] forKey:kBLImagePreloadManagerStoreKey]; [self releaseResources]; }); } + (void)releaseResources { _kImageNameCollectSetM = nil; _ioQueue = nil; _manualPreloadImageNames = nil; } + (void)manualPreloadArrayInitIfNeed { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if(! _manualPreloadImageNames && ! _isStoreTimeTick) { _manualPreloadImageNames = @[].mutableCopy; }}); } @endCopy the code

05. Optimization effect

With this layer optimized, I still ran 10 sets of tests on my iPhone 6s Plus. Let’s take a look at the results:

// Group 1. 2018-08-14 18:37:03.434442+0800 Beiliao [2603:1056626] Render time from data loading to home page: 2018-08-14 18:38:11.953393+0800 Beiliao [2608:1057951] Time from the completion of data loading to the start of the home page rendering: 2018-08-14 18:38:41.851729+0800 Beiliao [2610:1058585] Time from the completion of data loading to the start of the home page rendering: 2018-08-14 18:39:13.515297+0800 Beiliao [2612:1059171] Time from the completion of data loading to the beginning of the home page rendering: 2018-08-14 18:39:47.610475+0800 Beiliao [2614:1059832] Time from the completion of data loading to the start of the home page rendering: 2018-08-14 18:40:55.798904+0800 Beiliao [2618:1061142] Time from the completion of data loading to the start of home page rendering: 2018-08-14 18:41:25.785528+0800 Beiliao [2621:1061772] Time from the completion of data loading to the start of the home page rendering: 2018-08-14 18:41:56.550695+0800 Beiliao [2623:1062409] Time from the completion of data loading to the beginning of the home page rendering: 200791+0800 Beiliao [2625:1063009] Time from the completion of data loading to the beginning of the home page rendering: 2018-08-14 18:42:58.853888+0800 Beiliao [2627:1063666] From the completion of data loading to the start of the home page rendering time: 0.299298Copy the code

As can be seen, the data range is 0.253540-0.299298, with an average value of 0.268981. Compared with the average value of 1.05563 before optimization, the effect is very obvious.