mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Revert "Remove old website folder from master branch" (#1818)
This commit is contained in:
150
docs/guide/1-introduction.md
Normal file
150
docs/guide/1-introduction.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Getting started
|
||||
permalink: /guide/
|
||||
next: guide/2/
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
AsyncDisplayKit's basic unit is the *node*. ASDisplayNode is an abstraction
|
||||
over UIView, which in turn is an abstraction over CALayer. Unlike views, which
|
||||
can only be used on the main thread, nodes are thread-safe: you can
|
||||
instantiate and configure entire hierarchies of them in parallel on background
|
||||
threads.
|
||||
|
||||
To keep its user interface smooth and responsive, your app should render at 60
|
||||
frames per second — the gold standard on iOS. This means the main thread
|
||||
has one-sixtieth of a second to push each frame. That's 16 milliseconds to
|
||||
execute all layout and drawing code! And because of system overhead, your code
|
||||
usually has less than ten milliseconds to run before it causes a frame drop.
|
||||
|
||||
AsyncDisplayKit lets you move image decoding, text sizing and rendering, and
|
||||
other expensive UI operations off the main thread. It has other tricks up its
|
||||
sleeve too... but we'll get to that later. :]
|
||||
|
||||
## Nodes as drop-in view replacements
|
||||
|
||||
If you're used to working with views, you already know how to use nodes. The
|
||||
node API is similar to UIView's, with some additional conveniences — for
|
||||
example, you can access common CALayer properties directly. To add a node to
|
||||
an existing view or layer hierarchy, use its `node.view` or `node.layer`.
|
||||
|
||||
AsyncDisplayKit's core components include:
|
||||
|
||||
* *ASDisplayNode*. Counterpart to UIView — subclass to make custom nodes.
|
||||
* *ASControlNode*. Analogous to UIControl — subclass to make buttons.
|
||||
* *ASImageNode*. Like UIImageView — decodes images asynchronously.
|
||||
* *ASTextNode*. Like UITextView — built on TextKit with full-featured
|
||||
rich text support.
|
||||
* *ASTableView* and *ASCollectionView*. UITableView and UICollectionView
|
||||
subclasses that support nodes.
|
||||
|
||||
You can use these as drop-in replacements for their UIKit counterparts. While
|
||||
ASDK works most effectively with fully node-based hierarchies, even replacing
|
||||
individual views with nodes can improve performance.
|
||||
|
||||
Let's look at an example.
|
||||
|
||||
We'll start out by using nodes synchronously on the main thread — the
|
||||
same way you already use views. This code is a familiar sight in custom view
|
||||
controller `-loadView` implementations:
|
||||
|
||||
```objective-c
|
||||
_imageView = [[UIImageView alloc] init];
|
||||
_imageView.image = [UIImage imageNamed:@"hello"];
|
||||
_imageView.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
|
||||
[self.view addSubview:_imageView];
|
||||
```
|
||||
|
||||
We can replace it with the following node-based code:
|
||||
|
||||
```objective-c
|
||||
_imageNode = [[ASImageNode alloc] init];
|
||||
_imageNode.backgroundColor = [UIColor lightGrayColor];
|
||||
_imageNode.image = [UIImage imageNamed:@"hello"];
|
||||
_imageNode.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
|
||||
[self.view addSubview:_imageNode.view];
|
||||
```
|
||||
|
||||
This doesn't take advantage of ASDK's asynchronous sizing and layout
|
||||
functionality, but it's already an improvement. The first block of code
|
||||
synchronously decodes `hello.png` on the main thread; the second starts
|
||||
decoding the image on a background thread, possibly on a different CPU core.
|
||||
|
||||
(Note that we're setting a placeholder background colour on the node, "holding
|
||||
its place" onscreen until the real content appears. This works well with
|
||||
images but less so with text — people expect text to appear instantly,
|
||||
with images loading in after a slight delay. We'll discuss techniques to
|
||||
improve this later on.)
|
||||
|
||||
## Button nodes
|
||||
|
||||
ASImageNode and ASTextNode both inherit from ASControlNode, so you can use them
|
||||
as buttons. Let's say we're making a music player and we want to add a
|
||||
(non-skeuomorphic, iOS 7-style) shuffle button:
|
||||
|
||||
[]({{ site.baseurl }}/assets/guide/1-shuffle.png)
|
||||
|
||||
Our view controller will look something like this:
|
||||
|
||||
```objective-c
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// attribute a string
|
||||
NSDictionary *attrs = @{
|
||||
NSFontAttributeName: [UIFont systemFontOfSize:12.0f],
|
||||
NSForegroundColorAttributeName: [UIColor redColor],
|
||||
};
|
||||
NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"shuffle"
|
||||
attributes:attrs];
|
||||
|
||||
// create the node
|
||||
_shuffleNode = [[ASTextNode alloc] init];
|
||||
_shuffleNode.attributedString = string;
|
||||
|
||||
// configure the button
|
||||
_shuffleNode.userInteractionEnabled = YES; // opt into touch handling
|
||||
[_shuffleNode addTarget:self
|
||||
action:@selector(buttonTapped:)
|
||||
forControlEvents:ASControlNodeEventTouchUpInside];
|
||||
|
||||
// size all the things
|
||||
CGRect b = self.view.bounds; // convenience
|
||||
CGSize size = [_shuffleNode measure:CGSizeMake(b.size.width, FLT_MAX)];
|
||||
CGPoint origin = CGPointMake(roundf( (b.size.width - size.width) / 2.0f ),
|
||||
roundf( (b.size.height - size.height) / 2.0f ));
|
||||
_shuffleNode.frame = (CGRect){ origin, size };
|
||||
|
||||
// add to our view
|
||||
[self.view addSubview:_shuffleNode.view];
|
||||
}
|
||||
|
||||
- (void)buttonTapped:(id)sender
|
||||
{
|
||||
NSLog(@"tapped!");
|
||||
}
|
||||
```
|
||||
|
||||
This works as you would expect. Unfortunately, this button is only 14½
|
||||
points tall — nowhere near the standard 44×44 minimum tap target
|
||||
size — and it's very difficult to tap. We could solve this by
|
||||
subclassing the text node and overriding `-hitTest:withEvent:`. We could even
|
||||
force the text view to have a minimum height during layout. But wouldn't it be
|
||||
nice if there were a more elegant way?
|
||||
|
||||
```objective-c
|
||||
// size all the things
|
||||
/* ... */
|
||||
|
||||
// make the tap target taller
|
||||
CGFloat extendY = roundf( (44.0f - size.height) / 2.0f );
|
||||
_shuffleNode.hitTestSlop = UIEdgeInsetsMake(-extendY, 0.0f, -extendY, 0.0f);
|
||||
```
|
||||
|
||||
Et voilà! *Hit-test slops* work on all nodes, and are a nice example of what
|
||||
this new abstraction enables.
|
||||
|
||||
Next up, making your own nodes!
|
||||
211
docs/guide/2-custom-nodes.md
Normal file
211
docs/guide/2-custom-nodes.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Custom nodes
|
||||
permalink: /guide/2/
|
||||
prev: guide/
|
||||
next: guide/3/
|
||||
---
|
||||
|
||||
## View hierarchies
|
||||
|
||||
Sizing and layout of custom view hierarchies are typically done all at once on
|
||||
the main thread. For example, a custom UIView that minimally encloses a text
|
||||
view and an image view might look like this:
|
||||
|
||||
```objective-c
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
// size the image
|
||||
CGSize imageSize = [_imageView sizeThatFits:size];
|
||||
|
||||
// size the text view
|
||||
CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height);
|
||||
CGSize textSize = [_textView sizeThatFits:maxTextSize];
|
||||
|
||||
// make sure everything fits
|
||||
CGFloat minHeight = MAX(imageSize.height, textSize.height);
|
||||
return CGSizeMake(size.width, minHeight);
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
CGSize size = self.bounds.size; // convenience
|
||||
|
||||
// size and layout the image
|
||||
CGSize imageSize = [_imageView sizeThatFits:size];
|
||||
_imageView.frame = CGRectMake(size.width - imageSize.width, 0.0f,
|
||||
imageSize.width, imageSize.height);
|
||||
|
||||
// size and layout the text view
|
||||
CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height);
|
||||
CGSize textSize = [_textView sizeThatFits:maxTextSize];
|
||||
_textView.frame = (CGRect){ CGPointZero, textSize };
|
||||
}
|
||||
```
|
||||
|
||||
This isn't ideal. We're sizing our subviews twice — once to figure out
|
||||
how big our view needs to be and once when laying it out — and while our
|
||||
layout arithmetic is cheap and quick, we're also blocking the main thread on
|
||||
expensive text sizing.
|
||||
|
||||
We could improve the situation by manually cacheing our subviews' sizes, but
|
||||
that solution comes with its own set of problems. Just adding `_imageSize` and
|
||||
`_textSize` ivars wouldn't be enough: for example, if the text were to change,
|
||||
we'd need to recompute its size. The boilerplate would quickly become
|
||||
untenable.
|
||||
|
||||
Further, even with a cache, we'll still be blocking the main thread on sizing
|
||||
*sometimes*. We could try to shift sizing to a background thread with
|
||||
`dispatch_async()`, but even if our own code is thread-safe, UIView methods are
|
||||
documented to [only work on the main
|
||||
thread](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html):
|
||||
|
||||
> Manipulations to your application’s user interface must occur on the main
|
||||
> thread. Thus, you should always call the methods of the UIView class from
|
||||
> code running in the main thread of your application. The only time this may
|
||||
> not be strictly necessary is when creating the view object itself but all
|
||||
> other manipulations should occur on the main thread.
|
||||
|
||||
This is a pretty deep rabbit hole. We could attempt to work around the fact
|
||||
that UILabels and UITextViews cannot safely be sized on background threads by
|
||||
manually creating a TextKit stack and sizing the text ourselves... but that's a
|
||||
laborious duplication of work. Further, if UITextView's layout behaviour
|
||||
changes in an iOS update, our sizing code will break. (And did we mention that
|
||||
TextKit isn't thread-safe either?)
|
||||
|
||||
## Node hierarchies
|
||||
|
||||
Enter AsyncDisplayKit. Our custom node looks like this:
|
||||
|
||||
```objective-c
|
||||
#import <AsyncDisplayKit/AsyncDisplayKit+Subclasses.h>
|
||||
|
||||
...
|
||||
|
||||
// perform expensive sizing operations on a background thread
|
||||
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
|
||||
{
|
||||
// size the image
|
||||
CGSize imageSize = [_imageNode measure:constrainedSize];
|
||||
|
||||
// size the text node
|
||||
CGSize maxTextSize = CGSizeMake(constrainedSize.width - imageSize.width,
|
||||
constrainedSize.height);
|
||||
CGSize textSize = [_textNode measure:maxTextSize];
|
||||
|
||||
// make sure everything fits
|
||||
CGFloat minHeight = MAX(imageSize.height, textSize.height);
|
||||
return CGSizeMake(constrainedSize.width, minHeight);
|
||||
}
|
||||
|
||||
// do as little work as possible in main-thread layout
|
||||
- (void)layout
|
||||
{
|
||||
// layout the image using its cached size
|
||||
CGSize imageSize = _imageNode.calculatedSize;
|
||||
_imageNode.frame = CGRectMake(self.bounds.size.width - imageSize.width, 0.0f,
|
||||
imageSize.width, imageSize.height);
|
||||
|
||||
// layout the text view using its cached size
|
||||
CGSize textSize = _textNode.calculatedSize;
|
||||
_textNode.frame = (CGRect){ CGPointZero, textSize };
|
||||
}
|
||||
```
|
||||
|
||||
ASImageNode and ASTextNode, like the rest of AsyncDisplayKit, are thread-safe,
|
||||
so we can size them on background threads. The `-measure:` method is like
|
||||
`-sizeThatFits:`, but with side effects: it caches both the argument
|
||||
(`constrainedSizeForCalculatedSize`) and the result (`calculatedSize`) for
|
||||
quick access later on — like in our now-snappy `-layout` implementation.
|
||||
|
||||
As you can see, node hierarchies are sized and laid out in much the same way as
|
||||
their view counterparts. Custom nodes do need to be written with a few things
|
||||
in mind:
|
||||
|
||||
* Nodes must recursively measure all of their subnodes in their
|
||||
`-calculateSizeThatFits:` implementations. Note that the `-measure:`
|
||||
machinery will only call `-calculateSizeThatFits:` if a new measurement pass
|
||||
is needed (e.g., if the constrained size has changed).
|
||||
|
||||
* Nodes should perform any other expensive pre-layout calculations in
|
||||
`-calculateSizeThatFits:`, cacheing useful intermediate results in ivars as
|
||||
appropriate.
|
||||
|
||||
* Nodes should call `[self invalidateCalculatedSize]` when necessary. For
|
||||
example, ASTextNode invalidates its calculated size when its
|
||||
`attributedString` property is changed.
|
||||
|
||||
For more examples of custom sizing and layout, along with a demo of
|
||||
ASTextNode's features, check out `BlurbNode` and `KittenNode` in the
|
||||
[Kittens](https://github.com/facebook/AsyncDisplayKit/tree/master/examples/Kittens)
|
||||
sample project.
|
||||
|
||||
## Custom drawing
|
||||
|
||||
To guarantee thread safety in its highly-concurrent drawing system, the node
|
||||
drawing API diverges substantially from UIView's. Instead of implementing
|
||||
`-drawRect:`, you must:
|
||||
|
||||
1. Define an internal "draw parameters" class for your custom node. This
|
||||
class should be able to store any state your node needs to draw itself
|
||||
— it can be a plain old NSObject or even a dictionary.
|
||||
|
||||
2. Return a configured instance of your draw parameters class in
|
||||
`-drawParametersForAsyncLayer:`. This method will always be called on the
|
||||
main thread.
|
||||
|
||||
3. Implement either `+drawRect:withParameters:isCancelled:isRasterizing:` or
|
||||
`+displayWithParameters:isCancelled:`. Note that these are *class* methods
|
||||
that will not have access to your node's state — only the draw
|
||||
parameters object. They can be called on any thread and must be
|
||||
thread-safe.
|
||||
|
||||
For example, this node will draw a rainbow:
|
||||
|
||||
```objective-c
|
||||
@interface RainbowNode : ASDisplayNode
|
||||
@end
|
||||
|
||||
@implementation RainbowNode
|
||||
|
||||
+ (void)drawRect:(CGRect)bounds
|
||||
withParameters:(id<NSObject>)parameters
|
||||
isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock
|
||||
isRasterizing:(BOOL)isRasterizing
|
||||
{
|
||||
// clear the backing store, but only if we're not rasterising into another layer
|
||||
if (!isRasterizing) {
|
||||
[[UIColor whiteColor] set];
|
||||
UIRectFill(bounds);
|
||||
}
|
||||
|
||||
// UIColor sadly lacks +indigoColor and +violetColor methods
|
||||
NSArray *colors = @[ [UIColor redColor],
|
||||
[UIColor orangeColor],
|
||||
[UIColor yellowColor],
|
||||
[UIColor greenColor],
|
||||
[UIColor blueColor],
|
||||
[UIColor purpleColor] ];
|
||||
CGFloat stripeHeight = roundf(bounds.size.height / (float)colors.count);
|
||||
|
||||
// draw the stripes
|
||||
for (UIColor *color in colors) {
|
||||
CGRect stripe = CGRectZero;
|
||||
CGRectDivide(bounds, &stripe, &bounds, stripeHeight, CGRectMinYEdge);
|
||||
[color set];
|
||||
UIRectFill(stripe);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
This could easily be extended to support vertical rainbows too, by adding a
|
||||
`vertical` property to the node, exporting it in
|
||||
`-drawParametersForAsyncLayer:`, and referencing it in
|
||||
`+drawRect:withParameters:isCancelled:isRasterizing:`. More-complex nodes can
|
||||
be supported in much the same way.
|
||||
|
||||
For more on custom nodes, check out the [subclassing
|
||||
header](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/ASDisplayNode%2BSubclasses.h)
|
||||
or read on!
|
||||
102
docs/guide/3-asynchronous-display.md
Normal file
102
docs/guide/3-asynchronous-display.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Asynchronous display
|
||||
permalink: /guide/3/
|
||||
prev: guide/2/
|
||||
next: guide/4/
|
||||
---
|
||||
|
||||
## Realistic placeholders
|
||||
|
||||
Nodes need to complete both a *measurement pass* and a *display pass* before
|
||||
they're fully rendered. It's possible to force either step to happen
|
||||
synchronously: call `-measure:` in `-layoutSubviews` to perform sizing on the
|
||||
main thread, or set a node's `displaysAsynchronously` flag to NO to disable
|
||||
ASDK's async display machinery. (AsyncDisplayKit can still improve your app's
|
||||
performance even when rendering fully synchronously — more on that
|
||||
later!)
|
||||
|
||||
The recommended way to use ASDK is to only add nodes to your view hierarchy
|
||||
once they've been sized. This avoids unsightly layout changes as the
|
||||
measurement pass completes, but if you enable asynchronous display, it will
|
||||
always be possible for a node to appear onscreen before its content has fully
|
||||
rendered. We'll discuss techniques to minimise this shortly, but you should
|
||||
take it into account and include *realistic placeholders* in your app designs.
|
||||
|
||||
Once its measurement pass has completed, a node can accurately place all of its
|
||||
subnodes onscreen — they'll just be blank. The easiest way to make a
|
||||
realistic placeholder is to set static background colours on your subnodes.
|
||||
This effect looks better than generic placeholder images because it varies
|
||||
based on the content being loaded, and it works particularly well for opaque
|
||||
images. You can also create visually-appealing placeholder nodes, like the
|
||||
shimmery lines representing text in Paper as its stories are loaded, and swap
|
||||
them out with your content nodes once they've finished displaying.
|
||||
|
||||
## Working range
|
||||
|
||||
So far, we've only discussed asynchronous sizing: toss a "create a node
|
||||
hierarchy and measure it" block onto a background thread, then trampoline to
|
||||
the main thread to add it to the view hierarchy when that's done. Ideally, as
|
||||
much content as possible should be fully-rendered and ready to go as soon as
|
||||
the user scrolls to it. This requires triggering display passes in advance.
|
||||
|
||||
If your app's content is in a scroll view or can be paged through, like
|
||||
Instagram's main feed or Paper's story strip, the solution is a *working
|
||||
range*. A working range controller tracks the *visible range*, the subset of
|
||||
content that's currently visible onscreen, and enqueues asynchronous rendering
|
||||
for the next few screenfuls of content. As the user scrolls, a screenful or
|
||||
two of previous content is preserved; the rest is cleared to conserve memory.
|
||||
If she starts scrolling in the other direction, the working range trashes its
|
||||
render queue and starts pre-rendering in the new direction of scroll —
|
||||
and because of the buffer of previous content, this entire process will
|
||||
typically be invisible.
|
||||
|
||||
AsyncDisplayKit includes a generic working range controller,
|
||||
`ASRangeController`. Its working range size can be tuned depending on your
|
||||
app: if your nodes are simple, even an iPhone 4 can maintain a substantial
|
||||
working range, but heavyweight nodes like Facebook stories are expensive and
|
||||
need to be pruned quickly.
|
||||
|
||||
```objective-c
|
||||
ASRangeController *rangeController = [[ASRangeController alloc] init];
|
||||
rangeController.tuningParameters = (ASRangeTuningParameters){
|
||||
.leadingBufferScreenfuls = 2.0f; // two screenfuls in the direction of scroll
|
||||
.trailingBufferScreenfuls = 0.5f; // one-half screenful in the other direction
|
||||
};
|
||||
```
|
||||
|
||||
If you use a working range, you should profile your app and consider tuning it
|
||||
differently on a per-device basis. iPhone 4 has 512MB of RAM and a single-core
|
||||
A4 chipset, while iPhone 6 has 1GB of RAM and the orders-of-magnitude-faster
|
||||
multicore A8 — and if your app supports iOS 7, it will be used on both.
|
||||
|
||||
## ASTableView
|
||||
|
||||
ASRangeController manages working ranges, but doesn't actually display content.
|
||||
If your content is currently rendered in a UITableView, you can convert it to
|
||||
use `ASTableView` and custom nodes — just subclass `ASCellNode` instead
|
||||
of ASDisplayNode. ASTableView is a UITableView subclass that integrates
|
||||
node-based cells and a working range.
|
||||
|
||||
ASTableView doesn't let cells onscreen until their underlying nodes have been
|
||||
sized, and as such can fully benefit from realistic placeholders. Its API is
|
||||
very similar to UITableView (see the
|
||||
[Kittens](https://github.com/facebook/AsyncDisplayKit/tree/master/examples/Kittens)
|
||||
sample project for an example), with some key changes:
|
||||
|
||||
* Rather than setting the table view's `.delegate` and `.dataSource`, you set
|
||||
its `.asyncDelegate` and `.asyncDataSource`. See
|
||||
[ASTableView.h](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/ASTableView.h)
|
||||
for how its delegate and data source protocols differ from UITableView's.
|
||||
|
||||
* Instead of implementing `-tableView:cellForRowAtIndexPath:`, your data
|
||||
source must implement `-tableView:nodeForRowAtIndexPath:`. This method must
|
||||
be thread-safe and should not implement reuse. Unlike the UITableView
|
||||
version, it won't be called when the row is about to display.
|
||||
|
||||
* `-tableView:heightForRowAtIndexPath:` has been removed — ASTableView
|
||||
lets your cell nodes size themselves. This means you no longer have to
|
||||
manually duplicate or factor out layout and sizing logic for
|
||||
dynamically-sized UITableViewCells!
|
||||
|
||||
Next up, how to get the most out of ASDK in your app.
|
||||
139
docs/guide/4-making-the-most-of-asdk.md
Normal file
139
docs/guide/4-making-the-most-of-asdk.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Making the most of AsyncDisplayKit
|
||||
permalink: /guide/4/
|
||||
prev: guide/3/
|
||||
next: guide/5/
|
||||
---
|
||||
|
||||
## A note on optimisation
|
||||
|
||||
AsyncDisplayKit is powerful and flexible, but it is not a panacea. If your app
|
||||
has a complex image- or text-heavy user interface, ASDK can definitely help
|
||||
improve its performance — but if you're blocking the main thread on
|
||||
network requests, you should consider rearchitecting a few things first. :]
|
||||
|
||||
So why is it worthwhile to change the way we do view layout and rendering,
|
||||
given that UIKit has always been locked to the main thread and performant iOS
|
||||
apps have been shipping since iPhone's launch?
|
||||
|
||||
### Modern animations
|
||||
|
||||
Until iOS 7, static animations (à la `+[UIView
|
||||
animateWithDuration:animations:]`) were the standard. The post-skeuomorphism
|
||||
redesign brought with it highly-interactive, physics-based animations, with
|
||||
springs joining the ranks of constant animation functions like
|
||||
`UIViewAnimationOptionCurveEaseInOut`.
|
||||
|
||||
Classic animations aren't actually executed in your app. They're executed
|
||||
out-of-process, in the high-priority Core Animation render server. Thanks to
|
||||
pre-emptive multitasking, an app can block its main thread continuously without
|
||||
causing the animation to drop a single frame.
|
||||
|
||||
Critically, dynamic animations can't be offloaded the same way, and both
|
||||
[pop](https://github.com/facebook/pop) and UIKit Dynamics execute physics
|
||||
simulations on your app's main thread. This is because executing arbitrary
|
||||
code in the render server would introduce unacceptable latency, even if it
|
||||
could be done securely.
|
||||
|
||||
Physics-based animations are often interactive, letting you start an animation
|
||||
and interrupt it before it completes. Paper lets you fling objects across the
|
||||
screen and catch them before they land, or grab a view that's being pulled by a
|
||||
spring and tear it off. This requires processing touch events and updating
|
||||
animation targets in realtime — even short delays for inter-process
|
||||
communication would shatter the illusion.
|
||||
|
||||
(Fun fact: Inertial scrolling is also a physics animation! UIScrollView has
|
||||
always implemented its animations on the main thread, which is why stuttery
|
||||
scrolling is the hallmark of a slow app.)
|
||||
|
||||
### The main-thread bottleneck
|
||||
|
||||
Physics animations aren't the only thing that need to happen on the main
|
||||
thread. The main thread's [run
|
||||
loop](https://developer.apple.com/library/ios/documentation/cocoa/conceptual/multithreading/runloopmanagement/runloopmanagement.html)
|
||||
is responsible for handling touch events and initiating drawing operations
|
||||
— and with UIKit in the mix, it also has to render text, decode images,
|
||||
and do any other custom drawing (e.g., using Core Graphics).
|
||||
|
||||
If an iteration of the main thread's run loop takes too long, it will drop an
|
||||
animation frame and may fail to handle touch events in time. Each iteration of
|
||||
the run loop must complete within 16ms in order to drive 60fps animations, and
|
||||
your own code typically has less than 10ms to execute. This means that the
|
||||
best way to keep your app smooth and responsive is to do as little work on the
|
||||
main thread as possible.
|
||||
|
||||
What's more, the main thread only executes on one core! Single-threaded view
|
||||
hierarchies can't take advantage of the multicore CPUs in all modern iOS
|
||||
devices. This is important for more than just performance reasons — it's
|
||||
also critical for battery life. Running the CPU on all cores for a short time
|
||||
is preferable to running one core for an extended amount of time: if the
|
||||
processor can *race to sleep* by finishing its work and idling faster, it can
|
||||
spend more time in a low-power mode, improving battery life.
|
||||
|
||||
## When to go asynchronous
|
||||
|
||||
AsyncDisplayKit really shines when used fully asynchronously, shifting both
|
||||
measurement and rendering passes off the main thread and onto multiple cores.
|
||||
This requires a completely node-based hierarchy. Just as degrading from
|
||||
UIViews to CALayers disables view-specific functionality like touch handling
|
||||
from that point on, degrading from nodes to views disables async behaviour.
|
||||
|
||||
You don't, however, need to convert your app's entire view hierarchy to nodes.
|
||||
In fact, you shouldn't! Asynchronously bringing up your app's core UI, like
|
||||
navigation elements or tab bars, would be a very confusing experience. Those
|
||||
elements of your apps can still be nodes, but should be fully synchronous to
|
||||
guarantee a fully-usable interface as quickly as possible. (This is why
|
||||
UIWindow has no node equivalent.)
|
||||
|
||||
There are two key situations where asynchronous hierarchies excel:
|
||||
|
||||
1. *Parallelisation*. Measuring and rendering UITableViewCells (or your app's
|
||||
equivalent, e.g., story cards in Paper) are embarrassingly parallel
|
||||
problems. Table cells typically have a fixed width and variable height
|
||||
determined by their contents — the argument to `-measure:` for one
|
||||
cell doesn't depend on any other cells' calculations, so we can enqueue an
|
||||
arbitrary number of cell measurement passes at once.
|
||||
|
||||
2. *Preloading*. An app with five tabs should synchronously load the first
|
||||
one so content is ready to go as quickly as possible. Once this is
|
||||
complete and the CPU is idle, why not asynchronously prepare the other tabs
|
||||
so the user doesn't have to wait? This is inconvenient with views, but
|
||||
very easy with nodes.
|
||||
|
||||
Paper's asynchronous rendering starts at the story strip. You should profile
|
||||
your app and watch how people use it in the wild to decide what combination of
|
||||
synchrony and asynchrony yields the best user experience.
|
||||
|
||||
## Additional optimisations
|
||||
|
||||
Complex hierarchies — even when rendered asynchronously — can
|
||||
impose a cost because of the sheer number of views involved. Working around
|
||||
this can be painful, but AsyncDisplayKit makes it easy!
|
||||
|
||||
* *Layer-backing*. In some cases, you can substantially improve your app's
|
||||
performance by using layers instead of views. Manually converting
|
||||
view-based code to layers is laborious due to the difference in APIs.
|
||||
Worse, if at some point you need to enable touch handling or other
|
||||
view-specific functionality, you have to manually convert everything back
|
||||
(and risk regressions!).
|
||||
|
||||
With nodes, converting an entire subtree from views to layers is as simple
|
||||
as...
|
||||
|
||||
rootNode.layerBacked = YES;
|
||||
|
||||
...and if you need to go back, it's as simple as deleting one line. We
|
||||
recommend enabling layer-backing as a matter of course in any custom node
|
||||
that doesn't need touch handling.
|
||||
|
||||
* *Precompositing*. Flattening an entire view hierarchy into a single layer
|
||||
also improves performance, but comes with a hit to maintainability and
|
||||
hierarchy-based reasoning. Nodes can do this for you too!
|
||||
|
||||
rootNode.shouldRasterizeDescendants = YES;
|
||||
|
||||
...will cause the entire node hierarchy from that point on to be rendered
|
||||
into one layer.
|
||||
|
||||
Next up: AsyncDisplayKit, under the hood.
|
||||
89
docs/guide/5-under-the-hood.md
Normal file
89
docs/guide/5-under-the-hood.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Under the hood
|
||||
permalink: /guide/5/
|
||||
prev: guide/4/
|
||||
---
|
||||
|
||||
## Node architecture
|
||||
|
||||
*(Skip to the next section if you're not interested in AsyncDisplayKit implementation details.)*
|
||||
|
||||
We've described nodes as an abstraction over views and layers, and shown how to
|
||||
interact with the underlying UIViews and CALayers when necessary. Nodes don't
|
||||
wrap or vend their UIKit counterparts, though — an ASImageNode's `.view`
|
||||
is not a UIImageView! So how do nodes work?
|
||||
|
||||
**NOTE:** Classes whose names begin with `_` are private. Don't use them
|
||||
directly!
|
||||
|
||||
Creating a node doesn't create its underlying view-layer pair. This is why you
|
||||
can create nodes cheaply and on background threads. When you use a UIView or
|
||||
CALayer property on a node, you're actually interacting with a proxy object,
|
||||
[`_ASPendingState`](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/Private/_ASPendingState.h),
|
||||
that's preconfigured to match UIView and CALayer defaults.
|
||||
|
||||
The first access to a node's `.view` or `.layer` property causes both to be
|
||||
initialised and configured with the node's current state. If it has subnodes,
|
||||
they are recursively loaded as well. Once a node has been loaded, the proxy
|
||||
object is destroyed and the node becomes main-thread-affined — its
|
||||
properties will update the underlying view directly. (Layer-backed nodes do
|
||||
the same, not loading views.)
|
||||
|
||||
Nodes are powered by
|
||||
[`_ASDisplayLayer`](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/Details/_ASDisplayLayer.h)
|
||||
and
|
||||
[`_ASDisplayView`](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/Details/_ASDisplayView.h).
|
||||
These are lightweight to create and add to their respective hierarchies, and
|
||||
provide integration points that allow nodes to act as full-fledged views or
|
||||
layers. It's possible to create nodes that are backed by custom view or layer
|
||||
classes, but doing so is strongly discouraged as it disables the majority of
|
||||
ASDK's functionality.
|
||||
|
||||
When Core Animation asks an `_ASDisplayLayer` to draw itself, the request is
|
||||
forwarded to its node. Unless asynchronous display has been disabled, the
|
||||
actual draw call won't happen immediately or on the main thread. Instead, a
|
||||
display block will be added to a background queue. These blocks are executed
|
||||
in parallel, but you can enable `ASDISPLAYNODE_DELAY_DISPLAY` in
|
||||
[`ASDisplayNode(AsyncDisplay)`](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/Private/ASDisplayNode%2BAsyncDisplay.mm)
|
||||
to serialise the render system for debugging.
|
||||
|
||||
Common UIView subclass hooks are forwarded from `_ASDisplayView` to its
|
||||
underlying node, including touch handling, hit-testing, and gesture recogniser
|
||||
delegate calls. Because an `_ASDisplayView`'s layer is an `_ASDisplayLayer`,
|
||||
view-backed nodes also participate in asynchronous display.
|
||||
|
||||
## In practice
|
||||
|
||||
What does this mean for your custom nodes?
|
||||
|
||||
You can implement methods like `-touchesBegan:withEvent:` /
|
||||
`touchesMoved:withEvent:` / `touchesEnded:withEvent:` /
|
||||
`touchesCancelled:withEvent:` in your nodes exactly as you would in a UIView
|
||||
subclass. If you find you need a subclass hook that hasn't already been
|
||||
provided, please file an issue on GitHub — or add it yourself and submit a
|
||||
pull request!
|
||||
|
||||
If you need to interact or configure your node's underlying view or layer,
|
||||
don't do so in `-init`. Instead, override `-didLoad` and check if you're
|
||||
layer-backed:
|
||||
|
||||
```objective-c
|
||||
- (void)didLoad
|
||||
{
|
||||
[super didLoad];
|
||||
|
||||
// add a gesture recogniser, if we have a view to add it to
|
||||
if (!self.layerBacked) {
|
||||
_gestureRecogniser = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(_tap:)];
|
||||
[self.view addGestureRecognizer:_gestureRecogniser];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## *fin.*
|
||||
|
||||
Thanks for reading! If you have any questions, please file a GitHub issue or
|
||||
post in the [Facebook group](https://www.facebook.com/groups/551597518288687).
|
||||
We'd love to hear from you.
|
||||
Reference in New Issue
Block a user