What’s inside AsyncDisplayKit for iOS

Maybe you wont use it, but you can learn from it.​

I think most of you have been worked with UITableView/UICollectionView a lot, a lot of times before, right ?. And it’s living fine until one day, your product owner want to add some informations on cells or apply new design theme, … then suddenly, your tableview/collectionview become slowly, laggy scrolling.

It’s sad :( . But at least you know the reason. Your main thread is now too busy:

  • to calculate AutomaticDimension height for each cell.
  • to resolve complex constraints to layout contents in cells.
  • to render images, clip corners, create shadows for each cell.

AsyncDisplayKit (ASDK) was born from there. Yeh! to rescue you.

Concept

The concept of ASDK is simple:

To keep its user interface smooth and responsive, we move as much as possible UI-works in background thread. Those works are extracted into ASDisplayNode class, then leave the main thread less work.

p/s: If you’re product owner of the app, or the manager, you can stop here and ask your developers to read the remaining :v

Which UI-works can run off the main-thread?

Image Decoding: Yes, it is. Maybe you don’t know but image files in .png, .jpg are compressed-ones. It’s then decoded into pure Bitmap matrix before upload into GPU’s buffer.

Assign .productImage = [UIImage imageNamed:@"a.png"] in cells, in fact, take main-thread down. ( but it is your favorite code, right? :) )

My Pinterest Home :)

Pinterest, one of apps that apply ASDK, have a complex Home screen with collection of high-resolution images, still gain smooth-scrolling due to ASImageNode.

Now take a look at ASImageNode class to see how ASDK decode images:

+ (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
{
UIGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0);
....
//draw .png/.jpg into Bitmap using Core Graphics
@synchronized
(image) {
[image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1];
}
....
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
....
}

Aside from it, there’re extra image stuffs can slowdown the main-thread:

  • Corner-rounded: see each Pin’s image have 4 rounded-corners? .cornerRadius triggers off-screen rendering on every frame.
  • Effects: blur, shadow, … These effects cause the CPU/GPU to create additional buffers then process images.
  • Scale down from original image from the server.

With all of those stuffs, ASDK reserve for you an imageModificationBlock method which will be run in background thread.

_imageNode.imageModificationBlock = ^(UIImage *image) {
UIImage *newImage = [image applyBlurWithRadius:30 tintColor:aColor saturationDeltaFactor:1.8 maskImage:nil];
return newImage;
};
What if you don’t use ASDK to process image:
After downloading images, do all ASNetworkImage & imageModificationBlock code in background thread then return pure bitmap to cells. Remember, do it in CoreGraphic framework, not in .cornerRadius, .shadowRadius.

Layout off main-thread
When you scroll on a UICollectionView, AutoLayout will resolve constraints to calculate height of each cells and layout cell’s contents. The Cassowary algorithm that AutoLayout use become slowly (Big-O exponential time), special incase the cell is dynamic, contain large no of subviews and have resizable labels.

The idea of ASDK is to run all heavy calculation of layout in background thread, resolve complex constraints, and result in final node’s frames. Then at the time UIKit request layoutSubviews, ASDK just run setFrame which is lightweight.

Don’t believe? Let take a look at ASDisplayNode.mm :

Layout off main-thread

As you see, ASDisplayNode.mm pre-calculate layout of nodes within batchLayoutNodesFromContexts in a concurrent queue, off the main-thread.

Then at the time UIKit request layoutSubviews in main-thread, only work is

layoutSubviews in main-thread

Before go further with Layout in detail and others UI-processing works, let consider:

How can ASDisplayNode run UI-works off the main-thread?

ASDisplayNode’s stream (Credits to my Preview app! I tried to make it as beautiful as possible)

Take Pinterest as an example, Pinterest Home screen is an ASCollectionNode, each Pin is an ASCellNode which contain ASNetworkImageNodes, ASTextNodes. The lifecycle of a Pinterest’s Pin will go through states from Initial to Visible on screen :

  • Initial state: the ASCollectionNode run batch-calculate on Pin nodes. Pin nodes get layout-specs from sub-nodes then calculate layout for it’s sub-views. It result in _calculatedDisplayNodeLayout which contain all node’s frame.
  • Preload state: network-image node of the pin trigger background thread to lazily load on the image url , result in an local UIImage.
  • Display state: at this state, the node draw the image from previous state into CoreGraphic’s bitmap, result in decoded-content which is ready for visible in main-thread.
  • Visible state: almost in main-thread. The layoutSublayouts get the _calculatedDisplayNodeLayout from Initial state in order to set frames. The image node render the decoded-content.
What if the node enter next state while current state is not finished yet?
If image node enter Display state while the image is not downloaded yet, it will show placeholder. After downloaded, the image trigger display step again.
If collection node enter Display state while batch-calculation is not finished yet, it run layout in main-thread (at first appear only). It’s intelligent enough to calculate visible nodes first, then display/preload nodes. Also remember, calculations is run concurrently in multi-thread :) .

While Asynchronous mean do stuffs in background thread, so to free the main-thread off the heavy works, Concurrently can utilize CPU multiple-cores to reduce the time to be available in main-thread.

AsyncDisplayKit’s Layout

If you’re familiar with CSS box model, ASDK’s layout is a subset of CSS layout (padding, position: relative,..) and FlexBox .

If you’re familiar with UIStackView, ASDK’s ASStackLayoutSpec is something like UIStackView in spreading subviews, plus support flex-spacing and alignment.

If you still don’t figure out how ASDK’s layout look like, let check quickstart, some examples and FlexBox.

Layout Specifications

http://asyncdisplaykit.org/docs/layout2-quickstart.html

A layout system contains components that define how to layout views on screen. ASDK layout’s components include:

  • ASStackLayoutSpec: at the heart of ASDK layout is stack spec. Stack spec spread its subviews along its horizontal width or vertical height. Different from UIStackView always attract “gravity” subviews at start, stack spec allow attract “gravity” subviews at center, end, and even equal distributed spacing.
https://www.slideshare.net/anjlab/asyncdisplaykit
  • ASInsetLayoutSpec: like CSS padding style, allow you to inset inside.
  • ASRatioLayoutSpec, ASRelativeLayoutSpec: While ASRatioLayoutSpec allow you to think in ratio — not in fixed width-height, ASRelativeLayoutSpec allow you to think in relative position — not in absolute position.
  • ASOverlayLayoutSpec: stretch a layout spec to cover another one.
  • And some other layout spec…

ASDK’s layout composite all layout specs above to define the layout of an screen.

Why this? AutoLayout is still there.

One of targets when ASDK design its layout system is to make it simple enough, not only to run fast but also easy to implement.

Compared to AutoLayout, ASDK require all nodes must have an intrinsic size or explicitly set its size. For example ASImageNode need to set .preferedSize explicitly.

Compared to AutoLayout, ASDK don’t allow you to define constraint between sub-nodes with parent of parent and above. For example, you can’t say: “I don’t know my image’s size, but let make its width equal to width of its parent of parent”. And also can’t define the size-constraint between sub-nodes.

Two above requirements make the layout calculation much simpler. The ASLayoutSpec's calculateLayoutThatFits: method only need one pass to get all informations from sub-nodes then calculate final frame, don’t need to care about constraints with parents.

//ASRatioLayoutSpec.mm
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
...
// Choose the size closest to the desired ratio.
const auto &bestSize = ...;
const ASSizeRange childRange = (bestSize == sizeOptions.end()) ? constrainedSize : ASSizeRangeIntersect(constrainedSize, ASSizeRangeMake(*bestSize, *bestSize));
const CGSize parentSize = (bestSize == sizeOptions.end()) ? ASLayoutElementParentSizeUndefined : *bestSize;
ASLayout *sublayout = [self.child layoutThatFits:childRange parentSize:parentSize];
sublayout.position = CGPointZero;
return [ASLayout layoutWithLayoutElement:self size:sublayout.size sublayouts:@[sublayout]];
}

Another target is to make ASDK’s layout powerful enough to define even complex layout and support multi-resolution. With only ASStackLayoutSpec, ASOverlayLayoutSpec, ASCenterLayoutSpec, we can define the layout like Pinterest.

Why LayoutSpec is just a “spec”?

In order to run layout system off main-thread, ASDK need to take the layout specifications out of views. Back to AutoLayout ,when you create a constraint you need to initialize it with 2 UIViews. Because UIViews belong to main-thread, you can’t calculate layout off main-thread.

Another benefit is that you don’t need to creating “container” UIView in middle just to align, spacing, overlay… informative-views. Remember when you group some UIViews into a container view then center that container view on screen?

What if you don’t use ASDK to layout
- Reduce no of UIViews in cell’s content.
- Reduce the deep of view hierarchy in cell’s content.
- Pre-calculate cell’s height (see more here), label’s size, image’s size (after downloading model’s page) manually, cache it in a model then use in heightForCellAtIndex: and layoutSubViews: (don’t use AutoLayout). 
- Use ASDK is the last choice.

Others UI-processing works

Text rasterization off mainthread

ASDK team had big effort to build its own TextKit engine. TextKit engine calculate the frame for texts and draw it into an CoreGraphic bitmap before return to main-thread to render.

ASTextNode.mm
What if you don’t use ASDK:
You copy all TextKit folder from ASDK into your project =]]. But take care of license :v

CornerRadius in detail
See Pin’s images in Pinterest Home screen ? It is clipped in 4 corners to reveal the collection’s white-background underneath.

_photoNode.willDisplayNodeContentWithRenderingContext = ^(CGContextRef context) {
CGRect bounds = CGContextGetClipBoundingBox(context);
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:10] addClip];
};

This approach is called pre-composited alpha corners. A drawback is: it introduce tiny overhead due to alpha-blending in GPU with the content of collection’s white-background.

Another approach is that we composite collection’s white-background into the image, called pre-composited opaque corners. ASDK support it with the method to generate 4 overlay corners in UIImage+ASConvenience.m:

+ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(nullable UIColor *)cornerColor fillColor:(UIColor *)fillColor AS_WARN_UNUSED_RESULT;
_photoNode.willDisplayNodeContentWithRenderingContext = ^(CGContextRef context) {
CGRect bounds = CGContextGetClipBoundingBox(context);
UIImage *overlay = [UIImage as_resizableRoundedImageWithCornerRadius:10 cornerColor:[UIColor whiteColor] fillColor:[UIColor clearColor]];
[overlay drawInRect:bounds];
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:10] addClip];
};

Phew, enough for today. Let take a break, eat something, watch Goblin film.

What’s in part 2 of this series about ASDK ?

In part 2, I will review ASDK in : Architecture, Patterns, Preloading, Automatic SubNode Management.

Read More

If you want to learn how to use ASDK, check 2-parts tutorial on raywenderlich.com:
https://www.raywenderlich.com/124311/asyncdisplaykit-2-0-tutorial-getting-started

https://youtu.be/0bOdPUvSzG0

Like what you read? Give Le Tien Dung a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.