asyncdisplaykit february 2015.pdf

53
with Effortless Responsiveness Scott Goodson Instagram iOS Engineering Manager

Upload: jose-cegri

Post on 17-Aug-2015

239 views

Category:

Documents


5 download

TRANSCRIPT

withEffortless ResponsivenessScott GoodsonInstagram iOS Engineering ManagerOverviewResponsivenessand Content Why Responsiveness should be a top focus for every developer.AppIntegration AsyncDisplayKit can help you delete code and have fewer bugs.FrameworkArchitecture Deep dive on how AsyncDisplayKit handles the hard stuff.What is Responsiveness?ResponsivenessalwaysinteractiveNO stallswaiting approaches zeroNO long delayssomething happens instantlyNO ambiguitySome history from the Web:Web Responsiveness 0.4s -> 0.9s, lost 20% 100KB -> 80KB, increased 25% Every 100ms increase lost 1% 40% abandon sites with >3s load times(Google trafc + revenue, 2006) (Google Maps, 2006) (Amazon sales, 2007) (2011)Responsiveness was critical in the Web era!App Responsiveness Interaction is direct Expectations are set high by top apps Compuware survey: 60% expect app load time under 2s 79% will abandon apps that crash or freeze twiceEven more critical in the Mobile era: New Design ChallengesThe standard for interface responsiveness has increasedResponse Time~100-200msMouse Clicks~50-100msTap Events~5-10msContinous GesturesMax Main Thread DelayResponsiveness ! ContentUser triggers content preparation (Scrolling, Tapping, )Content preparation blocks user (Network, Layout, Rendering, )Two Impacts on UsersConceptually independent, but may occur together!Blocked interactionApp doesnt react to gestures and/or drops frames1WaitingNetwork / Time toPrepare Content2The Main ThreadiOS SDK was designed for single-core simplicity User interaction must be handled by the main thread Gestures and physics-based animations demand constant main thread availabilityWhat Blocks the Main Thread?System Objects Create Manipulate DestroyRendering Text Images DrawingLayout Text Measurement (hierarchical)All are required to prepare content!AsyncDisplayKit in 3 PartsCorePowerful FeaturesApp Integration1. Designing the CoreGoals: Generalized way to avoid main thread stalls Immediate familiarity for UIKit developers Constraints: UIKit and CA are not thread-safe Must Integrate with UIKit and CAViews & LayersCALayer.layer.delegateOn iOS, everything onscreen is a CALayer UIView wraps CALayer (without subclassing) UIView adds features like touch handlingUIViewViews & LayersUIKit and CoreAnimation are main-thread only. This causes most of the stutters in apps. How can we address this fundamentally?Main-thread only!CALayer.layer.delegateUIView.node.viewThe Asynchronous UI ObjectNode CALayer UIView.layer.delegateASDisplayNode is the core class of AsyncDisplayKit. AsyncDisplayKit extends UIKit :: UIKit extends CoreAnimation! This abstraction enabled major additional optimizationsASDisplayNode Mental Model is a NSObject owns a UIView (or CALayer) Thread safe!(alloc & set up on any queue)UIView CALayerORowns aNodeASDisplayNode Mental Model View in MVC (just like UIView / CALayer) 1-for-1 replacement of UIViews & CALayers All nodes render asynchronously by default Synchronous placeholder states should be considered Prefer interacting with Node, but: May access View or Layer if necessaryCALayerUIViewNodeASDisplayNode API@interface ASDisplayNode (UIViewBridge) - (void)setNeedsDisplay;// Marks the view as needing display. - (void)setNeedsLayout; // Marks the view as needing layout. @property (retain) CALayer *layer; @property (retain) id contents; // default=nil @property (assign) BOOL clipsToBounds;// default==NO @property (assign) CGFloat alpha; // default=1.0f @property (assign) CGRect frame;// default=CGRectZero @property (assign) CGPoint anchorPoint; // default={0.5, 0.5} @property (retain) UIColor *tintColor;// default=Blue @property (assign) UIViewContentMode contentMode; // Accessibility support @property (assign) BOOL isAccessibilityElement; @property (copy) NSString *accessibilityLabel; @property (assign) BOOL shouldGroupAccessibilityChildren; // Event handling - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;ASDisplayNode APINew stuff: @property (retain) UIView *view; - (void)addSubnode:(ASDisplayNode *)subnode; - (CGSize)measure:(CGSize)constrainedSize; @property (assign) CGSize calculatedSize; ASDisplayNode+Subclasses.h: - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize; - (void)didLoad; - (void)layout; ASDisplayNode LifecycleNodeCALayerUIViewNodeAt alloc / init time, nodes have no backing objects. Ideally, create them on background queues, set data model, call -measure: to complete any expensive layout.When the .view or .layer property is accessed, backing objects are created. Node tree root has its .view or .layer added to a UIWindow.ASDisplayNode LifecycleCALayerUIViewNodeUIWindow-layoutSublayers-layoutCoreAnimation calls layout on the main thread. Values calculated during measurement are applied.ASDisplayNode LifecyclePlaceholder state for each node is shown.Asynchronous rendering is scheduled.CALayerUIViewNodeUIWindow-display-setContents:Lifecycle ExampleAsynchronous LayoutAuthorTitleLinkBody TextStorydispatch_async(backgroundQueue, ^{ storyNode = [[FBStoryNode alloc] initWithStory:story]; [storyNode measure:CGSizeMake(screenWidth, FLT_MAX)]; });dispatch_async(backgroundQueue, ^{ storyNode = [[FBStoryNode alloc] initWithStory:story]; [storyNode measure:CGSizeMake(screenWidth, FLT_MAX)]; });dispatch_async(backgroundQueue, ^{ storyNode = [[FBStoryNode alloc] initWithStory:story]; [storyNode measure:CGSizeMake(screenWidth, FLT_MAX)]; });dispatch_async(backgroundQueue, ^{ storyNode = [[FBStoryNode alloc] initWithStory:story]; [storyNode measure:CGSizeMake(screenWidth, FLT_MAX)]; });- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize; !CGSize calculatedSize;Adding to Window (Mounting)AuthorTitleLinkBody TextStorydispatch_async(mainQueue, ^{ [window addSubnode:storyNode]; });dispatch_async(mainQueue, ^{ [window addSubview:storyNode.view]; });Asynchronous Rendering- (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } - (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = newContextOfSize(self.bounds.size); [self.node drawInContext:ctx]; dispatch_async(main, ^{ self.contents = ctx; }); }); } @implementation _ASDisplayLayerAuthorTitleLinkBody TextStoryAsyncDisplayKit in 3 PartsCorePowerful FeaturesApp Integration2. Powerful Features Optimizations ASTextNode & ASImageNode ASTableView & ASCollectionView Intelligent Preloading PlaceholdersConcurrencyAsynchronous is necessary for smoothness Concurrency reduces wait times signicantly 90% of iOS devices are dual-core; A8X is tri-core! Important concurrent operations: Layout (text measurement) Rendering (text, images, graphics) Improves Performance Per Watt!Layer-backingViews need ~5x more main thread CPU than layers Many elements dont need any UIView featureView Backed (Default).node.viewNode CALayer UIView.layer.delegateLayer-backing.layer.delegateNode CALayernode.layerBacked = YES;Views need ~5x more main thread CPU than layers Many elements dont need any UIView featureSubtree Rasterization Separate layers are convenient and exible Yet, a big draw method is more efcient Skip creating views and layers for subtrees CPU win: avoids many views & layers GPU win: single texture (less compositing)node.shouldRasterizeDescendants = YES;ASTextNodeA powerful TextKit-based rendering element with support for: Asynchronous, concurrent text layout and rendering Inline attachments including asynchronous images owing with text Dynamic, true-to-line text shape placeholdersUILabel replacement ASImageNodeA powerful image element: Asynchronous, concurrent image decoding. Customizable caching strategies. Loading multiple image representations transparently to speed rst display. Automatic request-ahead andrender-aheadUIImageView replacement ASTableView / ASCollectionView Asynchronous Table / Collection View Subclass of UITableView / UICollectionView Full support for editing and animation Automatically manages: Concurrent Layout Concurrent Rendering Congurable Request-Ahead and Render-AheadIntelligent PreloadingRENDER NETWORK VISIBLERender: content entering triggers rendering;exit triggers purging all backing stores.Network: triggers all loads associated with populating a story; includes next-N item fetch, as well as image loads.Intelligent PreloadingRENDER NETWORK VISIBLEUser changing direction flips the leading sideof the range.Behavior, including aggressiveness of each range,is variable for devices with differing RAM & CPU@property (nonatomic, assign) ASRangeTuningParameters rangeTuningParameters; PlaceholdersPlaceholders are synchronous - must be fast! backgroundColor is zero cost placeholderImage for quick-to-decode assets placeholderColor / placeholderInsets for ASTextNode@property (strong) UIColor *backgroundColor; - (UIImage *)placeholderImage; @property (assign) BOOL placeholderEnabled; @property (assign) BOOL placeholderFadesOut; @property (strong) UIColor *placeholderColor; @property (assign) UIEdgeInsets placeholderInsets;Powerful FeaturesAsyncDisplayKit in 3 PartsCoreApp Integration3. App IntegrationPractical considerations on using AsyncDisplayKit: Where to use Nodes Code Conversion Examples Subclassing & Wrapping Auto-layout Interface BuilderWhere to use Nodes Nodes dont need to replace every view. Focus on sections with lots of layout or rendering! Table + Collection Cells View Controllers with costly display work Launch time critical paths UI with continuous gestures Rule of thumb: adopt ASTableView / ASCollectionView and use nodes beneath that point.Simply change the superclass, and x common call sites! Converting Views@interface Foo : UIView { UILabel *_label; } @end @implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _label = [[UILabel alloc] init]; [self addSubview:_label]; return self; } - (void)layoutSubviews { _label.frame = self.bounds; } @end @interface Foo : UIView { ASTextNode *_label; } @end @implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _label = [[ASTextNode alloc] init]; [self addSubnode:_label]; return self; } - (void)layoutSubviews { _label.frame = self.bounds; } @end @interface Foo : ASDisplayNode { ASTextNode *_label; } @end @implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _label = [[ASTextNode alloc] init]; [self addSubnode:_label]; return self; } - (void)layout { _label.frame = self.bounds; } @end Subclassing@implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _label = [[ASTextNode alloc] init]; [self addSubnode:_label]; return self; } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { return [_label measure:constrainedSize]; } - (void)didLoad { // self.view is now available, set up gesture recognizers, placeholder, etc } - (void)layout { _label.frame = (CGRect){ CGPointZero, _label.calculatedSize }; } @endSimply change the class, and return cell nodes!ConvertingCollections@interface Foo : UIView { UICollectionView *_cv; } @end @implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _cv = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:flowLayout]; [self addSubview:_cv]; return self; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { } @end @interface Foo : UIView { ASCollectionView *_cv; } @end @implementation Foo - (instancetype)init { if (!(self = [super init])) return nil; _cv = [[ASCollectionView alloc] initWithFrame:frame collectionViewLayout:flowLayout]; [self addSubview:_cv]; return self; } - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath { } @end Wrapping Views and LayersRarely, it is useful to wrap an existing view or layer class. ASScrollNode is a simple wrapper around UIScrollView.ASDisplayNode *scrollNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ return [[UIScrollView alloc] init]; }]; ASScrollNode*scrollNode = [[ASScrollNode alloc] init];Autolayout & Autoresizing Supported, like all UIView features Except no asynchronous layout. Asynchronous rendering is still provided. Box model layout can be added if there is demand Same layout model used by React Native and Components. Would support async layout.Interface BuilderNot supported.Also unsupported by: React Native Reactive Cocoa Components (Facebook for iOS architecture) Also: Storyboards are Incompatible with Truth and Beauty.Technical TakeawaysSignificant app architecture is required for responsiveness: Robust parser and data model systems Fetching data at the right time Carefully crafted layout code Text drawing helpers Image decompression services Placeholders and loading indicatorsSignificant app architecture is required for responsiveness: Robust parser and data model systems " Fetching data at the right time " Carefully crafted layout code " Text drawing helpers " Image decompression services " Placeholders and loading indicatorsTechnical TakeawaysUnify & abstract can be very powerfulAccept thechallenge of ambitious designsDont reinvent the wheeldivide & conquer (e.g., UIKit)Dont alwaysAnnouncing 1.2!Find the GitHub at asyncdisplaykit.org Includes: ASEditableTextNode Network Range APIs Batch Request APIsTell us your thoughts!Join the AsyncDisplayKit Developers group (fb.me/asdk-dev). Ask any implementation question Share your experiences Suggest API improvements or feature requests Star us on Github! Send us pull requests!Demo!