基础知识
RunLoop
UIView的生命周期
某个视图的层次一改变,该视图就会收到一次回调。
- 调用addSubivew:成功后会给该视图发送didAddSubivew:回调,触发UIView的子类在心增视图时执行其他操作。
- didMoveToSuperview:会通知相关视图他们的上级视图已经变化。添加和移除都会调用,所以要判断 superView在不在
- 视图移动前会发出willMoveToSuperview:回调
- didMoveToWindow:回调和didMoveToSuperview:相似,从命名上能看出其区别。
- willMoveToWindow:在视图移动前发出的回调。
- willRemoveToSubview:回调通知父视图子视图即将被删除
ASDK渲染过程
主要流程简介
ASDK的渲染过程主要分为4条主线
- 初始化ASDisplayNode对应的UIView或者CALayer
- 在当前视图进入视图层级时,执行setDisplay方法
- display方法执行时,想后台线程分发绘制事物
- 注册主线层RunLoop的观察者,在每个RunLoop结束时回调
初始化ASDisplayNode对应的UIView或者CALayer
在当前视图进入视图层级时,执行setDisplay方法
display方法执行时,想后台线程分发绘制事物
派发异步绘制任务(_ASDisplayLayer)
1 | - (void)display |
1 |
|
主要是通过delegate的代理了派发流程
派发流程(ASDisplayNode + AsyncDisplay)
1 | - (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously |
代码比较长,但逻辑比较简单,主要做了三件事情:
- 创建displayBlock,真正逻辑的渲染都在这里
- 创建completionBlock,completionBlock会在主线程执行,负责将渲染完成的内存显示到屏幕
- 将以上两个block添加到_ASAsyncTransaction
到这里分发的过程就算完成了,以上只是将block添加到了transaction,那block的执行时机是什么时候呢?
注册主线层RunLoop的观察者,在每个RunLoop结束时回调
上一节中提到的两个block的执行时机,是通过注册主线层RunLoop的观察者来确定的。
_ASAsyncTransaction
1 | + (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup |
上述代码会在RunLoop
即将退出或进入休眠时,执行transactionGroup
的commit
方法。也就是每次RunLoop
结束时开始绘制内容
_ASAsyncTransactionGroup
1 | - (void)commit |
ASAsyncTransactionOperation
1 | - (void)commit |
1 | - (void)completeTransaction |
上述代码描述了commit流程,目前只看到了上一节中提到的completionBlock
在主线程被执行,displayBlock
又是什么时候被执行的呢?这就得看ASAsyncTransactionQueue
了
ASAsyncTransactionQueue 绘制事物队列
ASAsyncTransactionQueue
是后台绘制线程与主线层之间通信的桥梁,负责将后台绘制完成的内容,通知给主线层显示。其实是一个生产-消费者的设计模式,这里有两条生产线:
- 第一条生产线是上一节中提到的
display
方法中,不停地生产displayBlock
- 第二条生产线是,主线层RunLoop的观察者不停地生产
GroupNotify
,GroupNotify
其实是对completionBlock
的封装
具体消费流程:
视图进入视图层时,也就是
_ASDisplayView
的willMoveToWindow
被调用时,对应的CALayer执行setDisplay
方法对应的CALayer的
display
执行,向后台线程分发绘制事物,将上一节提到两个block添加到_ASAsyncTransaction,并间接调用ASAsyncTransactionQueue
的schedule
方法schedule
方法将displayBlock
压入堆栈,然后将displayBlock
出栈并执行,执行displayBlock
后调用leave
方法。这里为什么要用堆栈呢,主要为了保证最后进入视图层的View,最早被绘制。leave
方法会取出第二条生产先产生的notify
并执行,也就是在主线程中显示绘制完成的内容。
可以看出,ASDK确实将许多昂贵的操作移出了主线程,有效地加快了视图的绘制与渲染,保证了主线程的流畅性
ASDK是如何提高绘制效率的
在上一节中,我们了解到了ASDK的渲染流程,主要是通过将displayBlock
中的绘制放到后台线程,加快了视图的绘制与渲染,保证了主线程的流畅性。那ASDK又是如何提高绘制效率的呢?
先看一下displayBlock
的创建
1 | - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous |
可看到ASDK根据shouldBeginRasterizing
将displayBlock
指向了不同的block。
栅格化
shouldBeginRasterizing
是栅格化的一个属性,当它为true
时,ASDK会将子视图递归的绘制在父视图上。
1 | if (shouldBeginRasterizing) { |
绘制图片
这种方式更多的适用于图片节点ASImageNode
1 | - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { |
使用GC绘制
文字的绘制一般都会在- drawRect:withParameters:isCancelled:isRasterizing:
进行,这个方法只是提供了一个合适的用于绘制的上下文,该方法不止可以绘制文字,只是在这里绘制文字比较常见:
1 | - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { |
ASNetworkImageNode
使用方法
一般的使用方法,初始化并设置一个图片URL就可以了
1 | ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; |
ASNetworkImageNode的setURL
方法最终调用的其实是- (void)setURLs: resetToDefault:
方法
- (void)setURLs: resetToDefault:
1 | - (void)setURLs:(NSArray <NSURL *> *)URLs resetToDefault:(BOOL)reset{ |
做了两件事情:
- 取消上一张图片的下载,取消下载就是通过调用
_downloader
的cancelImageDownloadForIdentifier
方法来取消
- 预加载图片,预加载则是将创建
_downloader
的任务封装block扔到队列里,让异步线程去执行。
渲染过程
ASNetworkImageNode调用链
可以看到ASNetworkImageNode在willMoveToWindow
方法中,间接调用到了_downloadImageWithCompletion
方法,该方法实际上是将下载图片的block抛给异步线程处理。
1 | - (void)_downloadImageWithCompletion:(void (^)(id <ASImageContainerProtocol> imageContainer, NSError*, id downloadIdentifier))finished |
1 | void ASPerformBlockOnBackgroundThread(void (^block)(void)) |
那么ASNetworkImageView的图片加载逻辑是:
视图进入视图层(willMoveToWindow)-> 将下载图片封装成任务丢给异步线程(_downloadImageWithCompletion)-> 异步下载完成后,将图片渲染到屏幕,也就是在传给-downloadImageWithURLs: callbackQueue: downloadProgress: completion:;
方法的completion
block中将图片设置到ASNetworkImageView的
这个completion
block是在 - (void)_lazilyLoadImageIfNecessary
方法中创建并传给_downloadImageWithCompletion
的
completion
block代码1
2
3
4
5
6
7
8
9
10
11
12
13auto finished = ^(id <ASImageContainerProtocol>imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) {
if (imageContainer != nil) {
[strongSelf _locked_setCurrentImageQuality:1.0];
if ([imageContainer asdk_animatedImageData] && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) {
id animatedImage = [strongSelf->_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]];
[strongSelf _locked_setAnimatedImage:animatedImage];
} else {
[strongSelf _locked__setImage:[imageContainer asdk_image]];
}
strongSelf->_imageLoaded = YES;
}
}
事实上在_locked__setImage:
方法中又会调用setNeedsDisplay
方法,从而触发上一节讲到的异步渲染逻辑。
使用过程中存在的坑
通过前面两个小节可以发现,_downloader
的创建以及cancel
其实是在不同的线程里执行的。那么就有可能存在cancelImageDownloadForIdentifier
会在_downloader
创建之前执行,从而导致加载失效URL的情况,在页面快速滚动的时候会造成UIView的闪烁,以及产生性能问题,造成卡顿。