Use asdk performance tuning – improve the rendering performance of IOS interface

Time:2021-7-29

This series of articles will be helpful in several aspectsASDKIn the aspect of performance tuning, the implementation of the strategy is analyzed to help readers understand how asdk makes the complex UI interface reach the refresh rate of 60 FPS; This article will explain asdk’s optimization of rendering process from the rendering level of view, and give an overview of asdk.

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

In most scenarios, using more complex high-performance code to replace the available code will often lead to the decline of code maintainability. Therefore, it is more necessary for our developers to have a clear understanding of the time point and reason of optimization to avoid the problems caused by excessive optimization.

Developers familiar with IOS development know that most of the performance problems in IOS are blocking the main thread, resulting in perceptible delays in user interaction feedback.

Use asdk performance tuning - improve the rendering performance of IOS interface

In detail, there are three reasons:

  1. UI rendering takes a long time and the results cannot be submitted on time;

  2. Some needsIntensive computingThe processing of is executed in the main thread, causing the main thread to be blocked and unable to render the UI interface;

  3. The response of the network request is slow due to the problem of network status, and the UI layer cannot render because there is no model return.

The above problems will affect the performance of the application. The most common performance isUITableViewNot reached during sliding60 FPS, users can feel the obvious Caton.

Screen rendering

I believe that most developers who click this article know what FPS is, so what if we can optimize our app to reach 60 FPS? Before we know the method, let’s step back and ask another question: how does the screen render?

For the first question, you may need several articles to answer. I hope the whole series of articles can give you a satisfactory answer. three

CRT and LCD

The rendering of the screen may be fromCRT (cathode ray tube) displayandLCD (liquid crystal display) displayTalk about it.

Use asdk performance tuning - improve the rendering performance of IOS interface

CRT display is an ancient technology. It uses a cathode electron gun to emit electrons. Under the action of cathode high voltage, electrons are shot from the electron gun to the fluorescent screen to make the phosphor glow and display the image on the screen. That is why it will change color when a magnet is close to the screen of some old TV sets.

FPS is the refresh rate of CRT display. The electron gun will refresh the content on the display 60 – 100 times per second, even if there is no change in our opinion.

Use asdk performance tuning - improve the rendering performance of IOS interface

However, the principle of LCD is very different from CRT. The imaging principle of LCD is related to optics:

  • When no voltage is applied, the light will move forward and rotate 90 ° along the gap of liquid crystal molecules, so the light can pass through;

  • After the voltage is applied, the light advances in a straight line along the gap of liquid crystal molecules and is blocked by the filter.

If you can climb over the wall, I believe the following video will better help you understand the working principle of LCD:

Use asdk performance tuning - improve the rendering performance of IOS interface

Although the imaging principle of LCD is very different from CRT, the color of each pixel can be changedWhen changes are neededTo change the voltage, that is, the refresh frequency is not required. However, due to some historical reasons, the LCD still needs to obtain new images from the GPU according to a certain refresh frequency for display.

Screen tearing

But the display is only used to display the image on the screen. Who is the provider of the image? Images are often provided by GPU.

This leads to another problem. Since the frequency of the image generated by the GPU is not related to the refresh frequency of the display, what if the GPU is not ready to display the image when the display is refreshed; Or the rendering speed of GPU is too fast and the display has no time to refresh. What should I do if GPU has already started rendering the next frame of image?

Use asdk performance tuning - improve the rendering performance of IOS interface

If these two problems cannot be solved, the problem in the figure above will appearScreen tearing (screen teaching) phenomenon, one part of the screen displays the content of the previous frame, and the other part displays the content of the next frame.

We use two examples to illustrate two situations where screen tearing may occur:

  • If the refresh rate of the display is 75 Hz and the rendering speed of the GPU is 100 Hz, in the interval between two screen refreshes, the GPU will render 4 / 3 frames, and the subsequent 1 / 3 frames will cover the rendered frame stack, which will eventually lead to screen tearing effect at 1 / 3 or 2 / 3 of the screen;

  • The rendering speed of the GPU is lower than that of the display, for example, 50 Hz. In the interval between two screen refreshes, the GPU will only render 2 / 3 frames, and the remaining 1 / 3 will come from the previous frame. It is exactly the same as the above results, and the tearing effect will appear at the same position.

Here, some people will say that if the refresh rate of the display is exactly the same as the rendering speed of the GPU, it should solve the problem of screen tearing? Not really. The process of the display copying frames from the GPU still takes some time. If the screen is refreshed when copying images, it will still cause the screen tear problem.

Use asdk performance tuning - improve the rendering performance of IOS interface

Introducing multiple buffers can effectivelyrelieveThe screen is torn, that is, one is used at the same timeframe buffer(frame buffer) and multiplelook aside buffer (back buffer); Every time the display requests content, it willframe bufferTake the image out of the and render it.

Although buffers can alleviate these problems, they cannot solve them; If the backup buffer is drawn and the image in the frame buffer is not rendered, the image in the backup buffer will cover the frame buffer and still cause the screen to tear.

To solve this problem, we need the help of another mechanism, namely vertical synchronization (V-sync for short).

V-Sync

The main function of V-sync is to ensureOnly after the image in the frame buffer is rendered, the contents of the backup buffer can be copied to the frame bufferIdeally, V-sync would work this way:

Use asdk performance tuning - improve the rendering performance of IOS interface

Each time V-sync occurs, the CPU and GPU have completed the image processing and rendering, and the display can directly get the frames in the buffer. However, if the CPU or GPU takes a long time to process, the frame will be dropped:

Use asdk performance tuning - improve the rendering performance of IOS interface

When the V-sync signal is sent out, the CPU and GPU are not ready for the frame to be rendered, and the display will continue to use the current frameexacerbateThe display problem of the screen is, and the number of frames displayed per second will be less than 60.

Since frame dropping occurs many times, after V-sync is turned on, the rendering frequency of 40 ~ 50 FPS means that the picture frame rate output by the display will drop sharply from 60 FPs to 30 FPS. The reason will not be explained here. Readers can think for themselves.

In fact, the content about screen rendering has almost ended here. According to the principle of V-sync, optimizing application performance and improving app FPS can start from two aspects to optimize the processing time of CPU and GPU.

Readers can alsoTips for IOS to keep the interface smoothLearn more about this in this article.

Strategies for performance tuning

What are the CPUs and GPUs doing before each V-sync time point arrives? If we know what they are responsible for, we can improve performance by optimizing the code.

Use asdk performance tuning - improve the rendering performance of IOS interface

Many CPU operations delay the time when the GPU starts rendering:

  • Layout calculation – if your view level is too complex, or the view needs to be laid out repeatedly, especially when auto layout is used for automatic layout, it will have a serious impact on performance;

  • Lazy loading of view – in IOS, the view of the view controller will be loaded only when it is displayed on the screen;

  • Decompress pictures – IOS usually decodes pictures only when they are actually drawn. For a large picture, either directly or indirectlyUIImageViewOr when drawing to core graphics, you need to decompress the picture;

Broadly speaking, mostCALayerThe attributes of are drawn by GPU, such as image fillet, transformation and texture application; However, too much geometry, redrawing, off screen rendering and too large pictures will significantly reduce the performance of GPU.

The above content is fromCPU vs GPU · advanced skills of IOS Core Animation, you can have a deeper understanding of what the CPU and GPU do respectively in the above article.

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

AsyncDisplayKit

The first half of the article has talked about several strategies of performance tuning from the principle of screen rendering; andAsyncDisplayKitAccording to the above strategy, help us optimize the application performance.

Use asdk performance tuning - improve the rendering performance of IOS interface

Asyncdisplaykit (hereinafter referred to as asdk) is an IOS framework open source by Facebook, which can help the most complex UI interface maintain smooth and rapid response.

Asdk has experienced more than a year from development to open source. In fact, it is not a simple frameworkIt is a complex framework, it is more like the re implementation of UIKit, encapsulating the whole UIKit and calayer layer one by oneNodeMove expensive rendering, picture decoding, layout, and other UI operations out of the main threadIn this way, the main thread can respond to the user’s operation in time.

Many articles analyzing asdk will have such a figure to introduce the most basic concepts in the framework:

Use asdk performance tuning - improve the rendering performance of IOS interface

The most basic unit in asdk isASDisplayNode, every node is rightUIViewas well asCALayerAbstract. But withUIViewThe difference is,ASDisplayNodeIt is thread safe. It can complete initialization and configuration in the background thread.

If calculated according to the refresh rate of 60 FPS, the rendering time of each frame is only 16ms, and the rendering should be completed within 16msUIViewCPU and GPU are facing great pressure in the creation, layout, rendering and rendering of.

Use asdk performance tuning - improve the rendering performance of IOS interface

However, after the A5 processor, multi-core devices have become the mainstream. The original practice of putting all operations into the main thread can no longer adapt to the complex UI interface, soAsdk puts the time-consuming CPU operation and GPU texture rendering process into the background process, so that the main thread can quickly respond to user operations

Asdk optimizes app performance through unique rendering skills, layout system instead of AutoLayout, intelligent preloading and other modules.

Render pass for asdk

What methods are used in asdk to render views? This article will mainly analyze the rendering process to understand how the bottom layer of asdk can improve the rendering performance of the interface.

Rendering around in asdkASDisplayNodeThere are four main lines in the process:

  • initializationASDisplayNodeCorrespondingUIViewperhapsCALayer

  • Executes when the current view enters the view levelsetNeedsDisplay

  • displayWhen the method is executed, the drawing transaction is sent to the background thread;

  • Register asRunLoopObserver, at eachRunLoopCallback at end.

Loading of uiview and calayer

When we run a project that uses asdk,-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]It is always the first method called in asdk, and the reason why this method is executed is oftenASDisplayNodeCorrespondingUIViewandCALayerCited:

- (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;
}

This involves an important concept in asdk. IfASDisplayNodeyeslayerBackedYes, it does not render the correspondingUIViewTo 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;
}

becauseUIViewandCALayerAlthough they can be used to display content, becauseUIViewCan be used to handle user interaction, so if you don’t need to useUIViewFeatures, direct useCALayerRendering can save a lot of rendering time.

If you have used Xcode to view the hierarchy of views, you should know,UIViewThere are levels in the debug view hierarchy; andCALayerNo, all doors are displayed on same plane.

Of the above methods-[ASDisplayNode _layerToLoad]as well as[ASDisplayNode _viewToLoad]Will only be based on the current nodelayerClassperhapsviewClassInitialize an object.

Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device GenerationsThis article comparesUIViewandCALayerThe rendering time of the.

Use asdk performance tuning - improve the rendering performance of IOS interface

-[ASDisplayNode asyncLayer]Just for the currentnodeHeldlayerEncapsulate to ensure that a_ASDisplayLayerExamples of:

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

most important of all-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]Method sets the current node toasyncLayerAgent, which will be used laterASDisplayNodebyCALayerRender content.

View Hierarchy

After initialization, whenASDisplayNodeWhen it is first added to the view hierarchy,-[_ASDisplayView willMoveToWindow:]Will be called.

_ Asdisplayview and_ ASDisplayLayer

_ASDisplayViewand_ASDisplayLayerAre private classes, and the corresponding relationship between them is actually the same asUIViewAndCALayerExactly the same.

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

_ASDisplayViewOverride many methods related to view level changes:

  • -[_ASDisplayView willMoveToWindow:]

  • -[_ASDisplayView didMoveToWindow]

  • -[_ASDisplayView willMoveToSuperview:]

  • -[_ASDisplayView didMoveToSuperview]

They are used to notify the corresponding when the hierarchy of the view changesASDisplayNodeRespond accordingly, such as-[_ASDisplayView willMoveToWindow:]Method is called when the view is added to the hierarchy:

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

setNeedsDisplay

If the current view is not in the view level, it will pass through_nodeExample method of-[ASDisplayNode __enterHierarchy]Add view level:

- (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 display of layer
    }
}

_flagsyesASDisplayNodeFlagsStructure to mark the currentASDisplayNodeFor some bool values, such as asynchronous display, rasterized sub view, etc., you don’t need to know what they have. It’s enough to understand the literal meaning of these values.

The first half of the above method is only right_flagsIf you need to rasterize the child views of the current view, that isCompresses all its child views with the current view into one layer, will be called recursively to these views-[ASDisplayNode willEnterHierarchy]Method notification current status:

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

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

and-[ASDisplayNode willEnterHierarchy]The of the current node is modifiedinterfaceStatereachASInterfaceStateInHierarchyIndicates that the current node is not included in thecellOr something else, but inwindowYes.

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

When the current node needs to be displayed on the screen, if its contentcontentsIf it is empty, it will call-[CALayer setNeedsDisplay]Method willCALayerIf it is marked as dirty, notify the system that the view needs to be redrawn in the next drawing cycle:

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

        #Flag of marking 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;
                [CATransaction commit];
                [layer addSublayer:_placeholderLayer];
            }
        }
    }
}

In willCALayerOnce marked dirty, it is executed in the draw loop-[CALayer display]Method to draw the content to be displayed; If the current view needs some bitmap, it will be the current bitmap in the code herenodeCorrespondinglayerAdd a placeholder layer of the appropriate color.

Use asdk performance tuning - improve the rendering performance of IOS interface

Dispatch asynchronous drawing transaction

Call in the previous section-[CALayer setNeedsDisplay]Method marks the current node as dirty, and all the nodes that need to be redrawn will be redrawn at the next drawing cycleCALayerimplement-[CALayer display], which is also the entrance of the method to be analyzed in this section:

- (void)display {
  [self _hackResetNeedsDisplay];

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

  [self display:self.displaysAsynchronously];
}

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

-[_ASDisplayLayer display]
    -[_ Asdisplaylayer display:] // submit the drawing work to asdisplaynode for processing
        -[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);

-[_ASDisplayLayer display]In fact, one will be created in the call stackdisplayBlock, it is actually a process of image rendering using core graphics, and the whole rendering process is managed in the form of transactions; anddisplayBlockIt will be distributed by GCD to the background concurrent process for processing.

Call the second method in the stack-[_ASDisplayLayer display]Will hand over the work of asynchronous drawing to its ownasyncDelegate, that isPart ISet inASDisplayNode

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

ASDisplayNode(AsyncDisplay)

A part is omitted here-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]Method implementation:

- (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();
    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);
  }
}

The omitted code context is very clear,-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]Returns a fordisplayBlock, and then construct acompletionBlock, execute at the end of drawing, and set the current in the main threadlayerContent of the.

If the current rendering is asynchronous, thedisplayBlockWrap it into a transaction and add it to the queue for execution. Otherwise, the current block will be executed synchronously and executedcompletionBlockCallback, notificationlayerUpdate the display.

It is clear here that we are more concerned about the part of asynchronous rendering, because this part is the key to improving the efficiency of asdk; And that’s what you get fromdisplayBlockThe method is beginning to understand.

Construction of displayblock

displayBlockThe creation of is generally divided into three different ways:

  1. Compress the subview of the current view into one layer and draw it on the current page

  2. use- displayWithParameters:isCancelled:Return aUIImage, for image nodesASImageNodeDraw

  3. use- drawRect:withParameters:isCancelled:isRasterizing:Draw text nodes in CG contextASTextNode

These three methods optimize the rendering speed of the view through asdk, and these operations will finally be thrown into the background concurrent thread for processing.

The following three parts of the code have been deleted, including canceling drawing, notifying the agent, controlling the number of concurrency and the code for debugging.

Rasterize subviews

If the current view needs to rasterize child views, the following construction method will be enabled to create a block, which will recursively draw the child views 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();
      UIGraphicsEndImageContext();

      return image;
    };
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    #: draw uiimage
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
    #: provide context and use CG for drawing
  }

  return [displayBlock copy];
}

Called during the compression of the view hierarchy-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]Method to get all of the child viewsdisplayBlock, getUIGraphicsBeginImageContextWithOptionsAfter the required parameters, create a new context and perform all the operationsdisplayBlockAfter drawing the of a subview to the current layer, useUIGraphicsGetImageFromCurrentImageContextTake out the contents of the layer and return.

-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]Its main function is to use core graphics to draw parameters such as background color, affine transformation, position size and fillet into the current context, and this process is recursive until there is no child node or no need to draw child nodes.

Draw picture

displayBlockThe second rendering strategy is more suitable for picture nodesASImageNodeDrawing of:

- (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) {
    #: rasterization
  } 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 and use CG for drawing
  }

  return [displayBlock copy];
}

adopt- displayWithParameters:isCancelled:The execution of returns a picture, but the drawing here is also inseparable from some C functions of core graphics. You will-[ASImageNode displayWithParameters:isCancelled:]See the use of CG in, it will usedrawParametersTo modify and draw your ownimageObject.

Drawing with CG

Text is usually drawn in- drawRect:withParameters:isCancelled:isRasterizing:This method only provides a suitable context for drawing. This method can not only draw text, but also draw text here

- (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) {
    #: rasterization
  } 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();
        UIGraphicsEndImageContext();
      }

      return image;
    };
  }

  return [displayBlock copy];
}

The above code is similar to the first part, except that the sub view will not be rasterized here; The code will decide whether to reopen a new context according to the situation, and then pass- drawRect:withParameters:isCancelled:isRasterizing:Method to achieve rendering.

Manage drawing transactions

Asdk provides a private transaction management mechanism, which consists of three parts_ASAsyncTransactionGroup_ASAsyncTransactionContaineras well as_ASAsyncTransaction, each of them has different functions:

  • _ASAsyncTransactionGroupDuring initialization, a callback will be registered in the runloop. At the end of each runloop, the callback will be executed to submitdisplayBlockResults of execution

  • _ASAsyncTransactionContainerFor currentCALayerProvides a container for holding transactions and provides a way to get new transactions_ASAsyncTransactionConvenient method of example

  • _ASAsyncTransactionThe asynchronous operation is encapsulated into a lightweight transaction object, and the GCD is encapsulated by C + + code

From the above section, we have obtained the data for drawingdisplayBlockThen you 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 {
    ...
  }
}

The first two lines of code are to get_ASAsyncTransactionInstance, which will be included in alayerIn the hash table, the last instance method is called.-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]Will be used for drawingdisplayBlockAdd to background parallel 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;
}

This queue is a parallel queue, and the priority isDISPATCH_QUEUE_PRIORITY_HIGHMake sure that the rendering of the UI takes place before other asynchronous operations are performed, and-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]Will be initialized inASDisplayNodeAsyncTransactionOperationAnd then pass incompletionBlock, callback at the end of 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();
    }
  });
}

scheduleMethod is a C + + method that willASAsyncTransactionQueue::GroupA block will be distributed in the block and executeddisplayBlockAnd then pass the results tooperation.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);
      }
    });
  }
}

ASAsyncTransactionQueue::GroupImplIn fact, the implementation is to encapsulate GCD and add some functions of maximum concurrency and thread lock. adoptdispatch_asyncDistribute blocks toqueueExecute block immediately to return the dataASDisplayNodeAsyncTransactionOperationexample.

Callback

stay_ASAsyncTransactionGroupcallmainTransactionGroupClass method, the+[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver]Register callback in runloop:

+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup {
  static CFRunLoopObserverRef observer;
  CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  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);
}

The above code will execute the callback when runloop is about to exit or runloop begins to sleep_transactionGroupRunLoopObserverCallback, and this callback method is the entry of this main line:

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

In the previous section, only the drawing code will be submitted to the background concurrent process, and the results will be submitted here, that is, the drawing content will be started at the end of each runloop cycle, and-[_operationCompletionBlock commit]Method can help us understand how the content is submitted and returnednodeHeldlayerOf:

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

-[_ASAsyncTransactionGroup commit]Method completes the submission of the drawing transaction-[_ASAsyncTransaction commit]Will be called innotifyMethod, in the previous sectiondisplayBlockThe execution of the incoming block is executed at the end of execution.-[_ASAsyncTransaction completeTransaction]method:

- (void)commit {
  ASDisplayNodeAssertMainThread();
  __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);
    
  _group->notify(_callbackQueue, ^{
    ASDisplayNodeAssertMainThread();
    [self completeTransaction];
  });
}

We analyze how the method is called and how the block is executed before the above block is executed in chronological order; This has to go back to the part of distributing drawing transactions, inASAsyncTransactionQueue::GroupImpl::scheduleMethod, usingdispatch_asyncBlock will be distributed:

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;
        }
      }
      ...
    });
  }
}

staydisplayBlockCalled after executiongroupYesleavemethod:

void ASAsyncTransactionQueue::GroupImpl::leave() {
  if (_pendingOperations == 0) {
    std::list<GroupNotify> notifyList;
    _notifyList.swap(notifyList);
    
    for (GroupNotify & notify : notifyList) {
      dispatch_async(notify._queue, notify._block);
    }
  }
}

It’s finally implemented here- commitThe block added in, that is-[_ASAsyncTransaction completeTransaction]method:

- (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);
  }
}

Finally, finally,-[ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:]The callback method was executeddisplayBlockThe result of execution returned to calayer:

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

That is, in-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]MethodcompletionBlock

asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
  ASDisplayNodeCAssertMainThread();
  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];
  }
};

A large amount of data transfer in this part is carried out through blocks, transaction Submission from runloop, andnotifyMethod is added to return the drawn resultCALayerObject, and here it can be said that the drawing process of the entire asdk for the view content is over.

summary

Asdk has three parts to optimize the drawing process: rasterizing sub views, drawing images and drawing text.

It intercepts notifications when the view is added to the hierarchy- willMoveToWindow:Method, and then call it manually- setNeedsDisplay, force allCALayerimplement- displayUpdate content;

Then throw all the above operations into the background concurrent thread, register a callback in the runloop, and update the completed transactions at the end of each runloop- commit, directly return the corresponding in the form of a picturelayer.contentIn, complete the update of the content.

From its implementation, it does solve many expensive CPU and GPU operations, effectively speeds up the rendering and rendering of views, and ensures the smooth execution of the main thread.

References

other

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · Github

Source: http://draveness.me/asdk-rend…