This series of articles will analyze the implementation of ASDK’s performance tuning strategy from several aspects to help readers understand how ASDK can achieve a complex UI refresh rate of 60 FPS. This article will explain how ASDK optimizes the rendering process from the view rendering level and give an overview of ASDK.

In client or front-end development, performance optimization, especially the UI, is often not a priority.

Because replacing available code with more complex high-performance code often leads to less maintainability in most scenarios, it is even more important that we developers have a clear understanding of when and why to optimize to avoid the problems caused by over-optimization.

Developers familiar with iOS development know that performance issues in iOS are mostly caused by blocking the main thread causing perceived delays in user interaction feedback.

In detail, there are three main reasons:

  1. UI rendering takes a long time and results cannot be submitted on time;
  2. Some computation-intensive processing is performed in the main thread, which blocks the main thread and renders the UI.
  3. Network requests respond slowly due to network state issues, and the UI layer cannot render due to no model return.

All of the above issues can affect application performance, most commonly when a UITableView slides below 60 FPS and users feel a noticeable lag.

Screen rendering

Most of the developers who clicked on this post already know what an FPS is, so how can we optimize our App to get to 60 FPS? Before we get into the details, let’s step back and ask another question: How does the screen render?

It may take several articles to answer the first question, but hopefully the whole series will give you a satisfactory answer. 3

The CRT and LCD

Rendering of screens may start with CRT (Cathode Ray tube) displays and liquid-crystal displays.

CRT display is a relatively old technology. It uses a cathode electron gun to emit electrons. Under the action of high pressure from the cathode, the electrons are fired from the electron gun to the fluorescent screen, making the phosphor glow and displaying the image on the screen, which is why magnets close to some old TV screens will make them change color.

The FPS is the refresh rate of the CRT display, and the electron gun will refresh the display 60-100 times per second, even if nothing seems to have changed.

But the principle of LCD and CRT is very different, LCD imaging principle is related to optics:

  • Without voltage, light moves along the gap between the liquid crystal molecules and rotates 90°, so light can pass through;
  • After the voltage is applied, the light travels straight through the gap between the liquid crystal molecules and is blocked by the filter plate.

If you can scale a wall, the following video will help you better understand how LCD works:

Review images

Although the imaging principle of LCD is completely different from that of CRT. The color of each pixel can be changed only when it needs to be changed, that is, the refresh frequency is not required. However, due to some historical reasons, LCD still needs to obtain new images from GPU at a certain refresh frequency for display.

Screen tearing

But the monitor is just for putting the image on the screen, and who is the provider of the image? The graphics are provided by what we often call gpus.

This leads to another problem. Since the frequency of images generated by GPU is not correlated with the frequency of display refresh, what should BE done if GPU is not ready to display images when the display is refreshed? Or the GPU rendering speed is too fast, the display has no time to refresh, the GPU has already started rendering the next frame of the image?

If these two problems cannot be resolved, the Screen in the image above will tear, with one part of the Screen displaying the previous frame and another part displaying the next frame.

Let’s use two examples to illustrate the two situations in which screen tearing can occur:

  • If the refresh rate of the display is 75 Hz and the rendering speed of the GPU is 100 Hz, then in the interval between two screen refreshes, the GPU will render 3/4 frames, and the next 1/3 frames will overwrite the rendered frame stack, resulting in the screen tearing effect at 1/3 or 2/3 positions of the screen.
  • If the rendering speed of GPU is less than that of the display, say 50 Hz, then in the interval between two screen refreshes, the GPU will render only 2/3 frames, and the remaining 1/3 will come from the previous frame, which is exactly the same as the above result, and the tearing effect will appear in the same position.

At this point, one would say that if the display refreshed at exactly the same rate as the GPU rendered, that would solve the screen tear problem? It’s not. The process of the display copying frames from the GPU still takes some time, and if the screen is refreshed while copying the image, it will still cause the screen to tear.

Screen tearing can be effectively alleviated by introducing multiple buffers, that is, using a frame buffer and multiple back buffers simultaneously. Each time the display requests content, the image is taken from the frame buffer and rendered.

Buffers mitigate these problems, but they do not solve them; If the back-up buffer is drawn and the frame buffer’s image is not rendered, the image in the back-up buffer overwrites the frame buffer, still causing the screen to tear.

Solving this problem requires the help of another mechanism, Vertical synchronization, or V-sync.

V-Sync

The main purpose of V-sync is to ensure that the contents of the back-up buffer are copied to the frame buffer only after the image in the frame buffer is rendered. Ideally, V-sync would work like this:

Each time V-sync occurs, the CPU and GPU have finished processing and rendering the image, and the display can grab the frame directly from the buffer. However, if the processing time of CPU or GPU is long, the problem of frame drop will occur:

When the V-Sync signal is sent, the CPU and GPU do not have a frame ready to render, and the display will continue to use the current frame, which exacerbates the display problem, and the screen will display less than 60 frames per second.

Since there are many frame drops, a render rate of 40-50 FPS with V-Sync enabled means that the display output will drop dramatically from 60 FPS to 30 FPS, for reasons not explained here but left to the reader.

In fact, the content of screen rendering is almost finished here. According to the principle of V-Sync, the application performance can be optimized and the FPS of App can be improved from two aspects: the processing time of CPU and GPU can be optimized.

You can also learn more about how to keep the interface smooth on iOS.

Performance tuning strategies

What are the CPUS and Gpus doing before each V-sync time point? If we know what they do, we can improve performance by optimizing the code.

Many CPU operations delay the GPU’s rendering start time:

  • Layout calculation – If your view hierarchy is too complex, or the view needs to be laid out multiple times, especially if Auto Layout is used for automatic Layout, this can have a significant impact on performance.
  • Lazy loading of views – in iOS it only loads when the view controller’s view is displayed on screen;
  • Unzip the image – iOS will usually not decode the image until it is actually drawn, for a larger image, either directly or indirectlyUIImageViewOr to draw to Core Graphics, you need to extract the image;
  • .

Broadly speaking, most of CALayer’s attributes are drawn by the GPU, such as rounded corners, transforms, applying textures; However, too much geometry, redrawing, off-screen drawing, and large images can lead to a significant decrease in GPU performance.

You can get a better understanding of what the CPU and GPU do in this article.

In other words, if we solve the above problems, we can speed up the application rendering and greatly improve the user experience.

AsyncDisplayKit

The first half of the article has covered several performance tuning strategies from screen rendering principles; The AsyncDisplayKit helps us optimize application performance based on the above strategies.

AsyncDisplayKit (ASDK) is an iOS framework open-source by Facebook that helps keep even the most complex UI interfaces smooth and responsive.

It took more than a year for ASDK to open source. It’s not a simple framework, it’s a complex framework, it’s more like a reimplementation of UIKit, encapsulating the entire UIKit and CALayer layer into nodes, Move expensive rendering, image decoding, layout, and other UI operations out of the main thread so that the main thread can react to user actions.

Many articles analyzing ASDK have a diagram that illustrates the most basic concepts in the framework:

The basic unit in ASDK is ASDisplayNode, and each node is an abstraction of UIView and CALayer. But unlike UIView, ASDisplayNode is thread-safe and can be initialized and configured in background threads.

If the refresh rate is calculated at 60 FPS, the rendering time of each frame is only 16ms, and the CPU and GPU are under great pressure to complete the creation, layout, drawing and rendering of UIView within 16ms.

However, after THE A5 processor, multi-core devices became the mainstream, and the original practice of putting all operations into the main thread could not adapt to the complex UI interface, so ASDK put the time-consuming CPU operations and GPU rendering Texture into the background process. Enables the main thread to quickly respond to user operations.

ASDK optimizes App performance through unique rendering techniques, a layout system that replaces AutoLayout, intelligent preloading methods and other modules.

ASDK rendering process

What methods are used to render views in ASDK? This article will mainly start from the process of rendering analysis, understand how to improve the performance of ASDK interface rendering.

Rendering in ASDK takes place around ASDisplayNode and there are four main threads:

  • Initialize theASDisplayNodeThe correspondingUIVieworCALayer;
  • Executed when the current view enters the view hierarchysetNeedsDisplay;
  • displayMethod is used to issue a draw transaction to a background thread.
  • Register as aRunLoopThe observer, at everyRunLoopEnd callback.

UIView and CALayer loading

When we run a use ASDK engineering, – [ASDisplayNode _loadViewOrLayerIsLayerBacked:] always ASDK in the called method first, This method is executed because the UIView and CALayer corresponding to ASDisplayNode are referenced:

- (CALayer *)layer {
    if(! _layer) { ASDisplayNodeAssertMainThread();if(! _flags.layerBacked)return self.view.layer;
        [self _loadViewOrLayerIsLayerBacked:YES];
    }
    return _layer;
}

- (UIView *)view {
    if (_flags.layerBacked) return nil;
    if(! _view) { ASDisplayNodeAssertMainThread(); [self _loadViewOrLayerIsLayerBacked:NO];
    }
    return _view;
}Copy the code

If ASDisplayNode is layerBacked, it will not render the corresponding UIView to improve performance:

- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked {
    if (isLayerBacked) {
        _layer = [self _layerToLoad];
        _layer.delegate = (id<CALayerDelegate>)self;
    } else {
        _view = [self _viewToLoad];
        _view.asyncdisplaykit_node = self;
        _layer = _view.layer;
    }
    _layer.asyncdisplaykit_node = self;

    self.asyncLayer.asyncDelegate = self;
}Copy the code

Although BOTH UIView and CALayer can be used to display content, UIView can be used to handle user interaction, so if you do not need to use UIView features, you can directly use CALayer for rendering, which can save a lot of rendering time.

If you’ve ever looked at the Hierarchy of views in Xcode, you should know that UIViews are hierarchical in the Debug View Hierarchy; CALayer doesn’t. Its display is all on one plane.

The above methods -[ASDisplayNode _layerToLoad] and [ASDisplayNode _viewToLoad] will only initialize an object based on the current node’s layerClass or viewClass.

Layer Trees vs. Flat Drawing — Graphics Performance Across iOS Device Generations This article compares render times of UIView and CALayer.

-[ASDisplayNode asyncLayer] simply wraps the layer currently held by the node, ensuring that an instance of _ASDisplayLayer is returned:

- (_ASDisplayLayer *)asyncLayer {
    ASDN::MutexLocker l(_propertyLock);
    return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil;
}Copy the code

The most important thing is – [ASDisplayNode _loadViewOrLayerIsLayerBacked:] method will set the current node to asyncLayer agent, behind will use ASDisplayNode CALayer for rendering content.

The view hierarchy

After initialization, when ASDisplayNode is first added to the view hierarchy, -[_ASDisplayView willMoveToWindow:] is called.

_ASDisplayView and _ASDisplayLayer

_ASDisplayView and _ASDisplayLayer are private classes, and the correspondence between them is exactly the same as UIView and CALayer.

+ (Class)layerClass {
    return [_ASDisplayLayer class];
}Copy the code

_ASDisplayView overrides a number of methods related to view hierarchy changes:

  • -[_ASDisplayView willMoveToWindow:]
  • -[_ASDisplayView didMoveToWindow]
  • -[_ASDisplayView willMoveToSuperview:]
  • -[_ASDisplayView didMoveToSuperview]

They are used to notify the corresponding ASDisplayNode to react when the view’s hierarchy changes, such as the -[_ASDisplayView willMoveToWindow:] method called when the view is added to the hierarchy:

- (void)willMoveToWindow:(UIWindow *)newWindow {
    BOOLvisible = (newWindow ! =nil);
    if (visible && !_node.inHierarchy) {
        [_node __enterHierarchy];
    }
}Copy the code

setNeedsDisplay

If the current view is not in the view hierarchy, it is added to the view hierarchy via the _node instance method -[ASDisplayNode __enterHierarchy] :

- (void)__enterHierarchy {
    if(! _flags.isInHierarchy && ! _flags.visibilityNotificationsDisabled && ! [self __selfOrParentHasVisibilityNotificationsDisabled]) {
        _flags.isEnteringHierarchy = YES;
        _flags.isInHierarchy = YES;

        if (_flags.shouldRasterizeDescendants) {
            [self _recursiveWillEnterHierarchy];
        } else{[self willEnterHierarchy];
        }
        _flags.isEnteringHierarchy = NO;

        Update the layer display}}Copy the code

_flags is the ASDisplayNodeFlags structure, which is used to mark the BOOL values of the current ASDisplayNode, such as asynchronous display, rasterized subview, etc. You don’t need to know what the BOOL values are, just take them literally.

The first part of this method is just a flag for _flags. If you want to rasterize the current view’s subviews, that is, compress all of its subviews into one layer with the current view, The -[ASDisplayNode willEnterHierarchy] method is recursively called to these views to inform them of the current state:

- (void)_recursiveWillEnterHierarchy {
  _flags.isEnteringHierarchy = YES;
  [self willEnterHierarchy];
  _flags.isEnteringHierarchy = NO;

  for (ASDisplayNode *subnode in self.subnodes) { [subnode _recursiveWillEnterHierarchy]; }}Copy the code

And – [ASDisplayNode willEnterHierarchy] to modify the current node interfaceState ASInterfaceStateInHierarchy, said the current node is not included in the cell or the other, But in Windows.

- (void)willEnterHierarchy {
  if(! [self supportsRangeManagedInterfaceState]) {
    self.interfaceState = ASInterfaceStateInHierarchy; }}Copy the code

When the current node needs to be displayed on the screen, if its contents are empty, the -[CALayer setNeedsDisplay] method is called to mark the CALayer as dirty and inform the system that the view needs to be redrawn in the next draw loop:

- (void)__enterHierarchy {
     if(! _flags.isInHierarchy && ! _flags.visibilityNotificationsDisabled && ! [self __selfOrParentHasVisibilityNotificationsDisabled]) {

        # Flag the node

        if (self.contents == nil) {
            CALayer *layer = self.layer;
            [layer setNeedsDisplay];

            if ([self _shouldHavePlaceholderLayer]) {
                [CATransaction begin];
                [CATransaction setDisableActions:YES];
                [self _setupPlaceholderLayerIfNeeded];
                _placeholderLayer.opacity = 1.0;
                [CATransactioncommit]; [layer addSublayer:_placeholderLayer]; }}}}Copy the code

After the CALayer is marked dirty, the draw loop executes the -[CALayer display] method to draw what it wants to show; If the current view needs some placeholders, the code here adds placeholder layers of the appropriate color for the layer corresponding to the current node.

Dispatch asynchronous draw transactions

After calling the -[CALayer setNeedsDisplay] method in the previous section to mark the current node as dirty, the next drawing loop will execute -[CALayer display] on all calayers that need to be redrawn, This is also the entrance to the method to be analyzed in this section:

- (void)display {
  [self _hackResetNeedsDisplay];

  ASDisplayNodeAssertMainThread();
  if (self.isDisplaySuspended) return;

  [self display:self.displaysAsynchronously];
}Copy the code

The call stack of this method is quite complex. Before the specific analysis, the author will first give the call stack of this method to give the reader a brief impression of the implementation of this method:

-[_ASDisplayLayer display]
    -[_ASDisplayLayer display:] // Leave the drawing to ASDisplayNode
        -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
            -[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]
                -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]            
            -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_parentTransactionContainer]
            -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_asyncTransaction]
                -[_ASAsyncTransaction initWithCallbackQueue:completionBlock:]
                -[_ASAsyncTransactionGroup addTransactionContainer:]
            -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
                ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
                    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);Copy the code

-[_ASDisplayLayer display] creates a displayBlock in the call stack, which is actually a process of drawing images using Core Graphics. The whole drawing process is managed in the form of transactions; The displayBlock is distributed by the GCD to a concurrent process in the background.

The second method on the call stack -[_ASDisplayLayer display] hands off the asynchronous drawing to its own asyncDelegate, the ASDisplayNode set up in Part 1:

- (void)display:(BOOL)asynchronously {
  [_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}Copy the code

ASDisplayNode(AsyncDisplay)

Here omitted part – [ASDisplayNode (AsyncDisplay) displayAsyncLayer: asynchronously:] the implementation of the methods:

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { ASDisplayNodeAssertMainThread(); . asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];

  if(! displayBlock)return;

  asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
    ASDisplayNodeCAssertMainThread(a);if(! canceled && ! isCancelledBlock()) {UIImage *image = (UIImage *)value;
      _layer.contentsScale = self.contentsScale;
      _layer.contents = (id)image.CGImage; }};if (asynchronously) {
    CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
    _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
    [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
  } else {
    UIImage *contents = (UIImage *)displayBlock();
    completionBlock(contents, NO); }}Copy the code

The omitted code is very clear, – [ASDisplayNode (AsyncDisplay) _displayBlockWithAsynchronous: isCancelledBlock: rasterizing:] returns a displayBlock, A completionBlock is then constructed and executed at the end of the drawing, setting the contents of the current layer in the main thread.

If the current render is asynchronous, the displayBlock is wrapped as a transaction and added to the queue for execution, otherwise the current block is executed synchronously and the completionBlock callback is executed to inform the Layer to update the display.

The synchronous part is already clear, but we are more concerned with the asynchronous part, because this part is the key to ASDK’s efficiency; And that starts with how you get the displayBlock.

The construction of displayBlock

Displayblocks are generally created in three different ways:

  1. Compress the subviews of the current view into a layer to draw on the current page
  2. use- displayWithParameters:isCancelled:Returns aUIImage, to the image nodeASImageNodeTo draw
  3. use- drawRect:withParameters:isCancelled:isRasterizing:Draws the text node in the CG contextASTextNode

All three methods use ASDK to optimize the rendering speed of the view, and these operations are eventually thrown into a concurrent thread in the background.

The following three sections of the code have been cut, leaving out code for undrawing, notifying agents, controlling concurrency, and debugging.

Rasterize subviews

If the current view requires rasterization of its subviews, a block is created by enabling the following construct, which recursively draws the subviews on the parent view:

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if(! rasterizing &&self.shouldRasterizeDescendants) {
    NSMutableArray *displayBlocks = [NSMutableArray array];
    [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];

    CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay;
    BOOL opaque = self.opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f;

    displayBlock = ^id{

      UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);

      for (dispatch_block_t block in displayBlocks) {
        block();
      }

      UIImage *image = UIGraphicsGetImageFromCurrentImageContext(a);UIGraphicsEndImageContext(a);return image;
    };
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    # : Draw UIImage
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
    # : Provide context, use CG drawing
  }

  return [displayBlock copy];
}Copy the code

In the process of compression the view hierarchy is called – [ASDisplayNode (AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock: displayBlocks:] Methods, get all the displayBlock of child views, needs in the get UIGraphicsBeginImageContextWithOptions parameters, create a new context, Perform all displayBlock after child view mapped to the current layer, use UIGraphicsGetImageFromCurrentImageContext out the content of the layer and return.

– [ASDisplayNode (AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock: displayBlocks:] the implementation of some trival, Its main function is to use Core Graphics to draw parameters such as background color, affine transformation, position size, and rounded corners into the current context, and the process is recursive until no child nodes exist or need to be drawn.

Draw pictures

DisplayBlock’s second rendering strategy is more applicable to the rendering of image nodes ASImageNode:

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if(! rasterizing &&self.shouldRasterizeDescendants) {
    # : Rasterize
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    id drawParameters = [self drawParameters];

    displayBlock = ^id{
      UIImage *result = nil;
      if (flags.implementsInstanceImageDisplay) {
        result = [self displayWithParameters:drawParameters isCancelled:isCancelledBlock];
      } else {
        result = [[self class] displayWithParameters:drawParameters isCancelled:isCancelledBlock];
      }
      return result;
    };
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
    # : Provide context, use CG drawing
  }

  return [displayBlock copy];
}Copy the code

Through – displayWithParameters: isCancelled: Execution returns a picture, but the draw here is also inseparable from the Core Graphics of some of the C function, will you be in – [ASImageNode displayWithParameters: isCancelled:] see for the use of CG, It uses drawParameters to modify and draw the image object it holds.

Drawing in CG

Text rendering generally in – drawRect: withParameters: isCancelled: isRasterizing:, this method only provides a suitable for drawing the context, the method not only can draw text, is to draw text here are common:

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if(! rasterizing &&self.shouldRasterizeDescendants) {
    # : Rasterize
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    # : Draw UIImage
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
      if(! rasterizing) {UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
      }

      if (flags.implementsInstanceDrawRect) {
        [self drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
      } else{[[self class] drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
      }

      UIImage *image = nil;
      if(! rasterizing) { image =UIGraphicsGetImageFromCurrentImageContext(a);UIGraphicsEndImageContext(a); }return image;
    };
  }

  return [displayBlock copy];
}Copy the code

The above code is similar to part 1, except that there is no rasterization of the subview; Code depending on the situation will decide whether or not to open a new context, then through – drawRect: withParameters: isCancelled: isRasterizing: method implementation.

Managing draw transactions

ASDK provides a private management mechanism, consists of three parts _ASAsyncTransactionGroup, _ASAsyncTransactionContainer and _ASAsyncTransaction, these three each have different functions:

  • _ASAsyncTransactionGroupA callback is registered to the Runloop at initialization, and at the end of each Runloop, the callback is performed to commitdisplayBlockResult of execution
  • _ASAsyncTransactionContainerFor the currentCALayerContainers are provided for holding transactions and for getting new ones_ASAsyncTransactionConvenience methods for instances
  • _ASAsyncTransactionEncapsulate asynchronous operations into lightweight transaction objects, using C++ code to encapsulate GCD

From the above section, we have the displayBlock for drawing, and then we need to add the block to the drawing transaction:

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
  ...

  if (asynchronously) {
    CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
    _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
    [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
  } else{... }}Copy the code

The first two lines of code get an instance of _ASAsyncTransaction, which is contained in a layer hash table, Last call instance methods – [_ASAsyncTransaction addOperationWithBlock: priority: queue: completion:] will place for drawing displayBlock added to the background in parallel in the queue:

+ (dispatch_queue_t)displayQueue {
  static dispatch_queue_t displayQueue = NULL;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
  });

  return displayQueue;
}Copy the code

This queue is a parallel queue with a DISPATCH_QUEUE_PRIORITY_HIGH priority to ensure that UI rendering takes place before other asynchronous operations are performed, And – [_ASAsyncTransaction addOperationWithBlock: priority queue: completion:] will be initialized in ASDisplayNodeAsyncTransactionOperation And then pass in the completionBlock, calling back at the end of the drawing:

- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block priority:(NSInteger)priority queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion {
  ASDisplayNodeAssertMainThread();

  [self _ensureTransactionData];

  ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
  [_operations addObject:operation];
  _group->schedule(priority, queue, ^{
    @autoreleasepool{ operation.value = block(); }}); }Copy the code

The schedule method is a c + +, it will be to ASAsyncTransactionQueue: : Group distributed in a block, the block is executed in displayBlock, then put the results to the operation. The value:

void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
  ASAsyncTransactionQueue &q = _queue;
  ASDN::MutexLocker locker(q._mutex);

  DispatchEntry &entry = q._entries[queue];

  Operation operation;
  operation._block = block;
  operation._group = this;
  operation._priority = priority;
  entry.pushOperation(operation);

  ++_pendingOperations;

  NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;

  if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
    --maxThreads;

  if (entry._threadCount < maxThreads) {
    bool respectPriority = entry._threadCount > 0;
    ++entry._threadCount;

    dispatch_async(queue, ^{
      while(! entry._operationQueue.empty()) { Operation operation = entry.popNextOperation(respectPriority); {if (operation._block) {
            operation._block();
          }
          operation._group->leave();
          operation._block = nil;
        }
      }
      --entry._threadCount;

      if (entry._threadCount == 0) { q._entries.erase(queue); }}); }}Copy the code

ASAsyncTransactionQueue: : GroupImpl its implementation is actually on the GCD encapsulation, and add some functions of maximum concurrency, thread lock. Through dispatch_async will block distribution to the queue, immediately implement the block, the data back to the ASDisplayNodeAsyncTransactionOperation instance.

The callback

When _ASAsyncTransactionGroup calls the mainTransactionGroup class method to get a singleton, Through + [_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver] into the Runloop register callback:

+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup {
  static CFRunLoopObserverRef observer;
  CFRunLoopRef runLoop = CFRunLoopGetCurrent(a);CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit);
  CFRunLoopObserverContext context = {0, (__bridge void *)transactionGroup, &CFRetain, &CFRelease.NULL};

  observer = CFRunLoopObserverCreate(NULL, activities, YES, INT_MAX, &_transactionGroupRunLoopObserverCallback, &context);
  CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
  CFRelease(observer);
}Copy the code

The above code will be when they are about to exit the Runloop or Runloop began dormancy execute callback _transactionGroupRunLoopObserverCallback, and this callback method is the entrance to a common thread:

static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
  ASDisplayNodeCAssertMainThread(a); _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info; [group commit]; }Copy the code

In the previous section, we only committed the drawing code to a concurrent process in the background, but here we commit the result, which starts drawing at the end of each Runloop, The call stack for the -[_operationCompletionBlock Commit] method helps us understand how the content is committed and how the Layer held by Node is passed back:

-[_ASAsyncTransactionGroup commit]
    -[_ASAsyncTransaction commit]
        ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t)
            _notifyList.push_back(GroupNotify)Copy the code

-[_ASAsyncTransactionGroup COMMIT] completes the commit of the drawn transaction, while -[_ASAsyncTransactionGroup COMMIT] calls notify, Call the block execution -[_ASAsyncTransaction completeTransaction] method passed in here after the displayBlock execution in the previous section has finished:

- (void)commit {
  ASDisplayNodeAssertMainThread();
  __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);

  _group->notify(_callbackQueue, ^{
    ASDisplayNodeAssertMainThread();
    [self completeTransaction];
  });
}Copy the code

We examine in chronological order how methods are called and how blocks are executed before the above blocks are executed; Distributing map transaction that will have to return to the part, in ASAsyncTransactionQueue: : GroupImpl: : the schedule method, using the distributed dispatch_async will block:

void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
  ...
  if (entry._threadCount < maxThreads) {
    ...    
    dispatch_async(queue, ^{
      ...
      while(! entry._operationQueue.empty()) { Operation operation = entry.popNextOperation(respectPriority); { ASDN::MutexUnlocker unlock(q._mutex);if (operation._block) {
            operation._block();
          }
          operation._group->leave();
          operation._block = nil; }}... }); }}Copy the code

After displayBlock is executed, the group leave method is called:

void ASAsyncTransactionQueue::GroupImpl::leave() {
  if (_pendingOperations == 0) {
    std::list<GroupNotify> notifyList;
    _notifyList.swap(notifyList);

    for (GroupNotify & notify : notifyList) {
      dispatch_async(notify._queue, notify._block); }}}Copy the code

The -[_ASAsyncTransaction completeTransaction] method is finally executed:

- (void)completeTransaction {
  if(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) ! = ASAsyncTransactionStateComplete) {BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled);
    for (ASDisplayNodeAsyncTransactionOperation *operation in_operations) { [operation callAndReleaseCompletionBlock:isCanceled]; } __atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST); }}Copy the code

The last of the last – [ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:] method performs the callback will be executed displayBlock results returned CALayer:

- (void)callAndReleaseCompletionBlock:(BOOL)canceled; {
  if (_operationCompletionBlock) {
    _operationCompletionBlock(self.value, canceled);
    self.operationCompletionBlock = nil; }}Copy the code

It was – [ASDisplayNode (AsyncDisplay) displayAsyncLayer: asynchronously:] the methods of constructing completionBlock:

asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
  ASDisplayNodeCAssertMainThread(a);if(! canceled && ! isCancelledBlock()) {UIImage *image = (UIImage *)value;
    BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
    if (stretchable) {
      ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image);
    } else {
      _layer.contentsScale = self.contentsScale;
      _layer.contents = (id)image.CGImage;
    }
    [self didDisplayAsyncLayer:self.asyncLayer]; }};Copy the code

A lot of the data passing in this part is done through blocks, the submission of transactions from Runloop, and the addition of blocks through notify, all in order to draw the result back to CALayer. This is the end of the ASDK’s process of drawing the view’s content.

conclusion

ASDK optimizes the rendering process in three parts: raster subview, rendering image and rendering text.

It intercepts the notification of the -willMoveToWindow: method when the view joins the hierarchy, and then manually calls -setNeedsdisplay to force all calayers to perform -display updates;

At the end of each Runloop, -commit the completed transaction and pass it directly back to the corresponding layer.content as an image to complete the content update.

From the perspective of its implementation, it does solve a lot of expensive CPU and GPU operations, effectively speed up the drawing and rendering of views, and ensure smooth execution of the main thread.

References

  • How VSync works, and why people loathe it
  • Imaginative: Why does it flow at 60 FPS?
  • IOS tips for keeping the interface smooth
  • CADiplayLink Class Reference – Developer- Apple
  • CPU vs GPU · iOS Core animation advanced skills
  • Understand UIView drawing
  • Introduce to AsyncDisplayKit
  • AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling
  • Layer Trees vs. Flat Drawing — Graphics Performance Across iOS Device Generations
  • Understand RunLoop in depth

other

Making Repo: iOS – Source Code – Analyze

Follow: Draveness dead simple

Source: draveness me/asdk – render…