前言
AsyncDisplayKit是14年开源出来的库了,每一个库开发出来肯定都是为了解决某些问题的,所以藉着有任务在身就研究一下这个UI库的原理作为原始积累吧。
60fps
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!
以上摘自AsyncDisplayKit的Github介绍,在开始介绍AsyncDisplayKit之前,首先要介绍一下这个数字及单位以作科普。fps即feet pre second, 在iOS应用界面流畅体验当中,业界通常会用用户在操作时界面的帧数达到60帧每秒来作为一个应用流畅的基准。
在开发中我们可以通过XCode自带的Instruments中的Core Animation来查看及调试我们的界面的帧数,找到帧数最低的从而进行优化。
简介
AsyncDisplayKit能让你通过将图像解码、布局以及渲染操作放在后台线程,从而带来超级响应的用户界面,也就是说不再会因界面卡顿而阻断用户交互。简而言之,就是可以让我即使开发出了很复杂的界面,只要我们使用这个库,就都可以达到60fps,做到最好的用户体验。
为什么这个库出现了?
我们知道,iOS的View渲染是交给主线程去做渲染的,并且UIView是非线程安全的,不能做异步渲染。当应用启动,页面初始化时,所有我们写的UI都会在主线程进行加载,如果UI过于复杂,就会造成卡顿。
所以,Facebook的Paper团队给我们带来这个lib,AsyncDisplayKit。
原理
AsyncDisplayKit’s basic unit is the node. An ASDisplayNode is an abstraction over UIView, which in turn is an abstraction over CALayer.
AsyncDisplayKit的最小单元是其自定义出来的ASDisplayNode。ASDisplayNode是UIView之上的抽象层,同时也是CALayer的抽象层。和只能被用在主线程的视图不同,nodes是线程安全的:你能并行的实例化并设置整个node层级,并且在后台线程里运行。假设我们要用这个库渲染一个UIImage,AsyncDisplayKit会让你把image的解码,text sizing和渲染,以及其他的在费时的UI从左放置在了其他线程。(在使用view的时候,这些操作都只能放置在UI线程)
举个栗子,传统的UIImage:
_imageView = [[UIImageView alloc] init];
_imageView.image = [UIImage imageNamed:@"hello"];
_imageView.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageView];
而AsyncDisplayKit的UIImage:
_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];
ASImageNode为例
我们来看看ASImageNode的继承关系图:
可以看到,一切的起点都是ASDisplayNode,其实跟UIView->UIControl->UIImage的继承结构很相像。
阅读了下源码ASImageNode.mm 后发现,当 ASImageNode 初始化执行到+(void)initialize时,会先开启一个全局断言函数来监听UI渲染过程的error。
+ (void)initialize
{
[super initialize];
if (self != [ASImageNode class]) {
// Prevent custom drawing in subclasses
ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASImageNode class], self, @selector(displayWithParameters:isCancelled:)), @"Subclass %@ must not override displayWithParameters:isCancelled: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASImageNode class]));
}
}
当执行到init时,除了定义一些关于UIImage的Layer的初始属性外,还会定义一个UI渲染的Runloop模式
_animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode;
当给node设置Image时,会触发:
- (void)setImage:(UIImage *)image
{
ASDN::MutexLocker l(__instanceLock__);
if (!ASObjectIsEqual(_image, image)) {
_image = image;
[self invalidateCalculatedLayout];
if (image) {
[self setNeedsDisplay];
if ([ASImageNode shouldShowImageScalingOverlay] && _debugLabelNode == nil) {
ASPerformBlockOnMainThread(^{
_debugLabelNode = [[ASTextNode alloc] init];
_debugLabelNode.layerBacked = YES;
[self addSubnode:_debugLabelNode];
});
}
} else {
self.contents = nil;
}
}
}
在配置好image后,ASImageNode就会触发- (UIImage *)displayWithParameters:(id<NSObject> *)parameter isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
这个function,我们可以看到,在对image进行一系列的配置后,会讲image封装成一个ASImageNodeContentsKey,然后再打包成一个ASWeakMapEntry,从而使我们的图片以cache的形式保存,当进行图片渲染时,该库就会从异步吧cache取出并渲染到界面上,从而优化了主线程,达到了提高用户体验的目的。
总结
综上,AsyncDisplayKit将一些UI绘制可以移到工作线程的工作剥离主线程,并使用iOS的线程技巧来做同步,使用帧数达到60fps,而且在我看来这些技术技巧其实是通用的,完全可以用于iOS甚至Android等其他客户端的编程当中。
当然,AsyncDisplayKit大量的采用线程,也带来了一些接口API在线程同步中不好使用的问题,所以,个人觉得在开发过程如果UI绘制占用的内存并不是特别大的话,可以采用原生的开发模式,毕竟这只是一个给我们优化界面的方式,我们需要考虑维护的成本。 暂写到这,以后有想到什么再续。