12/08/2018, 09:28

AsyncDisplayKit - Bí quyết đến từ facebook.

_ Trong các ứng dụng di động vào thời điểm hiện tại, có lẽ ứng dụng Facebook là một trong những ứng dụng có trải nghiệm người dùng tốt nhất. Mặc dù có giao diện tương đối phức tạp nhưng ứng dụng chạy rất mượt mà. Để làm được điều này, facebook đã áp dụng rất nhiều kĩ thuật vào ứng dụng của mình. Và ...

_ Trong các ứng dụng di động vào thời điểm hiện tại, có lẽ ứng dụng Facebook là một trong những ứng dụng có trải nghiệm người dùng tốt nhất. Mặc dù có giao diện tương đối phức tạp nhưng ứng dụng chạy rất mượt mà. Để làm được điều này, facebook đã áp dụng rất nhiều kĩ thuật vào ứng dụng của mình. Và một trong những công cụ đó đã được các kĩ sư Facebook public : Opensource : AsyncDisplayKit _

**I, Mở đầu **

AsyncDisplaykit là một iOS Framework có thể giúp cho các giao diện dù phức tạp nhất cũng có thể trở nên mượt mà hơn. Ban đầu, nó được tạo ra cho ứng dụng Paper.

Để cài đặt framework này, bạn có thể sử dụng Cocoapods

pod 'AsyncDisplayKit'

Hoặc bạn cũng có thể copy project bình thường bằng cách thêm file AsyncDisplayKit vào workspace đang dùng. Thêm vào thư viện libAsyncDisplayKit.a.

Nếu bạn đang sử dụng ngôn ngữ của swift, đừng quên thêm header vào file Objective-C bridging header. #import <AsyncDisplayKit/AsyncDisplayKit.h>

Điểm khác biệt của framework này đó là các layer của UIView , CALayer đều thread-safe. Thể hiện qua khái niệm "node". node-view-layer.png

Bạn có thể khởi tạo các node ( view) ở các background thread. Qua đó giúp cho ứng dụng mượt mà hơn.

**II, Concept. **

Đơn vị cơ bản của AsyncDisplayKit là node ASDisplayNode là lớp trừu tượng phía trên của UIView. Không giống như views vốn chỉ có thể sử dụng trên main thread, nodes có thể được khởi tạo và tuỳ chỉnh ở các background threads.

Nhằm giữ cho giao diện được mượt, ứng dụng nên có tỉ lệ khung hình xấp xỉ 60 frames/s. Đây chính là tiêu chuẩn vàng cho iOS. Điều này có nghĩa là main thread có 1/60 s để tạo ra 1 frame.

AsyncDisplayKit cho phép những công việc như image decoding, text sizing, rendering ... được tách biệt khỏi main thread.

Các đối tượng cơ bản của AsyncDisplayKit bao gồm :

  • ASDisplayNode : UIView
  • ASControlNode : UIControl
  • ASImageNode : UIIMageView
  • ASTextNode : UITextView
  • ASTableView, ASCollectionView : UITableView, UICollectionView

Ví dụ

Nếu ta khởi tạo một imageView bình thường :

 _imageView = [[UIImageView alloc] init];

_imageView.image = [UIImage imageNamed:@"hello"];

_imageView.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);

[self.view addSubview:_imageView];

Khởi tạo với AsyncDisplayKit

 _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];

Nếu bạn khởi tạo với cách bình thường, image sẽ được decode ở mainthread, trong khi với AsyncDisplayKit, image được decode ở background thread. Vậy là ứng dụng của bạn đã được improve rồi đấy.

** III, Kiến trúc Views **

Căn chỉnh và sắp xếp các views đối v ới AsyncDisplayKit đều được hoàn thành một lần trên main thread. Ví dụ, đối với một UIView bao gồm textview và imageview như sau :

 - (CGSize)sizeThatFits:(CGSize)size
{
  // size the image
  CGSize imageSize = [_imageView sizeThatFits:size];

  // size the text view
  CGSize maxTextSize = CGSizeMake(size.awidth - imageSize.awidth, size.height);
  CGSize textSize = [_textView sizeThatFits:maxTextSize];

  // make sure everything fits
  CGFloat minHeight = MAX(imageSize.height, textSize.height);
  return CGSizeMake(size.awidth, minHeight);
}

- (void)layoutSubviews
{
  CGSize size = self.bounds.size; // convenience

  // size and layout the image
  CGSize imageSize = [_imageView sizeThatFits:size];
  _imageView.frame = CGRectMake(size.awidth - imageSize.awidth, 0.0f,
                                imageSize.awidth, imageSize.height);

  // size and layout the text view
  CGSize maxTextSize = CGSizeMake(size.awidth - imageSize.awidth, size.height);
  CGSize textSize = [_textView sizeThatFits:maxTextSize];
  _textView.frame = (CGRect){ CGPointZero, textSize };
}

Đối với code bình thường như trên, chúng ta đang căn chỉnh các subviews hai lần.

Việc tính toán phải chặn main thread lại khiến cho tài nguyên tiêu tốn rất nhiều.

Chúng ta có thể cải thiện tình huống trên với AsyncDisplayKit .

#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.awidth - imageSize.awidth,
                                  constrainedSize.height);
  CGSize textSize = [_textNode measure:maxTextSize];

  // make sure everything fits
  CGFloat minHeight = MAX(imageSize.height, textSize.height);
  return CGSizeMake(constrainedSize.awidth, 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.awidth - imageSize.awidth, 0.0f,
                                imageSize.awidth, imageSize.height);

  // layout the text view using its cached size
  CGSize textSize = _textNode.calculatedSize;
  _textNode.frame = (CGRect){ CGPointZero, textSize };
}

ASImageNodeASTextNode là thread safe nên chúng ta có thể căn chỉnh dưới background.

Phương thức measure tương tự như sizeThatFits , nhưng bên cạnh đó nó còn cache lại các tham số : constrainedSizeForCalculatedSize và kết quả của calcualatedSize cho việc sử dụng sau này.

Một vài tip nhỏ khi sử dụng AsyncDisplayKit :

  • Các hàm tính toán tốn tài nguyên nên được sử dụng trong calculateSizeThatFits để được cache
  • Nên gọi hàm [self invalidateCalculateSize] khi có thể.

**IV, Custom drawing **

Khi chúng ta subclass một đối tượng view, thay vì sử dụng -drawRect:

  1. Sử dụng -drawParametersForAsyncLayer:
  2. Sử dụng +drawRect:withParameters:isCancelled:isRasterizing: hoặc +displayWithParameters:isCancelled:

Chú ý rằng các class method trên sẽ không ảnh hưởng đến trạng thái của các node. Nó chỉ thực thi việc vẽ và có thể được gọi từ bất kì thread nào, nhưng nên là từ background.

ví dụ :

 @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
0