preface

YYAsyncLayer is a tool for asynchronous drawing and display. Originally from YYKitDemo to contact this tool, in order to ensure smooth list scrolling, view drawing, and image decoding tasks into the background thread, before YYAsyncLayer or want to start from YYKitDemo performance optimization, although some off topic…

YYKitDemo

There are two main proxy methods optimized for lists, one related to drawing display and the other related to calculating layout:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;  
Copy the code

Normal logic might feel it should be called firsttableView : cellForRowAtIndexPath :returnUITableViewCellObject, in fact the call order is returned firstUITableViewCellBecause of the height ofUITableViewInherited fromUIScrollView, sliding range by propertiescontentSizeTo determine,UITableViewThe sliding range needs to pass through each rowUITableViewCellIf the complex cell is calculated in the scrolling process of the list, it may cause a certain degree of lag. Suppose there are 20 pieces of data, and the current screen shows 5,tableView : heightForRowAtIndexPath :The method performs 20 times to return all heights and calculate the sliding range,tableView : cellForRowAtIndexPath :Run five times to return the number of cells displayed on the current screen.Take a brief look at the process from the figure. Return JSON data from the network request, encapsulate the height of the Cell and the Layout of the internal view into a Layout object. Before displaying the Cell, all Layout objects are calculated in the asynchronous thread and stored in an arraytableView: heightForRowAtIndexPath :You only need to fetch from the array to avoid repeated layout calculations. While callingtableView: cellForRowAtIndexPath :Asynchronously draw the layout of the internal view of the Cell, and asynchronously draw and decode the picture. Here we will talk about today’s protagonist YYAsyncLayer.

YYAsyncLayer

First, introduce several classes:

  • YYAsyncLayer: Inherits from CALayer, drawing and creating drawing threads in this class.
  • YYTransaction: Used to create a RunloopObserver to listen for the idle time of the MainRunloop and store the YYTranaction object into the collection.
  • YYSentinel: provides access to the current valuevalue(read only) property, and- (int32_t)increaseThe increment method returns a new onevalueValue, a tool for determining whether an asynchronous draw task has been canceled.

The figure above is the realization idea of the whole asynchronous drawing, explained step by step. Now suppose you want to draw a Label, which is actually inherited from UIView, rewrite + (Class)layerClass, and call the following methods where you want to redraw, such as setter, layoutSubviews.

+ (Class)layerClass {
    return YYAsyncLayer.class;
}
- (void)setText:(NSString *)text {
    _text = text.copy;
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)layoutSubviews {
    [super layoutSubviews];
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
Copy the code

YYTransaction has a selector and target attribute, which is a contentsNeedUpdated method. It does not immediately update the display in the background thread. Instead, the YYTransaction object itself is committed and saved in a transactionSet collection, as shown in the figure above.

+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
    if(! target || ! selector)return nil;
    YYTransaction *t = [YYTransaction new];
    t.target = target;
    t.selector = selector;
    return t;
}
- (void)commit {
    if(! _target || ! _selector)return;
    YYTransactionSetup();
    [transactionSet addObject:self];
}
Copy the code

Also register a RunloopObserver in yyTransaction.m, Listen for kCFRunLoopBeforeWaiting and kCFRunLoopEx of MainRunloop in kCFRunLoopCommonModes including kCFRunLoopDefaultMode and UITrackingRunLoopMode The state of it, that is, when a Runloop is idle to perform an update display operation.

KCFRunLoopBeforeWaiting: Runloop is going to sleep. KCFRunLoopExit: is about to exit the Runloop.


static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain(a);CFRunLoopObserverRef observer;
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true.// repeat
                                           0xFFFFFF.// after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}
Copy the code

The following are the RunloopObserver callback methods, which extract SEL methods from transactionSet and distribute them to each Runloop execution to avoid a Runloop execution that takes too long.

static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (transactionSet.count == 0) return;
    NSSet *currentSet = transactionSet;
    transactionSet = [NSMutableSet new];
    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
    }];
}
Copy the code

The next step is asynchronous drawing, which is handled in a more clever way. When using GCD, a large number of concurrent tasks are submitted to the background thread, resulting in the thread being locked and hibernated, creating a serial queue equal to the number of cpus currently active (activeProcessorCount). Limit MAX_QUEUE_COUNT and store the queue in an array. YYAsyncLayer. YYAsyncLayerGetDisplayQueue m have a method to obtain the queue for drawing of this part (YYKit independent tools YYDispatchQueuePool). There is a parameter in creating a queue that tells the quality of service of the task to be executed in the queue. After iOS8+, the system is different than before.

  • Queue priority before iOS8:

DISPATCH_QUEUE_PRIORITY_HIGH 2 High priority DISPATCH_QUEUE_PRIORITY_DEFAULT 0 Default priority DISPATCH_QUEUE_PRIORITY_LOW (-2) Low priority DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN Background priority

  • After iOS8 + :

QOS_CLASS_USER_INTERACTIVE 0x21, User Interaction (Hopefully soon, QOS_CLASS_USER_INITIATED 0x19, user expectation QOS_CLASS_DEFAULT 0x15, default QOS_CLASS_UTILITY 0x11, QOS_CLASS_BACKGROUND 0x09, background QOS_CLASS_UNSPECIFIED 0x00 utility (available for time-consuming operations)

/// Global display queue, used for content rendering.
static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
#ifdef YYDispatchQueuePool_h
    return YYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated);
#else
#define MAX_QUEUE_COUNT 16
    
    static int queueCount;
    static dispatch_queue_t queues[MAX_QUEUE_COUNT];            // An array of queues
    static dispatch_once_t onceToken;
    static int32_t counter = 0;
    dispatch_once(&onceToken, ^{
        // The number of processors activated by the program
        queueCount = (int) [NSProcessInfo processInfo].activeProcessorCount;        
        queueCount = queueCount < 1 ? 1 : (queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount);
        if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
            for (NSUInteger i = 0; i < queueCount; i++) {
                dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr); }}else {
            for (NSUInteger i = 0; i < queueCount; i++) {
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
                dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); }}});/ / to counter + 1
    int32_t cur = OSAtomicIncrement32(&counter);
    if (cur < 0) cur = -cur;
    // Fetch from array each time
    return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
#endif
}
Copy the code

Next is the code for the drawing part, which is provided in the external interface YYAsyncLayerDelegate proxy – (YYAsyncLayerDisplayTask *)newAsyncDisplayTask method for the callback drawing code, And whether it can asynchronously draw a BOOl property displaysAsynchronously, while overriding CALayer’s display method to call the draw method – (void)_displayAsync:(BOOl)async. It is necessary to know when the background draw task will be cancelled, and the following two cases need to be cancelled and YYSentinel’s increase method is called to increase the value (thread-safe) :

  • Call in viewsetNeedsDisplayThe current drawing task is cancelled and the view needs to be displayed again.
  • And the view is called freedeallocMethods.

YYAsyncLayerDisplayTask class is defined in yyAsynclayer. h, which has three block properties for drawing callback operations. From the name, we can see that they are about to draw, drawing, and drawing completed callback. Whether the current drawing has been cancelled can be determined from the BOOL(^isCancelled)(void) argument passed in to the block.

@property (nullable.nonatomic.copy) void (^willDisplay)(CALayer *layer); 
@property (nullable.nonatomic.copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
@property (nullable.nonatomic.copy) void (^didDisplay)(CALayer *layer, BOOL finished);
Copy the code

Below is part of the – (void) _displayAsync: (BOOL) async rendering code, mainly some logical judgment and drawing function, create the queue through YYAsyncLayerGetDisplayQueue before asynchronous execution, In this case, YYSentinel is used to determine whether the current value is equal to the previous value. If it is not, it means that the drawing task is canceled, and the drawing process will determine whether to cancel for several times. If it is, return will be used to ensure that the canceled task can exit in time.

if (async) {  / / asynchronous
        if (task.willDisplay) task.willDisplay(self);
        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        NSLog(@" --- %d ---", value);
        // Determine whether the current count is equal to the previous count
        BOOL (^isCancelled)() = ^BOOL() {
            returnvalue ! = sentinel.value; };CGSize size = self.bounds.size;
        BOOL opaque = self.opaque;
        CGFloat scale = self.contentsScale;
        CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
        if (size.width < 1 || size.height < 1) {    // View width and height less than 1
            CGImageRef image = (__bridge_retained CGImageRef) (self.contents);
            self.contents = nil;
            if (image) {
                dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
                    CFRelease(image);
                });
            }
            if (task.didDisplay) task.didDisplay(self.YES);
            CGColorRelease(backgroundColor);
            return;
        }
        // draw asynchronously
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            if (isCancelled()) {
                CGColorRelease(backgroundColor);
                return;
            }
            // Enable image context
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
            CGContextRef context = UIGraphicsGetCurrentContext(a);if (opaque) {   / / not transparent
                CGContextSaveGState(context);
                {
                    if(! backgroundColor ||CGColorGetAlpha(backgroundColor) < 1) {
                        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                        CGContextAddRect(context, CGRectMake(0.0, size.width * scale, size.height * scale));
                        CGContextFillPath(context);
                    }
                    if (backgroundColor) {
                        CGContextSetFillColorWithColor(context, backgroundColor);
                        CGContextAddRect(context, CGRectMake(0.0, size.width * scale, size.height * scale));
                        CGContextFillPath(context); }}CGContextRestoreGState(context);
                CGColorRelease(backgroundColor);
            }
            task.display(context, size, isCancelled);
            if (isCancelled()) {
                
                UIGraphicsEndImageContext(a);dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self.NO);
                });
                return;
            }
            // Get the image from the current image context
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext(a);UIGraphicsEndImageContext(a);if (isCancelled()) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self.NO);
                });
                return;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                if (isCancelled()) {
                    if (task.didDisplay) task.didDisplay(self.NO);
                } else {
                    self.contents = (__bridge id)(image.CGImage);
                    if (task.didDisplay) task.didDisplay(self.YES); }}); });Copy the code