1.Node的几个常用方法及用途
-
在一个节点容器的上下文之外使用一个节点有一些弊端,因为所有的节点都具有当前界面状态的概念 ASInterfaceState
-
ASInterfaceState的更新由ASRangeController控制而ASRangeController又由所有的节点容器在内部创建和维护,一个节点的状态有大概三种:Preload、Display、Visible
-init:
initNodeBlocks这个方法会在后台线程调用,因为没有其他方法在init完成之前运行,所以这个不需要加锁。 init方法必须能够在任何队列上调用、不能在节点初始化方法中初始任何UIKit对象、不能在这个方法中添加手势、node.layer,node.view.x相关操作。-didLoad:
当后台视图初始化完成时,会被调用,保证是在主线程,可以在这里执行UIKit的代码。-layoutSpecThatFits
该方法定义节点的布局,并在后台线程上进行了大量的计算,修改布局的地方,这个地方同样也不能使用node.view、node.layer以及他们的属性,此外,最好不要在这个方法中创建其他节点。-layout
1.在此方法中调用super将会使用layoutSpec对象计算布局,所有子节点都将计算其size和position。2.layout在概念上类似于UIViewController 的-viewWillLayoutSubviews,这是一个更改hidden属性、修改view属性、设置背景颜色的好地方。
3.layout中计算子节点的size是很精确的,例如collectionNode可以铺满屏幕,这种情况不被ASLayoutSpec很好的支持,此时简单的做法就是在这个方法中手动设定frame。
2.AsyncDisplayKit的原理实现
AsyncDisplayKit是Facebook开源的一套用于iOS界面流畅的异步绘制UI的框架,其核心是对CPU以及GPU的优化,CPU方面是将一些高消耗运算放在后台线程中进行,减轻主线程负担。CGU方面则是将每一帧的内容渲染成一张texture,这个工作在后台线程完成。
AsyncDisplayKit的基本单元是node,node是UIView以及对应Layer的抽象,与UIView最大的区别是Node是线程安全的,并且可以设置对应的Node内部层次后在后台线程运行,UIView将大量的耗时操作全部都放在了主线程上进行
ASDisplayNode是线程安全的,可以在后台线程创建和修改,Node刚创建时,并不会在内部创建UIView和CALayer,直到主线程第一次访问view或者layer属性时,它才会在内部生成对应的对象。当它的属性(frame、transform)改变时,并不会立刻同步到其持有的view或者layer上,而是把这些改变的属性保存到内存的一个中间变量上,等某一个时机一次性设置到内部的view或layer上,实际绘制代码在ASDisplayNode+AsyncDisplay.mm中。
主线程中比较耗时的主要是图像、布局以及绘制
-
图像
a.解压
UIImage对其内部图像格式的解压发生在即将将图片交给GPU渲染的时候调用drawInRect。 函数或者将UIView可见时,UIImage会将其内部图像格式如PNG,JPEG进行解压。AsyncDisplayKit就是采用后面那个方式将UIView预先放置如一个frame为空的workingView中以达到将view内部的image进行预解压目的。b.处理
图像一般会进行blend运算、strech、crop,这些工作其实可以在GPU中处理,但是UIKit并没有提供此类的API,所以一般我会用CoreGraphic来做,但是CoreGraphic 是CPU drawing比较耗时的。AsyncDisplayKit将这些图像处理放在工程线程中来处理,虽然而是CPU drawing,但是不会影响到UI的顺滑响应。 -
布局
以ASTableView为例,TableView的UI布局需要计算每行的高度,然后计算行内元素的布局,将行插入到TableView中。TableView又是scrollView,需要滚动,一旦行的生成和渲染比较慢,必然会影响到滑动时的流畅体验。这个过程只有将行插入到TableView中需要在UI线程中进行,AsyncDisplayKit在子线程分批次计算行以及其子元素的布局,计算好之后通过dispatch_group_notify通知UI线程将row插入到View中。AsyncDisplayKit还考虑到设备的CPU核数,根据核数来分批次计算row的大小
-
绘制
a.AsyncDisplayKit另一个强大之处在于将UI CALayer 的backing image的绘制放入到工作线程,UIView的真正展现内容在于其CALayer的contents属性,而contents属性对应一个Backing image(内存位图)。默认情况下UIView一旦出现,其会自动创建一个Backing image。
b.AsyncDisplayKit通过CALayer的delegate控制baking image,并通过Core Graphic的方式在工作线程上将View以及其子节点绘制到backing image上,绘制好之后在UI线程上将这个backing image传给CALayer的contents,然后将CALayer的渲染树交给GPU。绘制以组的形式执行减少了Graphic context 的切换。
c.通过transaction的方式管理dispatch_group之间的关系,并且只有在UI线程处于闲置状态或退出时才将transaction commit 并将backing image 赋给CALayer的contents。
核心技术
1.iOS的显示系统是由VSync信号驱动,VSync信号由硬件时钟生成,每秒发出60次,iOS图形服务接收到VSync信号后,会通过IPC通知到App内。App的Runloop在启动后会注册对应的CFRunLoopSource通过mach_port接收传过来的信号通知,随后Source的回调会驱动整个App的动画和显示。
2.CoreAnimation在Runloop中注册了一个Observer,监听BeforeWaiting和Exit事件.当一个触摸事件到来时,Runloop被唤醒,app会执行一些操作(比如创建或调整视图),这些操作都会被CALayer捕获,并通过CATransaction提交到一个中间状态去。当上面的所有操作结束后,Runloop即将进入休眠或退出时,关注该事件的Observer会得到通知,在回调中就会把所有的中间状态合并提交到GPU去显示,如果有动画,CALayer会通过DisplayLink 等机制多次触发相关流程。
3.AsyncDisplayKit在此处也模拟了Core这个机制,所有针对ASNode的修改和提交,总有些任务是必须放在主线程的。当主线这个任务的时候 ,ASNode会把任务封装到ASAsyncTransaction并提交到一个全局的容器中。
AsyncDisplayKit源码阅读笔记
在UIKit 中,将一个UIView加入到视图层级或调用setNeedDisplay后,将会触发绘制逻辑,而处于性能考虑并不会立即绘制,而仅仅是设置这个view为dirty,在下一次绘制事件到来的时候对view进行重绘。
在AsyncDisplayKit中也是从触发重绘的入口着手。
- (void)setNeedsDisplay
{
_bridge_prologue_write;
if (_hierarchyState & ASHierarchyStateRasterized) {
//当前node已被栅格化时,ASDK不再重绘这个图层,转而不断向上寻找这个图层的父图层,
} else {
// 若当前的node的backingView/Layer尚已被初始化,,且setNeedsDisplay是在主线程被调用,
//则简单得转发这个消息给自己的backingView/layer
BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);
if (shouldApply) {
_messageToViewOrLayer(setNeedsDisplay);
} else {
//若不满足上述条件则在node层设置一个标志位,标识自己需要重绘
[ASDisplayNodeGetPendingState(self) setNeedsDisplay];
}
[self __setNeedsDisplay];
}
}
复制代码
ASDK 保证node是线程安全的,所有消息都可以在非主线程调用。
- (void)__setNeedsDisplay {
BOOL shouldScheduleForDisplay = NO;
{
ASDN::MutexLocker l(__instanceLock__);
BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState);
if (_layer != nil && !_flags.synchronous && nowDisplay && [self _implementsDisplay]) {
shouldScheduleForDisplay = YES;
}
}
if (shouldScheduleForDisplay) {
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
}
}
复制代码
__setNeedsDisplay 主要是根据当前是否有重绘逻辑,判断是否需要重绘,如果当前的layer为空,或上面的nowDisplay为false,证明当前的node还未被展示到屏幕上,不需要走绘制的逻辑,因为在view/layer进入对应的hierarchy时,绘制逻辑自然会跑一遍,若当前的node不支持异步绘制,也不会走绘制的逻辑。 因此此时必定是在后台线程(因为有layer又在主线程的话,此消息已被转发给对应的backingView/layer);最后如果并没有override绘制逻辑或不需要栅格化子图层或不是图片展示类型的图层,则此处不会走异步绘制。
当if语句判断通过会执行下面的代码
- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock
void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
复制代码
这两个方法干的比较重要的事就是调用了 [layer setNeedsDisplay]。
也就是说我们的绘制逻辑会最终走到layer层的display方法。
node的backingView/backingLayer进入相应的hierarchy时,如何处罚绘制。
不论是直接实例化——ASDisplayLayer或其子类,加入到node.layer,或者直接对封装好的node的layer层级进行操作,如[node.layer addSublayer: subnode.layer],都不会得到异步绘制的优化。
在_ASDisplayLayer.mm文件的- (void)display:(BOOL)asynchronously中可以看到,layer其实是将实际绘制任务交给自己的delegate完成,所以当直接实例化_ASDisplayLayer或其子类时,delegate为nil,自然没办法执行绘制逻辑。而对于第二种情况,则是因为ASDK仅仅在View层上加入了对层级变化的监听,即在_ASDisplayView.mm的-(void)willMoveToWindow方法作为绘制周期的入口,调用了[ASDisplayNode __enterHierarchy]。
- (void)__enterHierarchy
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy");
ASDN::MutexLocker l(_propertyLock);
if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
_flags.isEnteringHierarchy = YES;
_flags.isInHierarchy = YES;
if (_flags.shouldRasterizeDescendants) {
[self _recursiveWillEnterHierarchy];
} else {
[self willEnterHierarchy];
}
_flags.isEnteringHierarchy = NO;
if (self.contents == nil) {
CALayer *layer = self.layer;
[layer setNeedsDisplay];
if ([self _shouldHavePlaceholderLayer]) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self _setupPlaceholderLayerIfNeeded];
_placeholderLayer.opacity = 1.0;
[CATransaction commit];
[layer addSublayer:_placeholderLayer];
}
}
}
}
复制代码
经过两种不同的逻辑,代码会进入到[_ASDisplayLayer display],并交给[strongAsyncDelegate displayAsyncLayer:self asynchronously:asynchronously] 执行真正的绘制逻辑,对于一般情况下delegate就是ASDisplayNode。
接下来了解何时调用displayBlock这个进行绘制的。
if (asynchronously) {
CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
复制代码
-
获取containerLayer,即当前layer层级中拥有transaction的最高层layer;
-
是调用获取containerLayer的asyncdisplaykit_asyncTransaction,这个可以看做是对CGD的封装。 在这个_ASAsyncTransactionContainerlass中的 asyncdisplaykit_asyncTransaction方法如下:
- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction { _ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncLayerTransaction; if (transaction == nil) { NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions; if (transactions == nil) { transactions = [NSHashTable hashTableWithOptions:NSPointerFunctionsObjectPointerPersonality]; self.asyncdisplaykit_asyncLayerTransactions = transactions; } transaction = [[_ASAsyncTransaction alloc] initWithCallbackQueue:dispatch_get_main_queue() completionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) { [transactions removeObject:completedTransaction]; //通知方法... }]; [transactions addObject:transaction]; self.asyncdisplaykit_currentAsyncLayerTransaction = transaction; //通知方法.. } [[_ASAsyncTransactionGroup mainTransactionGroup] addTransactionContainer:self]; return transaction; } 复制代码
2.1、asyncdisplaykit_asyncLayerTransactions是一个哈希表,用来存 _ASAsyncTransaction实例的。
2.2、 asyncdisplaykit_currentAsyncTransaction表示当前事务的引用;
在这个方法的最后把自己添加到一个事务组中。
3.displayBlock和completionBlock交个第二步getter获取的transaction这个事务管理。
具体来看看addOperationWithBlock这个方法的执行逻辑。
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
priority:(NSInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
[self _ensureTransactionData];
ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (self.state != ASAsyncTransactionStateCanceled) {
operation.value = block();
}
}
});
}
复制代码
3.1 completionBlock被保存到transaction的_operations数组中,而displayBlock被schedule在某一个执行,执行完的结果被存放在operation.value。
3.2 而_group->schedule则是调了这个函数void ASAsyncTransactionQueue::GroupImpl::schedule
void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
{
ASAsyncTransactionQueue &q = _queue;
ASDN::MutexLocker locker(q._mutex);
DispatchEntry &entry = q._entries[queue];
//1
Operation operation;
operation._block = block;
operation._group = this;
operation._priority = priority;
entry.pushOperation(operation);
//2
++_pendingOperations; // enter group
NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;
if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
--maxThreads;
if (entry._threadCount < maxThreads) {
bool respectPriority = entry._threadCount > 0;
++entry._threadCount;
//3
dispatch_async(queue, ^{
ASDN::MutexLocker lock(q._mutex);
// go until there are no more pending operations
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
{
ASDN::MutexUnlocker unlock(q._mutex);
if (operation._block) {
operation._block();
}
operation._group->leave();
operation._block = nil; // the block must be freed while mutex is unlocked
}
}
--entry._threadCount;
if (entry._threadCount == 0) {
NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen
q._entries.erase(queue);
}
});
}
}
复制代码
3.3 简单地封装block,这个方法中的Operation并不是上面方法中提到TransactionOperation混淆,_pendingOperations是一个标志位,代表当前需要排队的任务数量。然后在异步Block主要的逻辑就是要在对应的queue上执行的任务都执行一遍,最后调用operation._group->leave()。
3.4在leave这方法中
void ASAsyncTransactionQueue::GroupImpl::leave()
{
std::lock_guard<std::mutex> l(_queue._mutex);
--_pendingOperations;
if (_pendingOperations == 0) {
std::list<GroupNotify> notifyList;
_notifyList.swap(notifyList);
for (GroupNotify & notify : notifyList) {
dispatch_async(notify._queue, notify._block);
}
_condition.notify_one();
if (_releaseCalled) {
delete this;
}
}
}
复制代码
当前的任务都完成后取了一个类型为GroupNotify的list,并且遍历并用GCD异步执行
程序走到这我们已经成功把displayBlock和completionBlock进行了提交,并且看到displayBlock是如何被封装执行,其返回值又如何传入 ASAsyncTransactionOperation实例中。
但是目前我们还不知道在什么地方执行completionBlock,从而将displayBlock的结果
4.接下来就看看_ASAsyncTransactionGroup.m中, 可以看到这个事务注册了对主线程Runloop的监听,而我们的_ASAsyncTransaction就是加入到这个group中的,器监听分别是kCFRunLoopBeforeWaiting 和 kCFRunLoopExit。
- (void)commit
{
ASDisplayNodeAssertMainThread();
if ([_containers count]) {
NSHashTable *containersToCommit = _containers;
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
for (id<ASAsyncTransactionContainer> container in containersToCommit) {
// Note that the act of committing a transaction may open a new transaction,
// so we must nil out the transaction we're committing first.
_ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
container.asyncdisplaykit_currentAsyncTransaction = nil;
[transaction commit];
}
}
}
复制代码
事务组的commit就是遍历所有的事务,执行事务的commit。
在commit的方法中调用了_ASAsyncTransaction的commit,此处又出现了_group->notify方法,在主线程中执行completionBlock。
- (void)callAndReleaseCompletionBlock:(BOOL)canceled;
{
if (_operationCompletionBlock) {
_operationCompletionBlock(self.value, canceled);
self.operationCompletionBlock = nil;
}
}
复制代码
将displayBlock的结果在这里交给completionBlock,让它在主线程展示绘制的结果。
总结:ASyncDisplayKit的绘制流程被触发,会一方面监听MainRunLoop的事件,另一方面会执行绘制逻辑(递归地获取整个nodeTree的绘制代码,并在一个上下文中会被截图),同时在MainRunLoop运行到合适的时机,将后台绘制结果简单的赋值给layer.contents,完成界面的展示。
后台绘制耗时很长,而runloop的observerCallBack通知会在绘制完成前结束,runloop进入休眠后。由于绘制结束后的notify最终会在mainQueue中执行layer.contents = image的逻辑,因此runloop会因为需要操作主线程事务而被唤醒,以__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__的方式执行赋值操作。