1 使用 Hit Test Slop 来扩大某个节点的点击区间

个人觉得这个功能还是十分便捷的,有时候设计很经常又要求图片显示区域小,但是点击区域又要很大,这时候就可以使用Hit Test Slop来完成。
hitTestSlop属性是一个UIEdgeInsets类型,可以通过它来扩大或者缩小可点击区域。值得注意的是这个属性在ASDisplayNode类中,所以任何的节点都可以使用它来扩展点击区域。

Hit Test的可视化调试工具:

http://texturegroup.org/docs/debug-tool-hit-test-visualization

使用方法:

在AppDelegate.m 文件中导入AsyncDisplayKit+Debug.h 在 didFinishLaunchingWithOptions 方法里面添加

[ASControlNode setEnableHitTestDebug:YES]

需要注意的是上面这个调用必须在所有的ASControlNodes初始化之前。

2 提前批量获取

在默认情况下,在用户滚动列表的时候,当到达数据还没到达界面上的时候可以提前批量获取数据。如果需要提前获取可以指定leadingScreensForBatching

tableNode.view.leadingScreensForBatching = 3.0; 

指定leadingScreensForBatching 后我们滚动列表,一旦列表到达需要预加载的区域,就会调用shouldBatchFetchForTableNode方法,这个方法可以决定是否需要预先加载数据。

- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
if (_weNeedMoreContent) {
return YES;
}
return NO;
}

如果shouldBatchFetchForTableNode返回YES 那么就会通过tableNode:willBeginBatchFetchWithContext 来获取数据。注意这个方法是在后台调用的,如果我们需要处理一些只有在主线程才能执行的动作那么需要切换到主线程。

- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context  {
// 从远程网络或者本地获取数据
NSArray *newPhotos = [SomeSource getNewPhotos];

// 将数据插入table
[self insertNewRowsInTableNode:newPhotos];

// 决定是否还需要继续获取数据
_stillDataToFetch = ...;

// 结束批量获取
[context completeBatchFetching:YES];
}
3 图片修改Block

imageModificationBlock 会在图片显示到ASImageNode之前对图片进行一些预处理,使用imageModificationBlock可以很方便得添加图片特效,添加圆角,边界或者其他处理。这是相当方便的功能。

4 占位图

任何一个ASDisplayNode子类都可以重写-placeholderImage方法来提供一个占位图,这个占位图将会等到节点的内容完全加载后消失。要使用占位图,必须设置placeholderEnabled属性为YES,还可以设置placeholderFadeDuration 来指定占位图消失的过渡时间。同时还需要注意的是placeholderImage是在后台线程中调用的所以必须保证它是线程安全的,所以我们在加载图片的时候可以使用-[UIImage imageWithContentsOfFile:] 但是不能使用-[UIImage imageNamed:].因为后者是线程不安全的。

除了placeholderImage ASNetworkImageNode 节点也有对应的占位图,我们可以使用defaultImage来指定一个图片,当URL为空或者给定的URL加载失败的时候就会显示这个默认图。

5 视图与图层之间的切换

视图和图层是两个不同量级的组件,我们一般会在不需要触摸处理的情况下使用图层,但是在开发中我们还是会默认使用视图,因为需求不断改变的,现在不需要触摸处理不代表以后不需要触摸处理,一旦需要处理触摸事件的时候估计就傻眼了,因为在UIKit中从视图切换到图层是十分费时的,这种事情遇到过才知道这个过程的痛苦,如果使用Texture 就不存在这个问题,在Texture中将整个子树从视图转换为图层非常简单,只需要下面一行代码就可以搞定:

rootNode.isLayerBacked = YES;
6 将子树光栅化

将整个视图层次结构转化为单个视图层可以提高性能,在Texture中值需要下面一行代码,就可以将从该点开始的整个节点层次结构呈现为一个层

[rootNode enableSubtreeRasterization];

1. Texture 异步绘制源码分析
1.1 整体流程图

在开始分析代码之前先给大家看下整个异步绘制的关键流程图,我把它划分成两条主线,第一条以setNeedDisplay为起点,将displayBlock返回的UImage在Group中转换为ASAsyncTransationOperation的value,另一条是以
RunLoop休眠前及退出RunLoop事件为触发点,将第一条主线产生的UImage通过ASAsyncTransationOperation 的 operationCompleteBlock 传到layer.content.

整个图可以分成两大块,第一块是displayBlock生成UImage,第二块是UImage怎么传到layer.content

围绕着这两条主线,两大块就可以捋清楚整个异步绘制的整个流程。

1.2 触发异步绘制的入口点

Texture触发异步绘制的入口点主要有如下两个地方:

  • UIView加入视图层级,这时候会调用willMoveToWindow
  • 直接调用setNeedsDisplay方法

其实最终都会归并到setNeedsDisplay

首先看下willMoveToWindow,这里会调用ASDisplayNode的__enterHierarchy

- (void)willMoveToWindow:(UIWindow *)newWindow {
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
BOOL visible = (newWindow != nil);
if (visible && !node.inHierarchy) {
[node __enterHierarchy];
}
}

在__enterHierarchy中会调用子节点的__enterHierarchy,并且根据具体情况创建placeHolderLayer添加到当前layer上

- (void)__enterHierarchy
{
ASDisplayNodeAssertMainThread();
//.......

if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
//.......
//更新状态
[self willEnterHierarchy];
//遍历节点调用节点的__enterHierarchy
for (ASDisplayNode *subnode in self.subnodes) {
[subnode __enterHierarchy];
}
//.......
if (self.contents == nil) { //如果内容为空
CALayer *layer = self.layer;
[layer setNeedsDisplay]; //调用layer的setNeedsDisplay
//如果有占位图则显示占位图
if ([self _locked_shouldHavePlaceholderLayer]) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
//显示placeHolderLayer
[self _locked_setupPlaceholderLayerIfNeeded];
_placeholderLayer.opacity = 1.0;
[CATransaction commit];
//将placehHolderLayer添加到layer
[layer addSublayer:_placeholderLayer];
}
}
}
[self didEnterHierarchy];
}

这里比较关键的是setNeedsDisplay,在这里会创建后续需要的渲染队列,渲染队列是一个高优先级的串行异步队列,displayBlock就是这个队列中执行的,

- (void)setNeedsDisplay
{
//...........
{
//...........
if (isRasterized == NO && shouldApply == NO) {
// We can't release the lock before applying to pending state, or it may be flushed before it can be applied.
[ASDisplayNodeGetPendingState(self) setNeedsDisplay];
}
}

if (isRasterized) {
ASPerformBlockOnMainThread(^{
// The below operation must be performed on the main thread to ensure against an extremely rare deadlock, where a parent node
// begins materializing the view / layer hierarchy (locking itself or a descendant) while this node walks up
// the tree and requires locking that node to access .rasterizesSubtree.
// For this reason, this method should be avoided when possible. Use _hierarchyState & ASHierarchyStateRasterized.
ASDisplayNodeAssertMainThread();
ASDisplayNode *rasterizedContainerNode = self.supernode;
while (rasterizedContainerNode) {
if (rasterizedContainerNode.rasterizesSubtree) {
break;
}
rasterizedContainerNode = rasterizedContainerNode.supernode;
}
//将根节点标记为dirty
[rasterizedContainerNode setNeedsDisplay];
});
} else {
if (shouldApply) {
// If not rasterized, and the node is loaded (meaning we certainly have a view or layer), send a
// message to the view/layer first. This is because __setNeedsDisplay calls as scheduleNodeForDisplay,
// which may call -displayIfNeeded. We want to ensure the needsDisplay flag is set now, and then cleared.
[viewOrLayer setNeedsDisplay];
}
[self __setNeedsDisplay];
}
}
- (void)__setNeedsDisplay
{
BOOL shouldScheduleForDisplay = NO;
{
MutexLocker l(__instanceLock__);
BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState);
// FIXME: This should not need to recursively display, so create a non-recursive variant.
// The semantics of setNeedsDisplay (as defined by CALayer behavior) are not recursive.
if (_layer != nil && !checkFlag(Synchronous) && nowDisplay && [self _implementsDisplay]) {
shouldScheduleForDisplay = YES;
}
}

if (shouldScheduleForDisplay) {
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
}
}

+ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node
{
static dispatch_once_t onceToken;
static ASRunLoopQueue<ASDisplayNode *> *renderQueue;
dispatch_once(&onceToken, ^{
renderQueue = [[ASRunLoopQueue<ASDisplayNode *> alloc] initWithRunLoop:CFRunLoopGetMain()
retainObjects:NO
handler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) {
[dequeuedItem _recursivelyTriggerDisplayAndBlock:NO];
if (isQueueDrained) {
CFTimeInterval timestamp = CACurrentMediaTime();
[[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification
object:nil
userInfo:@{ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp: @(timestamp)}];
}
}];
});

as_log_verbose(ASDisplayLog(), "%s %@", sel_getName(_cmd), node);
[renderQueue enqueue:node];
}

经过层层调用会调到_ASDisplayLayer的display方法

- (void)display
{
ASDisplayNodeAssertMainThread();
[self _hackResetNeedsDisplay];

if (self.displaySuspended) {
return;
}

[self display:self.displaysAsynchronously];
}

display方法中的实际工作就是调用 - (void)display:(BOOL)asynchronously,在- (void)display:(BOOL)asynchronously中将流程转到asyncDelegate中

- (void)display:(BOOL)asynchronously
{
if (CGRectIsEmpty(self.bounds)) {
_attemptedDisplayWhileZeroSized = YES;
}
[self.asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}
1.3 几个重要类之间的关系

接下来就是比较重要的异步绘制的流程了,这里主要分两大块:

  1. displayBlock,completionBlock 指责是什么,怎么构建出来的,在什么时机调用?
  2. 整个异步绘制的流程是怎样的?

对于第一点我们先留一个印象,displayBlock是用于生成UImage的,completionBlock是用于将displayBlock获得的UIImage设置到layer.content中。我们先把最大块的第二点给梳理清楚—整个异步绘制的流程。

在开始之前需先梳理下_ASAsyncTransactionGroup,ASAsyncTransactionContainer,_ASAsyncTransaction,ASAsyncTransactionQueue,ASAsyncTransactionOperation,DispatchEntry,Operation 这些类的关系,其实梳理了这些类的关系整个异步绘制流程就会显得十分清晰,建议大家可以结合文章开始时候给出的图来看。

  • _ASAsyncTransactionGroup

_ASAsyncTransactionGroup 用于管理ASAsyncTransactionContainer,ASAsyncTransactionContainer通过_ASAsyncTransactionGroup的addTransactionContainer方法添加到_ASAsyncTransactionGroup中,这里有个很重要的_ASAsyncTransactionGroup 就是mainTransactionGroup,在每个主线程RunLoop即将进入睡眠期间,以及由于RunLoop模式切换等原因导致的当前RunLoop退出的时候将添加mainTransactionGroup中ASAsyncTransactionContainer中的_ASAsyncTransaction 调用commit进行提交,这里先卖个关子,先不介绍commit到底是干啥的。我们先看看mainTransactionGroup:


+ (_ASAsyncTransactionGroup *)mainTransactionGroup
{
ASDisplayNodeAssertMainThread();
static _ASAsyncTransactionGroup *mainTransactionGroup;
//懒加载_ASAsyncTransactionGroup
if (mainTransactionGroup == nil) {
mainTransactionGroup = [[_ASAsyncTransactionGroup alloc] _init];
//注册监听runloop结束或者进入休眠之前的通知
[mainTransactionGroup registerAsMainRunloopObserver];
}
return mainTransactionGroup;
}

mainTransactionGroup 是以懒加载的方式实例化出来,在创建的时候会注册监听kCFRunLoopBeforeWaiting和kCFRunLoopExit这两个通知,在RunLoop睡眠和退出之前,在主线程调用commit。

- (void)registerAsMainRunloopObserver
{
ASDisplayNodeAssertMainThread();
static CFRunLoopObserverRef observer;
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
kCFRunLoopExit); // before exiting a runloop run

observer = CFRunLoopObserverCreateWithHandler(NULL, // allocator
activities, // activities
YES, // repeats
INT_MAX, // order after CA transaction commits
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
ASDisplayNodeCAssertMainThread();
//在进入runloop休眠之前或者切换runloop模式导致runloop退出的时候会调用commit
[self commit];
});
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}

在commit中遍历添加到mainTransactionGroup中的TransactionContainer,调用asyncdisplaykit_currentAsyncTransaction的commit方法提交,
提交后会将asyncdisplaykit_currentAsyncTransaction设置为nil。

- (void)commit
{
ASDisplayNodeAssertMainThread();

if ([_containers count]) {
NSHashTable *containersToCommit = _containers;
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];

//遍历添加到mainTransactionContainer中的TransactionContainer,调用asyncdisplaykit_currentAsyncTransaction的commit方法
for (id<ASAsyncTransactionContainer> container in containersToCommit) {
_ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
container.asyncdisplaykit_currentAsyncTransaction = nil;
[transaction commit];
}
}
}
  • ASAsyncTransactionContainer

ASAsyncTransactionContainer 是Transaction的容器,它其实是一个CALayer,最重要的有两个属性:

asyncdisplaykit_currentAsyncTransaction  当前的异步事务
asyncdisplaykit_asyncLayerTransactions 当前layer的所有异步事务

在 - (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously方法中会在整个层级树中找到最顶层的父节点,调用ASAsyncTransactionContainer的asyncdisplaykit_asyncTransaction方法为父节点创建asyncdisplaykit_asyncLayerTransactions以及asyncdisplaykit_currentAsyncTransaction,然后将当前TransactionContainer 添加到 mainTransactionGroup。代码如下:

- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction
{
_ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncTransaction;
if (transaction == nil) {
//如果当前的异步Transaction为空
NSMutableSet<_ASAsyncTransaction *> *transactions = self.asyncdisplaykit_asyncLayerTransactions;
if (transactions == nil) {
//检查transactions是否为空,如果为空则创建一个并作为asyncdisplaykit_asyncLayerTransactions
transactions = ASCreatePointerBasedMutableSet();
self.asyncdisplaykit_asyncLayerTransactions = transactions;
}
__weak CALayer *weakSelf = self;
transaction = [[_ASAsyncTransaction alloc] initWithCompletionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) {
__strong CALayer *self = weakSelf;
if (self == nil) {
return;
}
[self.asyncdisplaykit_asyncLayerTransactions removeObject:completedTransaction];
if (self.asyncdisplaykit_asyncLayerTransactions.count == 0) {
// Reclaim object memory.
self.asyncdisplaykit_asyncLayerTransactions = nil;
}
[self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction];
}];

//将transaction添加到transactions进行管理
[transactions addObject:transaction];
self.asyncdisplaykit_currentAsyncTransaction = transaction;
[self asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:transaction];
}
//ASAsyncTransactionContainer----> transations ----> transaction
//将当前TransactionContainer 添加到 mainTransactionGroup,在runloop结束或者休眠之前会调用transaction的commit
[_ASAsyncTransactionGroup.mainTransactionGroup addTransactionContainer:self];
return transaction;
}
  • _ASAsyncTransaction

_ASAsyncTransaction 是啥?源码中给出了如下的描述:

@summary ASAsyncTransaction provides lightweight transaction semantics for asynchronous operations.
ASAsyncTransaction 为异步操作提供了轻量级的事务片段

@desc ASAsyncTransaction provides the following properties:

  • Transactions group an arbitrary number of operations, each consisting of an execution block and a completion block.
    Transactions 将任意多个operations打包在一起,每个Transaction包含一个executionBlok以及一个completionblock
  • The execution block returns a single object that will be passed to the completion block.
    execution block将会生成一个对象,然后将这个对象传递给completionblock
  • Execution blocks added to a transaction will run in parallel on the global background dispatch queues;
    the completion blocks are dispatched to the callback queue.
    添加到transaction的execution blocks将会在后台线程中并行运行,而completion block将会在回调队列中执行。
  • Every operation completion block is guaranteed to execute, regardless of cancelation.
    However, execution blocks may be skipped if the transaction is canceled.
    只要transaction不被取消每个completion block都会确保执行。
  • Operation completion blocks are always executed in the order they were added to the transaction, assuming the
    callback queue is serial of course.
    completion总是会按照加入到transaction的顺序执行。

应用到当前场景,简单得说就是,每个_ASAsyncTransaction包裹着display Block和 complage Block,display block将会在后台并行执行,然后将结果传递给complete block,complete block会按照顺序在主线程中顺序执行。

@implementation _ASAsyncTransaction
{
ASAsyncTransactionQueue::Group *_group;
NSMutableArray<ASAsyncTransactionOperation *> *_operations;
}

_ASAsyncTransaction 有两个主要的私有成员属性,_group和_operations。这就会扯到ASAsyncTransactionQueue,ASAsyncTransactionOperation,Operation,DispatchEntry

  • ASAsyncTransactionOperation

首先看下_operations 它其实是一个ASAsyncTransactionOperation数组,ASAsyncTransactionOperation类的结构如下,

@interface ASAsyncTransactionOperation : NSObject

- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock;

@property (nonatomic) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock;
@property id value; // set on bg queue by the operation block

@end

这里比较关键的是operationCompletionBlock和value,operationCompletionBlock是最前面提到的complete block 用于将display block 返回的UIImage传递给layer.content.

value 就是 display block 返回的UIImage,这里会在后台线程中生成UIImage,然后将其保存到value中,这个后面会介绍。最关键的是callAndReleaseCompletionBlock这个方法,CompletionBlock就是在这个方法中调用的,至于callAndReleaseCompletionBlock什么时候调用先留在后面。

- (void)callAndReleaseCompletionBlock:(BOOL)canceled;
{
ASDisplayNodeAssertMainThread();
if (_operationCompletionBlock) {
//调用CompletionBlock
_operationCompletionBlock(self.value, canceled);
_operationCompletionBlock = nil;
}
}

总而言之ASAsyncTransactionOperation封装着complete block,和一个存储display block执行结果的value值,并且在某个时期调用complete block

  • ASAsyncTransactionQueue

ASAsyncTransactionQueue 其实是一个并发数受限的轻量级operation队列,它包含了Group,Operation,DispatchEntry 三大对象。

Group 有点类似于 dispatch_group_t,它比较重要的有两个方法:

// 在指定队列中调度block
virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0;

// 所有先前调度的block都完成的时候发送的通知block
virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0;

schedule 方法中会创建一个Operation然后通过DispatchEntry pushOperation添加到DispatchEntry,DispatchEntry中的线程数是受控制的,它最多是当前处理器内核数的两倍,如果还有线程数可以用,那么将会从DispatchEntry中按照优先级pop出一个operation执行它的block。要注意这里的block不是display block也不是complete block。而是执行display block后将值存储到operation.value中。

void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
{
ASAsyncTransactionQueue &q = _queue;
std::lock_guard<std::mutex> l(q._mutex);

DispatchEntry &entry = q._entries[queue];

Operation operation;
operation._block = block;
operation._group = this;
operation._priority = priority;
entry.pushOperation(operation);

++_pendingOperations; // enter group

#if ASDISPLAYNODE_DELAY_DISPLAY
NSUInteger maxThreads = 1;
#else
NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;

// Bit questionable maybe - we can give main thread more CPU time during tracking.
if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
--maxThreads;
#endif

if (entry._threadCount < maxThreads) { // we need to spawn another thread

// first thread will take operations in queue order (regardless of priority), other threads will respect priority
bool respectPriority = entry._threadCount > 0;
++entry._threadCount;

dispatch_async(queue, ^{
std::unique_lock<std::mutex> lock(q._mutex);

// go until there are no more pending operations
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
lock.unlock();
if (operation._block) {
operation._block();
}
operation._group->leave();
operation._block = nil; // the block must be freed while mutex is unlocked
lock.lock();
}
--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);
}
});
}
}

notify 方法是将block和queue封装成GroupNotify后添加到_notifyList,我们看到schedule方法中会调用_group->leave()

void ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t queue, dispatch_block_t block)
{
std::lock_guard<std::mutex> l(_queue._mutex);

if (_pendingOperations == 0) {
dispatch_async(queue, block);
} else {
GroupNotify notify;
notify._block = block;
notify._queue = queue;
_notifyList.push_back(notify);
}
}

在leave中会将_notifyList取出来,将GroupNotify的block放到queue中执行。

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;
}
}
}

那么这里就有两个问题,notify什么时候被调用,_notifyList放的是什么东西。这个也放在流程中讲,这里只需要了解,Group 实际上是一个线程数受控的类似于dispatch_group_t的任务队列,每次调用shedule的时候,会将block封装成一个Operation,然后添加到DispatchEntry,然后在DispatchEntry线程数没达到最大值的时候,会从DispatchEntry中取出来一个执行它的block,然后在leave中,将_notifyList运行完。

  • DispatchEntry && Operation

DispatchEntry 放着两个东西,一个是操作列表_operationQueue,一个是以优先级为key,操作列表为value的map _operationPriorityMap。在添加的时候会同时往这两个上面同时添加。

struct DispatchEntry // entry for each dispatch queue
{
typedef std::list<Operation> OperationQueue;
typedef std::list<OperationQueue::iterator> OperationIteratorList; // each item points to operation queue
typedef std::map<NSInteger, OperationIteratorList> OperationPriorityMap; // sorted by priority

OperationQueue _operationQueue;
OperationPriorityMap _operationPriorityMap;
int _threadCount;

Operation popNextOperation(bool respectPriority); // assumes locked mutex
void pushOperation(Operation operation); // assumes locked mutex
};

Operation 就是个结构体封装着block group和优先级

struct Operation
{
dispatch_block_t _block;
GroupImpl *_group;
NSInteger _priority;
};

稍稍总结下,我们会将一个block(非display block)包装在Operation 然后添加到DispatchEntry,中的OperationQueue和OperationPriorityMap,在Group的schedule方法中从DispatchEntry按照优先级取出后,在后台线程中执行block,并在执行完后执行leave方法。

这里有几个预留的问题下面要介绍的:

  1. Operation 中的block到底是啥
  2. Group的notify操作是干啥的,什么时候调用,leave 方法中的_notifyList放的是什么东西
1.4 异步绘制流程

有了上面的铺垫这里可以开始讲整个异步绘制流程了,我们先看下:- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
{
ASDisplayNodeAssertMainThread();

//......
///1. CancelBlock displayBlock completetionBlock 初始化
//创建CancelBlock
asdisplaynode_iscancelled_block_t isCancelledBlock = nil;
if (asynchronously) {
uint displaySentinelValue = ++_displaySentinel;
__weak ASDisplayNode *weakSelf = self;
isCancelledBlock = ^BOOL{
__strong ASDisplayNode *self = weakSelf;
return self == nil || (displaySentinelValue != self->_displaySentinel.load());
};
} else {
isCancelledBlock = ^BOOL{
return NO;
};
}

// 创建displayBlock,这里会调用delegate display 或者draw方法来获取到UIImage内容
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously
isCancelledBlock:isCancelledBlock
rasterizing:NO];
//.........

// 创建completionBlock:这个block会在当前异步事务完成后并完成渲染后在主线程被调用,或者在同步的情况下(asynchronously == NO)直接调用
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) { //是否被取消,同步的情况下isCancelledBlock始终为NO
UIImage *image = (UIImage *)value; //value放置的是image
BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)); //图像是否是可拉伸的
if (stretchable) {
ASDisplayNodeSetResizableContents(layer, image);
} else {
//不可拉伸的情况下直接将内容作为contents
layer.contentsScale = self.contentsScale;
layer.contents = (id)image.CGImage;
}
//............
}
};

//............

if (asynchronously) {
// 异步渲染操作被包含在transaction中,transaction可以允许并行处理最终将结果以同步的方式应用到layer的contents属性中
// Async rendering operations are contained by a transaction, which allows them to proceed and concurrently
// while synchronizing the final application of the results to the layer's contents property (completionBlock).

// 2. 我们先查看我们是否可以将自己添加到父类的transaction容器中
// First, look to see if we are expected to join a parent's transaction container.
CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;

// 如果transaction没有存在那么这里将会实例化并将它添加到_ASAsyncTransactionGroup,它会在runloop结束或者即将进入休眠之前提交出去
// In the case that a transaction does not yet exist (such as for an individual node outside of a container),
// this call will allocate the transaction and add it to _ASAsyncTransactionGroup.
// It will automatically commit the transaction at the end of the runloop.
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;

//3. 将displayBlock添加到transaction,transaction将会立即启动执行
// Adding this displayBlock operation to the transaction will start it IMMEDIATELY.
// The only function of the transaction commit is to gate the calling of the completionBlock.
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
//同步的情况下直接通过displayBlock生成contents,并通过completionBlock设置到layer.content
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
},

上面代码有三个关键点:

  1. 创建CancelBlock displayBlock completetionBlock 这里先不展开
  2. 顺着层级树找到最顶层的节点作为TransationContainer,并根据实际情况创建它的asyncdisplaykit_currentAsyncTransaction。并将TransationContainer添加到mainTransactionGroup。
  3. 调用addOperationWithBlock 往asyncdisplaykit_currentAsyncTransaction中添加display Block和complete Block.
- (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
{
ASDisplayNodeAssertMainThread();
//...........
//创建一个TransationOperation将它添加到_operations,并且在displayQueue中执行displayBlock() 并将输出的UIImage保存到value中
ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (self.state != ASAsyncTransactionStateCanceled) {
operation.value = block();
}
}
});
}

addOperationWithBlock 完成两个工作:

  1. 将complete block 封装到ASAsyncTransactionOperation,并添加到_operations中
  2. 调用group 的 schedule,这里就可以解答第一个问题,schedule 中的block其实是如下的一个block,用于执行displayBlock后将输出的UIImage保存到value中
^{
@autoreleasepool {
if (self.state != ASAsyncTransactionStateCanceled) {
operation.value = block();
}
}
}

还有第二个问题:
Group的notify操作是干啥的,什么时候调用,leave 方法中的_notifyList放的是什么东西

ASAsyncTransaction的commit方法中

- (void)commit
{
ASDisplayNodeAssertMainThread();
NSAssert(self.state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction");
self.state = ASAsyncTransactionStateCommitted;

if ([_operations count] == 0) {
// Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously.
if (_completionBlock) {
_completionBlock(self, NO);
}
} else {
NSAssert(_group != NULL, @"If there are operations, dispatch group should have been created");
//通过_group发出通知将后台得到的UImage通过completeBlock
_group->notify(dispatch_get_main_queue(), ^{
[self completeTransaction];
});
}
}

- (void)completeTransaction
{
ASDisplayNodeAssertMainThread();
ASAsyncTransactionState state = self.state;
if (state != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (state == ASAsyncTransactionStateCanceled);
//遍历每个ASAsyncTransactionOperation 让后台生成的image通过ComplateBlock设置到layer.content上
for (ASAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}

// Always set state to Complete, even if we were cancelled, to block any extraneous
// calls to this method that may have been scheduled for the next runloop
// (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled)
self.state = ASAsyncTransactionStateComplete;

//通过回调通知当前transation结束
if (_completionBlock) {
_completionBlock(self, isCanceled);
}
}
}

我们知道commit是在Runloop进入休眠状态或者退出之前调用的,这里调用了group的notify方法,将completeTransaction 添加到_notifyList,在group shedule方法中执行block后将display block产生的UIImage 放到value中,然后调用leave方法,将_notifyList中的block放到queue中执行,也就是在主线程中执行complete block,将value传出来最终设置到layer.content中。

总结一下:

  1. 首先,在RunLoop进入休眠状态或者退出的时候通知到mainTransationGroup,mainTransationGroup 在commit方法中,遍历加入到mainTransationGroup中的每个TransationContainer,在TransationContainer中会对currentTransation调用一次commit方法。从而触发Group的notify方法,将在主线程执行的block操作放到Group的_notifyList中,这个bock操作就是将displayBlock生成的对象通过completeBlock传递给layer.content.

  2. 在displayAsyncLayer方法中将会创建displayBlock 和 complateBlock,然后封装到ASAsyncTransationOperation,并将它添加到当前页面的TransationContainer中的currentTransation中。在currentTransation中将diaplayBlock获取UIImage并存储到ASTransationOperation value中的操作封装成block,在Group中的DispathEntry中执行。执行完block后会调用Group leave,执行_notifyList中的任务,将ASTransationOperation value中的 UIImage通过complete Block设置到layer.content.

1.5 DisplayBlock && Complete Block
  1. Complete Block其实没多少东西可以介绍的,它的逻辑十分简单:就是将传入的value也就是displayBlock生成的UIImage设置到layer.content
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) { //是否被取消,同步的情况下isCancelledBlock始终为NO
UIImage *image = (UIImage *)value; //value放置的是image
BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)); //图像是否是可拉伸的
if (stretchable) {
ASDisplayNodeSetResizableContents(layer, image);
} else {
//不可拉伸的情况下直接将内容作为contents
layer.contentsScale = self.contentsScale;
layer.contents = (id)image.CGImage;
}
//........
};
  1. DisplayBlock
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous
isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock
rasterizing:(BOOL)rasterizing
{
ASDisplayNodeAssertMainThread();
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags;

__instanceLock__.lock();

flags = _flags;

// We always create a graphics context, unless a -display method is used, OR if we are a subnode drawing into a rasterized parent.
//如果没有display方法或者正在将子节点绘制到栅格化的父类上的时候需要创建Context
BOOL shouldCreateGraphicsContext = (flags.implementsImageDisplay == NO && rasterizing == NO);
//子节点时候需要栅格化
BOOL shouldBeginRasterizing = (rasterizing == NO && flags.rasterizesSubtree);
//是否实现了display方法
BOOL usesImageDisplay = flags.implementsImageDisplay;
//是否实现了drawRect方法
BOOL usesDrawRect = flags.implementsDrawRect;
//如果没有实现display/drawRect方法并且子节点不需要栅格化直接返回
if (usesImageDisplay == NO && usesDrawRect == NO && shouldBeginRasterizing == NO) {
// Early exit before requesting more expensive properties like bounds and opaque from the layer.
__instanceLock__.unlock();
return nil;
}

BOOL opaque = self.opaque;
CGRect bounds = self.bounds;
UIColor *backgroundColor = self.backgroundColor;
CGColorRef borderColor = self.borderColor;
CGFloat borderWidth = self.borderWidth;
CGFloat contentsScaleForDisplay = _contentsScaleForDisplay;

__instanceLock__.unlock();

// Capture drawParameters from delegate on main thread, if this node is displaying itself rather than recursively rasterizing.
id drawParameters = (shouldBeginRasterizing == NO ? [self drawParameters] : nil);

// Only the -display methods should be called if we can't size the graphics buffer to use.
if (CGRectIsEmpty(bounds) && (shouldBeginRasterizing || shouldCreateGraphicsContext)) {
return nil;
}
//..........
//是否栅格化
if (shouldBeginRasterizing) {
// Collect displayBlocks for all descendants.
// 用于放置子节点的displayBlock在父节点的displayBlock中直接遍历各个子节点的displayBlock
NSMutableArray *displayBlocks = [[NSMutableArray alloc] init];
[self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
CHECK_CANCELLED_AND_RETURN_NIL();

opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f;

//这里会将整个container的所有子节点的displayBlock打包给displayBlock,在执行container的displayBlock后会调用这些子节点的displayBlock将其绘制到container上
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
UIImage *image = ASGraphicsCreateImageWithOptions(bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
for (dispatch_block_t block in displayBlocks) {
if (isCancelledBlock()) return;
block();
}
});

ASDN_DELAY_FOR_DISPLAY();
return image;
};
} else {

displayBlock = ^id{

CHECK_CANCELLED_AND_RETURN_NIL();

__block UIImage *image = nil;
void (^workWithContext)() = ^{
CGContextRef currentContext = UIGraphicsGetCurrentContext();

if (shouldCreateGraphicsContext && !currentContext) {
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return;
}

// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
//根据情况调用display方法或者drawRect方法
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
ASDN_DELAY_FOR_DISPLAY();
};

if (shouldCreateGraphicsContext) {
return ASGraphicsCreateImageWithOptions(bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
} else {
workWithContext();
return image;
}
};
}
//...........
return displayBlock;
}

display Block 的开始会先通过flags来判断当前的子节点是否需要栅格化,flags这个结构体变量主要用来标记该Node的一些状态属性,以及是否重载了某些方法。如果需要栅格化,那么就需要遍历各个子节点,从各个子节点中获取自身的display Block放置到一个名字为displayBlocks的数组中。这些Block负责生成自身的UIImage.根节点的displayBlock中包含着子节点的display block.在绘制页面的时候,通过这些子节点的display block在绘图的context中将自己绘制出来。这里比较关键的是_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock方法:

- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks
{
// Skip subtrees that are hidden or zero alpha.
if (self.isHidden || self.alpha <= 0.0) {
return;
}

__instanceLock__.lock();
BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized);
__instanceLock__.unlock();

// if super node is rasterizing descendants, subnodes will not have had layout calls because they don't have layers
if (rasterizingFromAscendent) {
[self __layout];
}

// Capture these outside the display block so they are retained.
UIColor *backgroundColor = self.backgroundColor;
CGRect bounds = self.bounds;
CGFloat cornerRadius = self.cornerRadius;
BOOL clipsToBounds = self.clipsToBounds;

CGRect frame;

// If this is the root container node, use a frame with a zero origin to draw into. If not, calculate the correct frame using the node's position, transform and anchorPoint.
if (self.rasterizesSubtree) {
frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height);
} else {
CGPoint position = self.position;
CGPoint anchorPoint = self.anchorPoint;

// Pretty hacky since full 3D transforms aren't actually supported, but attempt to compute the transformed frame of this node so that we can composite it into approximately the right spot.
CGAffineTransform transform = CATransform3DGetAffineTransform(self.transform);
CGSize scaledBoundsSize = CGSizeApplyAffineTransform(bounds.size, transform);
CGPoint origin = CGPointMake(position.x - scaledBoundsSize.width * anchorPoint.x,
position.y - scaledBoundsSize.height * anchorPoint.y);
frame = CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height);
}

// Get the display block for this node.
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES];

// We'll display something if there is a display block, clipping, translation and/or a background color.
BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds;

// If we should display, then push a transform, draw the background color, and draw the contents.
// The transform is popped in a block added after the recursion into subnodes.
if (shouldDisplay) {
dispatch_block_t pushAndDisplayBlock = ^{
// Push transform relative to parent.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

CGContextTranslateCTM(context, frame.origin.x, frame.origin.y);

//support cornerRadius
if (rasterizingFromAscendent && clipsToBounds) {
if (cornerRadius) {
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip];
} else {
CGContextClipToRect(context, bounds);
}
}

// Fill background if any.
CGColorRef backgroundCGColor = backgroundColor.CGColor;
if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) {
CGContextSetFillColorWithColor(context, backgroundCGColor);
CGContextFillRect(context, bounds);
}

// If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store).
if (displayBlock) {
UIImage *image = (UIImage *)displayBlock();
if (image) {
BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage));
CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal;
[image drawInRect:bounds blendMode:blendMode alpha:1];
}
}
};
[displayBlocks addObject:pushAndDisplayBlock];
}

// Recursively capture displayBlocks for all descendants.
for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
}

// If we pushed a transform, pop it by adding a display block that does nothing other than that.
if (shouldDisplay) {
// Since this block is pure, we can store it statically.
static dispatch_block_t popBlock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
popBlock = ^{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextRestoreGState(context);
};
});
[displayBlocks addObject:popBlock];
}
}

_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock 这个方法中也调用_displayBlockWithAsynchronous来获取displayBlock,只不过需要注意的是rasterizing参数为YES导致_displayBlockWithAsynchronous中shouldBeginRasterizing为NO.直接走displayWithParameters/drawRect部分。绘制子节点的工作在pushAndDisplayBlock中完成,pushAndDisplayBlock就是被添加到displayBlocks的子block。

如果不需要栅格化,就直接走displayWithParameters/drawRect部分具体是displayWithParameters还是drawRect取决于flags中的标志,如果走displayWithParameters将会直接获取image返回,如果是drawRect将会在当前Context中绘制自身绘制。

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous
isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock
rasterizing:(BOOL)rasterizing
{
ASDisplayNodeAssertMainThread();
//........
//是否实现了display方法
BOOL usesImageDisplay = flags.implementsImageDisplay;
//是否实现了drawRect方法
BOOL usesDrawRect = flags.implementsDrawRect;
//..........
//是否栅格化
if (shouldBeginRasterizing) {
//.......
} else {
displayBlock = ^id{
__block UIImage *image = nil;
void (^workWithContext)() = ^{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
// .......
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
//根据情况调用display方法或者drawRect方法
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
ASDN_DELAY_FOR_DISPLAY();
};

if (shouldCreateGraphicsContext) {
return ASGraphicsCreateImageWithOptions(bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
} else {
workWithContext();
return image;
}
};
}
//...........
return displayBlock;
}
1. Texture 布局源码分析

最早iOS平台上的布局方式是基于frame的布局,但是后续iPhone推出了一系列不同尺寸的设备,这时候frame布局方式就显得十分麻烦,为了解决这个问题Auto Layout 就诞生了,如果使用过Auto Layout原生接口进行布局的经历,我相信一定是十分难受的,但是随着Masonry的推出,让Auto Layout逐渐被开发者所接受,
但是Auto Layout与frame 布局不同的是我们给出的是组件与组件之间的约束关系,然后由系统布局引擎帮我们计算出每个组件的实际frame。也就是说Auto Layout最终还是要转换成组件的frame,只不过这个转换系统帮我们做了,我们只需要给出约束条件就可以了。但是Auto Layout每个组件的约束最终都会转换为一个N元一次的线性等式或者不等式,要计算出整个页面每个组件frame的值,就需要根据这些N元一次的线性等式或者不等式组成的不等式组解出结果。这个时间是不确定的,并且界面越复杂不等式组也就越复杂,计算所耗费的时间也就越多。最要命的是它还会强制视图在主线程上布局。如果这个时间超过16.67ms就会导致界面的卡顿。

那么我们有没有一套可以不用手动计算布局参数,又能有较高的布局效率的布局引擎呢?我们先看下Texture的布局引擎的源码,在这里寻找下我们需要的答案。

Texture 布局引擎是可以在后台线程中运行的布局引擎,它采用了目前前端比较流行的FlexBox布局形式,具体的使用大家可以看下之前的介绍Texture布局使用的博客。Texture 2.X 目前采用了两套布局引擎:LayoutSpec 布局规范 和 Yoga 布局引擎,这里先介绍 LayoutSpec 布局规范引擎。

Texture 布局引擎比较突出的特点是能够在后台计算布局,并且能够缓存布局结果。

Texture中可以通过如下四种方式指定布局:

* 提供 layoutSpecBlock
* 覆写 - layoutSpecThatFits: 方法
* 覆写 - calculateSizeThatFits: 方法
* 覆写 - calculateLayoutThatFits: 方法
    • layoutSpecThatFits: 与 layoutSpecBlock 是完全等价的只不过是实现的形式不一样而已。
    • calculateSizeThatFits: 这种方式提供了手动布局的方式,通过在该方法内对 frame 进行计算,返回一个当前视图的 CGSize,它和UIView 中的 -[UIView sizeThatFits:] 非常相似 只不过 Texture中的布局会对所有计算出的布局进行缓存来提高性能。
    • calculateLayoutThatFits: 把上面的两种布局方式:手动布局和 Spec 布局封装成了一个接口,这样,无论是 CGSize 还是 ASLayoutSpec 最后都会以 ASLayout 的形式返回给方法调用者。一般推荐calculateLayoutThatFits覆写这个方法,而不是layoutSpecThatFits。

还是老规矩在介绍流程之前先熟悉下关键对象的组成:

  1. ASLayoutElement
@protocol ASLayoutElement <ASLayoutElementExtensibility, ASTraitEnvironment, ASLayoutElementAsciiArtProtocol>

//包括ASLayoutElementTypeLayoutSpec以及ASLayoutElementTypeDisplayNode,前者为布局约束元素,后一个表明为显示节点元素

@property (nonatomic, readonly) ASLayoutElementType layoutElementType;
/**
* 尺寸约束
*/
@property (nonatomic, readonly) ASLayoutElementStyle *style;
/**
* 当前节点的子节点集合
*/
- (nullable NSArray<id<ASLayoutElement>> *)sublayoutElements;

/**
* 要求节点基于给定的尺寸范围返回一个ASLayout布局
* 这个方法会缓存约束条件和布局结果,所以子类不能覆盖它,因为它会缓存calculateLayoutThatFits获得的结果,如果没有缓存结果那么这个方法的将会十分耗时
*/
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize;

/**
* 让子元素在calculateLayoutThatFits计算它们的布局,这个是供给内部使用的我们不应该覆写这个方法,但是可以通过覆写-calculateLayoutThatFits来代替,constrainedSize 是一个最大值和最小值的一个限制,最终得到的尺寸必须在这个范围内。这个方法也是会缓存计算的结果和约束条件
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize;

/**
* 通过覆写这个方法来计算当前布局元素的布局
*/
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize;

@end
  1. ASLayout

@interface ASLayout : NSObject

/**
* 当前布局对象
*/
@property (nonatomic, weak, readonly) id<ASLayoutElement> layoutElement;

/**
* 节点类型
*/
@property (nonatomic, readonly) ASLayoutElementType type;

/**
* 当前布局的Size
*/
@property (nonatomic, readonly) CGSize size;

/**
* 在父节点的位置
*/
@property (nonatomic, readonly) CGPoint position;

/**
* 子节点的布局
*/
@property (nonatomic, copy, readonly) NSArray<ASLayout *> *sublayouts;

/**
* 某个节点的位置
*/
- (CGRect)frameForElement:(id<ASLayoutElement>)layoutElement;

/**
* 根据postion和size计算出来的frame
*/
@property (nonatomic, readonly) CGRect frame;

//......

/**
* Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts
*/
- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT;

@end

在介绍关键都类之后我们来看下整个布局流程:
我们以手动调用节点都layoutThatFits方法作为入口点。在layoutThatFits:中简单得转调了下layoutThatFits:parentSize

- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize
{
return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];
}

- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize
{
//......
ASLayout *layout = nil;
//在每次调用-setNeedsLayout / -invalidateCalculatedLayout 方法的时候_layoutVersion会+1
NSUInteger version = _layoutVersion;
//先检查_calculatedDisplayNodeLayout是否可用,如果可用则优先使用
if (_calculatedDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) {
layout = _calculatedDisplayNodeLayout.layout;
} else if (_pendingDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) {
//检查_pendingDisplayNodeLayout是否可用,如果可用则优先使用
layout = _pendingDisplayNodeLayout.layout;
} else {
//如果_calculatedDisplayNodeLayout和_pendingDisplayNodeLayout都不可用则新建一个ASLayout作为_pendingDisplayNodeLayout
// Create a pending display node layout for the layout pass
layout = [self calculateLayoutThatFits:constrainedSize
restrictedToSize:self.style.size
relativeToParentSize:parentSize];
_pendingDisplayNodeLayout = ASDisplayNodeLayout(layout, constrainedSize, parentSize,version);
}

return layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}];
}

最初的时候_calculatedDisplayNodeLayout和_pendingDisplayNodeLayout都是不可用的,所以layout是由calculateLayoutThatFits:restrictedToSize:relativeToParentSize 返回的,然后将其包裹成ASDisplayNodeLayout赋值给_pendingDisplayNodeLayout,这里需要注意的是这里的_layoutVersion,每次调用-setNeedsLayout / -invalidateCalculatedLayout 方法的时候_layoutVersion会+1,当版本不对的时候isValid就会返回NO,这里的_calculatedDisplayNodeLayout和_pendingDisplayNodeLayout应该是一个布局缓存。这个等后续的时候进一步查看。

- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
restrictedToSize:(ASLayoutElementSize)size
relativeToParentSize:(CGSize)parentSize
{
ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(self.style.size, parentSize);
const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, styleAndParentSize);
ASLayout *result = [self calculateLayoutThatFits:resolvedRange];
return result;
}

在calculateLayoutThatFits中会对限制的范围做个重新修正,再以修正后的约束范围作为参数计算布局。

- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
//调用方法之前的检查,必须提供layoutSpecBlock或者重写calculateLayoutThatFits,calculateSizeThatFits或者layoutSpecThatFits
__ASDisplayNodeCheckForLayoutMethodOverrides;

//根据不同布局引擎类型实现不同的布局
switch (self.layoutEngineType) {
//使用布局规范引擎
case ASLayoutEngineTypeLayoutSpec:
return [self calculateLayoutLayoutSpec:constrainedSize];
#if YOGA
//使用YOGA布局引擎
case ASLayoutEngineTypeYoga:
return [self calculateLayoutYoga:constrainedSize];
#endif
default:
break;
}
return nil;
}

在Texture 2.0开始引入了YOGA布局引擎,YOGA布局是FaceBook推出的布局形式,我们这里以Texture 默认采用的LayoutSpec作为研究对象进行介绍。

- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize
{
// 没有通过_layoutSpecBlock也没有实现LayoutSpecThatFits指定布局则直接通过calculateSizeThatFits手动指定size
if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) {
//手动指定尺寸
CGSize size = [self calculateSizeThatFits:constrainedSize.max];
return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil];
}

// 通过_layoutSpecBlock 或者 layoutSpecThatFits来从node中获得ASLayoutElement
id<ASLayoutElement> layoutElement = [self _locked_layoutElementThatFits:constrainedSize];

// 调用ASLayoutElement的layoutThatFits:方法计算得到ASLayout。
ASLayout *layout = ({
AS::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation);
[layoutElement layoutThatFits:constrainedSize];
});

//.......
layout = [layout filteredNodeLayoutTree];

return layout;
}
  1. 首先先判断是否有设置过_layoutSpecBlock或者实现过layoutSpecThatFits:这两者其实是等效的,如果这两者都没有,那么就只能通过calculateSizeThatFits手动指定size。将用户手动指定的Size封装成ASLayout返回

  2. 如果有实现layoutSpecThatFits:或者有设置过_layoutSpecBlock那么就通过各自的方法返回实现了ASLayoutElement协议的对象。

  3. 通过调用layoutThatFits返回ASLayout,这里会调用各个ASLayoutSpec子类的calculateLayoutThatFits方法。这里以最简单的ASWrapperLayoutSpec为例子进行分析:

- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
NSArray *children = self.children;
const auto count = children.count;
ASLayout *rawSublayouts[count];
int i = 0;
CGSize size = constrainedSize.min;
//遍历子节点,获得子节点的ASLayout
for (id<ASLayoutElement> child in children) {
ASLayout *sublayout = [child layoutThatFits:constrainedSize parentSize:constrainedSize.max];
sublayout.position = CGPointZero;
//获得最大的宽高
size.width = MAX(size.width, sublayout.size.width);
size.height = MAX(size.height, sublayout.size.height);
//将子节点的布局保存起来
rawSublayouts[i++] = sublayout;
}
const auto sublayouts = [NSArray<ASLayout *> arrayByTransferring:rawSublayouts count:i];
//将size转换为ASLayout
return [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts];
}

ASWrapperLayoutSpec的功能就是用一个布局将轮廓包起开,这里会遍历每个子节点获取子节点的尺寸,获取最大的size,并通过layoutWithLayoutElement将size转换为ASLayout返回。

calculateLayoutLayoutSpec针对上面获得的ASLayout还有最后一步处理:

layout = [layout filteredNodeLayoutTree];

什么是扁平化,我们之前的步骤都是针对节点的宽高。扁平化就是针对position进行修正。

- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED
{

struct Context {
unowned ASLayout *layout;
CGPoint absolutePosition;
};
// Queue used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::deque<Context> queue;
//将_sublayouts数据先填到queue
for (ASLayout *sublayout in _sublayouts) {
queue.push_back({sublayout, sublayout.position});
}
//这个是扁平化的子布局
std::vector<ASLayout *> flattenedSublayouts;
while (!queue.empty()) {
//从queue中拿出数据
const Context context = std::move(queue.front());
queue.pop_front();

unowned ASLayout *layout = context.layout;
// Direct ivar access to avoid retain/release, use existing +1.
const NSUInteger sublayoutsCount = layout->_sublayouts.count;
const CGPoint absolutePosition = context.absolutePosition;
//是否是DisplayNode节点,如果是节点就不需要扁平化,看absolutePosition是否和layout.position是否相同,如果不相同就需要新建一个放到flattenedSublayouts
if (ASLayoutIsDisplayNodeType(layout)) {
if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) {
// Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position.
const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement
size:layout.size
position:absolutePosition
sublayouts:@[]];
flattenedSublayouts.push_back(newLayout);
} else {
flattenedSublayouts.push_back(layout);
}
} else if (sublayoutsCount > 0) {
//如果是一个布局容器,那么就需要根据节点之间的关系修正positon位置
// Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue.
unowned ASLayout *rawSublayouts[sublayoutsCount];
[layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)];
for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) {
queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position});
}
}
}
//将flattenedSublayouts转换成ASLayout
NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()];
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array];
[layout retainSublayoutElements];
return layout;
}

那我们怎么将ASLayout应用到节点的frame完成布局呢?我们在上面介绍ASLayout的时候有提到一个方法:

- (CGRect)frameForElement:(id<ASLayoutElement>)layoutElement;

它传入一个节点或者节点容器,会返回一个frame:

- (CGRect)frameForElement:(id<ASLayoutElement>)layoutElement
{
for (ASLayout *l in _sublayouts) {
if (l->_layoutElement == layoutElement) {
return l.frame;
}
}
return CGRectNull;
}

frameForElement这个方法在ASDisplayNode+Layout.mm类中的_layoutSublayouts调用,这里遍历当前节点的子节点,将每个节点传入frameForElement,获得frame,再将frame赋给node.frame,完成布局。

- (void)_layoutSublayouts
{
ASDisplayNodeAssertThreadAffinity(self);
//............
for (ASDisplayNode *node in self.subnodes) {
CGRect frame = [layout frameForElement:node];
if (CGRectIsNull(frame)) {
// There is no frame for this node in our layout.
// This currently can happen if we get a CA layout pass
// while waiting for the client to run animateLayoutTransition:
} else {
node.frame = frame;
}
}
}

可能大家还会有一个疑问,我们上面介绍的都是posistion,size那frame怎么得到的?其实frame 就是一个postion和size组合而来。

- (CGRect)frame
{
CGRect subnodeFrame = CGRectZero;
CGPoint adjustedOrigin = _position;
//.......
subnodeFrame.origin = adjustedOrigin;
CGSize adjustedSize = _size;
//......
subnodeFrame.size = adjustedSize;
return subnodeFrame;
}

那么是怎么触发_layoutSublayouts呢?我们看下ASDisplayNode.mm的__layout方法。

- (void)__layout
{
//......
BOOL loaded = NO;
{
//........
// This method will confirm that the layout is up to date (and update if needed).
// Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning).
l.unlock();
// 如果过期了就会重新计算,否则使用缓存的布局
[self _u_measureNodeWithBoundsIfNecessary:bounds];
l.lock();
//布局占位图
[self _locked_layoutPlaceholderIfNecessary];
}
//调用_layoutSublayouts
[self _layoutSublayouts];

if (loaded) {
ASPerformBlockOnMainThread(^{
[self layout];
[self _layoutClipCornersIfNeeded];
[self _layoutDidFinish];
});
}
[self _fallbackUpdateSafeAreaOnChildren];
}

而__layout 是再layoutIfNeeded中调用的。

- (void)layoutIfNeeded
{
//.......
if (shouldApply) {
// The node is loaded and we're on main.
// Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]).
[viewOrLayer layoutIfNeeded];
} else if (loaded == NO) {
// The node is not loaded and we're not on main.
[self __layout];
}
}

提到layoutIfNeeded我们一定会联想到setNeedsLayout,再UIView中会将UIView标记为dirty需要布局。那么ASDisplayNode呢?

- (void)setNeedsLayout
{
//.....
if (shouldApply) {
// The node is loaded and we're on main.
// Quite the opposite of setNeedsDisplay, we must call __setNeedsLayout before messaging
// the view or layer to ensure that measurement and implicitly added subnodes have been handled.
[self __setNeedsLayout];
[viewOrLayer setNeedsLayout];
} else if (loaded == NO) {
// The node is not loaded and we're not on main.
[self __setNeedsLayout];
}
}
- (void)__setNeedsLayout
{
[self invalidateCalculatedLayout];
}
- (void)invalidateCalculatedLayout
{
_layoutVersion++;
}

看到了吧它只是将_layoutVersion加1,这样再使用的时候由于版本不对就将原来的布局过期处理。

整个布局流程如下图所示:

概述

上一节已经提到了Texture 的布局引擎包含了如下几种布局规范,这一节将对这些布局规范进行展开

ASWrapperLayoutSpec
ASStackLayoutSpec
ASInsetLayoutSpec
ASOverlayLayoutSpec
ASBackgroundLayoutSpec
ASCenterLayoutSpec
ASRatioLayoutSpec
ASRelativeLayoutSpec
ASAbsoluteLayoutSpec
ASCornerLayoutSpec

具体布局规范


ASLayoutSpec

所有布局的父类,一般很少用到,但是可以作为间隔占位空间使用。

类型 字段名 说明
ASDimension .style.width ASLayoutElement内容区域的宽度属性,默认值是ASDimensionAuto,注意它不是最终的宽度值,最终的宽度值会受minWidth和maxWidth属性影响
ASDimension .style.height ASLayoutElement内容区域的高度属性,默认值是ASDimensionAuto
ASDimension .style.minWidth ASLayoutElement内容区域的最小宽度,默认是ASDimensionAuto
ASDimension .style.maxWidth ASLayoutElement内容区域的最大宽度,默认是ASDimensionAuto
ASDimension .style.minHeight ASLayoutElement内容区域的最小高度,默认是ASDimensionAuto
ASDimension .style.maxHeight ASLayoutElement内容区域的最大高度,默认是ASDimensionAuto
ASDimension .style.preferredSize preferredSize提供了一个建议的尺寸,如果它超过了提供的最小尺寸和最大尺寸,那么最小尺寸和最大尺寸将会强制限制,也就是说这个只是一个建议的尺寸数据,如果这个值没有提供那么节点的尺寸将会是它的固有尺寸
CGSize .style.minSize 设置最小的尺寸边界
CGSize .style.maxSize 设置最大的尺寸边界
ASLayoutSize .style.preferredLayoutSize preferredLayoutSize 为布局节点提供了一个建议的相对尺寸,ASLayoutSize 和 CGSize 的区别是它使用的是相对百分比而不是绝对的像素值,和preferredSize类似如果它超过了提供的最小尺寸和最大尺寸,那么最小尺寸和最大尺寸将会强制限制
ASLayoutSize .style.minLayoutSize minSize的百分比版本
ASLayoutSize .style.maxLayoutSize maxSize的百分比版本

ASStackLayoutSpec

这里有一个可以比较直观体验ASStackLayoutSpec的小游戏,大家可以在看完后体验下:
https://huynguyen.dev/froggy-asdk-layout/

如果大家对Flex不是很了解还可以通过下面的网站看下,比较直观
https://demos.scotch.io/visual-guide-to-css3-flexbox-flexbox-playground/demos/
https://www.flexboxpatterns.com/

ASStackLayoutSpec 是 Texture Flaxbox 布局的核心,Flexbox旨在为不同屏幕尺寸提供一致的布局。在堆叠布局中,我们可以将物品以垂直或水平堆叠方式对齐。堆栈布局可以是另一个堆栈布局的子布局,这使得使用堆栈布局规范几乎可以创建任何布局。

容器参数

ASStackLayoutSpec 在ASLayoutElement的基础上加上了7个额外的属性:

1. direction: 指明它的子元素的布局方向

纵向:ASStackLayoutDirectionVertical
横向:ASStackLayoutDirectionHorizontal

2. spacing: 子元素之间的间距

3. horizontalAlignment: 指定子项水平对齐的方式
4. verticalAlignment: 指定子项垂直对齐的方式
5. justifyContent: 它定义了沿主轴的对齐方式。

ASStackLayoutJustifyContentStart 从前端往后端排列
ASStackLayoutJustifyContentCenter 居中排列
ASStackLayoutJustifyContentEnd 从后端往前端排列
ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔
ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔

6. alignItems: 它定义了沿交叉轴的对齐方式。

ASStackLayoutAlignItemsStart 从前往后排列
ASStackLayoutAlignItemsEnd 从后往前排列
ASStackLayoutAlignItemsCenter 居中排列
ASStackLayoutAlignItemsStretch 拉伸排列
ASStackLayoutAlignItemsBaselineFirst 以第一个文字元素基线排列(主轴是横向才可用)
ASStackLayoutAlignItemsBaselineLast以最后一个文字元素基线排列(主轴是横向才可用)

这里需要注意的是主轴和交叉轴的定义,可以看我之前的Flutter分享的内容。

7. flexWrap: 子元素是否堆叠到单行或者多行,默认是堆叠到单行
8: alignContent: 多行的情况下交叉轴的对其方向
节点参数
类型 字段名 说明
CGFloat .style.spacingBefore 该对象在堆叠方向的起始部分添加一定的空间
CGFloat .style.spacingAfter 该对象在堆叠方向的结束部分添加一定的空间
CGFloat .style.flexGrow 如果所有的子节点在堆叠的方向尺寸和小于最小的尺寸,那么作为子节点的当前对象是否需要扩充它的大小
CGFloat .style.flexShrink 如果所有的子节点在堆叠的方向尺寸和大于最大的尺寸,那么作为子节点的当前对象是否需要压缩它的大小
CGFloat .style.flexBasis 为当前节点设置一个初始大小,但是这个初始大小并不是最终的大小它会受flexGrow/flexShrink的设置所影响
ASStackLayoutAlignSelf .style.alignSelf 为当前元素指定交叉轴的对齐方向,这个值将会覆盖alignItems
CGFloat .style.ascender 这个用于基准线对其的方式,表示从对象的顶部距离它的基准线的尺寸
CGFloat .style.descender 这个用于基准线对其的方式,表示从对象的基准线到节点对象底部的尺寸

ASInsetLayoutSpec

这个相当于在某个布局外面加个空白间距。如果在UIEdgeInsets中将INFINITY设置为一个值,则插入规范将仅使用子内在尺寸,也就是默认的尺寸。


ASOverlayLayoutSpec && ASBackgroundLayoutSpec

ASOverlayLayoutSpec 和 ASBackgroundLayoutSpec 为层叠布局,将一个布局叠加在另一个布局上,他们都需要注意的是它们的尺寸是根据基准大小尺寸计算而来的,基准的尺寸会通过constrainedSize传递给它们,所以基准必须要有一个固有的尺寸或者手动设定了它的宽高。对于ASOverlayLayoutSpec它的基准元素是底部的那个元素,对于ASBackgroundLayoutSpec 它的基准元素是顶部的元素。这一点需要注意下


ASCenterLayoutSpec

ASCenterLayoutSpec将其子控件中心置于其最大约束大小内。

如果ASCenterLayoutSpec的宽度或高度不受约束,则会缩小到子组件的大小。
ASCenterLayoutSpec有两个属性:

centeringOptions。确定子组件在中心规格内的居中方式。选项包括:无,X​​,Y,XY。
sizingOptions。确定ASCenterLayoutSpec将占用多少空间。选项包括:默认,最小X,最小Y,最小XY。


ASRatioLayoutSpec

ASRatioLayoutSpec可以缩放的固定高宽比来布局组件,ASRatioLayoutSpec必须有宽度或高度作为constrainedSize传递给它作为缩放的基数。


ASRelativeLayoutSpec

顾名思义是一个相对布局规范,指定了当前节点相对另一个节点的位置。


ASCornerLayoutSpec

ASCornerLayoutSpec 主要用于像小红点那样角标布局而设计的。


ASAbsoluteLayoutSpec

ASAbsoluteLayoutSpec可以通过设置它们的layoutPosition属性来指定其子项的确切位置(X/Y坐标)。绝对布局相对来说比较不灵活很难适配到各种机型。

节点参数
类型 字段名 说明
CGFloat .style.layoutPosition 当前节点在父节点的位置参数

尺寸创建

CGFloat, ASDimension

是一个可以容纳百分比值,像素值的尺寸数据

百分比方式

ASDimensionMake(@"50%");  
ASDimensionMakeWithFraction(0.5);

像素点方式

ASDimensionMake(@"70pt");
ASDimensionMake(70);
ASDimensionMakeWithPoints(70);

一般用ASDimensionMakeWithXXX会比较明确,个人比较喜欢这种方式。

CGSize, ASLayoutSize

和ASDimension类似,它的存在也是为了兼容像素和百分比而存在的,它的宽高元素是ASDimension。它用于.preferredLayoutSize, .minLayoutSize 以及 .maxLayoutSize这些属性中

ASLayoutSizeMake(ASDimension width, ASDimension height);
ASSizeRange

ASSizeRange 是由两个CGSize,这两个CGSize决定了某个元素的最小尺寸和最大尺寸区间


源码地址
Examples
学习资源

Texture 的组成

概述:

提到Texture大家可能会比较陌生,但是如果说AsyncDisplayKit估计搞iOS的很多人都会或多或少听到过,我们知道UIKit大多数的布局绘制工作都必须在主线程完成。Texture 中的Node是线程安全的我们可以在后台子线程中完成实例创建整个布局的计算。我们知道如果要保证界面流畅必须保证每秒60帧的帧率,也就是说要在16ms时间内完成所有的布局和绘制代码。但是主线程一般还有如事件相应之类的任务需要处理,所以往往留给布局绘制的时间通常小于10毫秒甚至更少。所以Texture的思路就是将Node设计成线程安全的,并将那些例如node创建,图片解码,文本尺寸计算,渲染等操作可以移到后台子线程的尽量移到后台线程,只有在必须在主线程处理的时候才切换到主线程。

Node Vs UIView Vs CALayer:

Node,UIView,CALayer 三者的关系如下图所示,详细的介绍会在后面详细张开来介绍。

节点 与 节点容器

Texture中提供的节点有如下几种:

Texture UIView
ASDisplayNode 等效于UIView,所有Node的父类
ASCellNode 等效于UITableViewCell和UICollectionViewCell,用于ASTableNode, ASCollectionNode和ASPagerNode
ASScrollNode 等效于UIScrollView,它用于创建一个包含其他节点的自定义滚动区域
ASEditableTextNode 等效于UITextView
ASTextNode 等效于UILabelView
ASImageNode,ASNetworkImageNode,ASMultiplexImageNode 等效于UIImageView
ASVideoNode 等效于AVPlayerLayer
ASVideoPlayerNode 等效于UIMoviePlayer
ASControlNode 等效于UIControl
ASButtonNode 等效于UIButton
ASMapNode 等效于MKMapView

这些节点和UIVi布局和的区别是所有的node在后台子线程中进行布局和显示,所以主线程可以预留有充足的时间来相应用户交互事件。

Texture中提供的节点容器有如下几种:

Texture UIView
ASCollectionNode 等效于UICollectionView
ASTableNode 等效于UITableView
ASPagerNode 等效于UIPageViewController
ASViewController 等效于UIViewController
ASNavigationController 等效于UINavigationController
ASTabBarController 等效于UITabBarController

那么节点容器和UIKit中的普通的视图容器优点在哪里呢?这就要提到Texture的智能预加载功能,在每个节点添加到一个滚动的或者翻页界面的时候,在界面滚动的时候,节点的interfaceState会不断改变,这个interfaceState属性是由ASRangeController来完成更新工作的,如果在节点容器外使用节点的话ASRangeController将不会更新节点的状态。这将导致界面上的节点不会感知到界面状态的改变,从而导致界面上闪烁的现象。

状态 描述
预加载 这个阶段是从网络或者磁盘加载资源的阶段
显示 这个阶段主要是完成文本珊格化和图片解码工作
可见 节点出现在界面上

这些阶段都有对应的回调节点:

预加载阶段

-didEnterPreloadState
-didExitPreloadState

显示阶段

-didEnterDisplayState
-didExitDisplayState

可见阶段

-didEnterVisibleState
-didExitVisibleState
布局引擎

Texture中不能使用AutoLayout布局方式,它内部实现了一套基于FlexBox的模式的布局引擎,Flex布局模式目前已经成为了前端UI框架的标配,目前比较流行的Flutter,SwiftUI都有它的影子。

Texture的布局性能介于手动布局和Auto Layout方式布局,并且明显快于Auto Layout,这主要是因为布局可以在后台线程中进行计算,所以不会干扰到用户交互,并且由于布局结果是不可变的数据结构,因此可以在后台预计算和缓存它们。

Texture的布局涉及到两个概念:

  • Layout Specs (布局规范)
Layout Specs 充当布局容器的角色,主要用于放置Layout Elements ,它主要关注的是Layout Elements 之间的位置关系。
Texture 提供了如下的布局容器:
ASWrapperLayoutSpec
ASStackLayoutSpec
ASInsetLayoutSpec
ASOverlayLayoutSpec
ASBackgroundLayoutSpec
ASCenterLayoutSpec
ASRatioLayoutSpec
ASRelativeLayoutSpec
ASAbsoluteLayoutSpec
ASCornerLayoutSpec

  • Layout Elements (布局元素)
ASDisplayNodes 和 ASLayoutSpecs 都遵循ASLayoutElement协议,所以ASDisplayNodes 和 ASLayoutSpecs 都可以作为布局元素节点。

这里还需要了解的一个概念是布局元素的固有尺寸
有些元素在没有加载元素之前本身是有一个固有尺寸的,这体现在如果我们没有为它添加宽高,它本身就有一个固有宽高值这些Node元素包括:

ASImageNode
ASTextNode
ASButtonNode

反之某些元素在没有加载元素之前是没有尺寸数据的,因此在数据未加载之前可能需要我们给他指定一个宽高,这类元素有:

ASVideoNode
ASVideoPlayerNode
ASNetworkImageNode
ASEditableTextNode

资料汇总:

源码地址
官方文档地址
Examples
官方资源
iOS 保持界面流畅的技巧
使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能
AsyncDisplayKit 即刻技术团队系列文章
AsyncDisplayKit近一年的使用体会及疑难点
AsyncDisplayKit 2.0 Tutorial: Getting Started
AsyncDisplayKit源码阅读笔记
AsyncDisplayKit(一)初识
ASDK源码剖析

开源代码信息
YYText的使用

YYLabel特性介绍

YYLabel 是一个只读TextView,它除了具备UILabel所必备的特性外,还支持如下特性:

* 支持异步布局和渲染,避免阻塞UI线程
* 通过扩展CoreText属性来支持更多的文本特效
* 允许文本和UIImage, UIView 和 CALayer 进行图文混排
* 允许添加局部用户可交互的高亮链接
* 允许添加和移除container路径来控制文本container的形状
* 支持纵向布局显示文本

YYTextView特性介绍

和YYLabel不同的是YYTextView是一个可滚动的,可编辑(当然也可以设置为不可编辑)多行文本控件。

* 通过扩展CoreText属性来支持更多的文本特效
* 允许文本和UIImage, UIView 和 CALayer 进行图文混排
* 允许添加局部用户可交互的高亮链接
* 允许添加和移除container路径来控制文本container的形状
* 支持纵向布局显示文本
* 允许用户在text view之间复制图像和富文本
* 可以将属性文本作为占位文本

YYLabel/YYTextView 常用属性

/**
显示在label上的文本,它和attributedText是互斥的,一旦设置了attributedText 就会覆盖 text
*/
@property (nullable, nonatomic, copy) NSString *text;

/**
text的文本字体,也会影响到attributedText
*/
@property (null_resettable, nonatomic, strong) UIFont *font;

/**
text的颜色,默认是黑色,这个值的设置也会影响到attributedText
*/
@property (null_resettable, nonatomic, strong) UIColor *textColor;

/**
text文本阴影的颜色,这个值的设置也会影响到attributedText
*/
@property (nullable, nonatomic, strong) UIColor *shadowColor;

/**
阴影的偏移,默认是CGSizeZero,这个值的设置也会影响到attributedText
*/
@property (nonatomic) CGSize shadowOffset;

/**
text 阴影模糊半径,默认为0,这个值的设置也会影响到attributedText
*/
@property (nonatomic) CGFloat shadowBlurRadius;

/**
text的对齐方式
*/
@property (nonatomic) NSTextAlignment textAlignment;

/**
垂直对齐方式
*/
@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;

/**
属性文本内容
*/
@property (nullable, nonatomic, copy) NSAttributedString *attributedText;

/**
断行模式
*/
@property (nonatomic) NSLineBreakMode lineBreakMode;

/**
在文本被截断的时候,作为截断文本的字符串,默认为“...”
*/
@property (nullable, nonatomic, copy) NSAttributedString *truncationToken;

/**
最大文本行数
*/
@property (nonatomic) NSUInteger numberOfLines;

/**
多行文本的推荐宽度,如果文本的宽度超过这个设定值,文本内容将会显示到新的一行或者多行,所以会增加整个文本的高度。
*/
@property (nonatomic) CGFloat preferredMaxLayoutWidth;


/**
可以点击的修饰样式

UIDataDetectorTypePhoneNumber 手机号码
UIDataDetectorTypeLink URL链接
UIDataDetectorTypeAddress 地址
UIDataDetectorTypeCalendarEvent 日历事件
UIDataDetectorTypeShipmentTrackingNumber 船舶跟踪号码
UIDataDetectorTypeFlightNumber 航空号码
UIDataDetectorTypeLookupSuggestion 用户查询建议
UIDataDetectorTypeNone 无修饰样式
UIDataDetectorTypeAll 全部样式
*/
@property (nonatomic) UIDataDetectorTypes dataDetectorTypes;

/**
* 用于修改dataDetectorTypes的样式
*/
@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *linkTextAttributes;


/**
* 高亮文本样式
*/
@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *highlightTextAttributes;

/**
* 用户新输入的文本属性
*/
@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *typingAttributes;

/**
占位文本
*/
@property (nullable, nonatomic, copy) NSString *placeholderText;

/**
占位文本字体
*/
@property (nullable, nonatomic, strong) UIFont *placeholderFont;

/**
占位文本颜色
*/
@property (nullable, nonatomic, strong) UIColor *placeholderTextColor;

/**
占位文本富文本属性
*/
@property (nullable, nonatomic, copy) NSAttributedString *placeholderAttributedText;


/**
当前选择的Range
*/
@property (nonatomic) NSRange selectedRange;

/**
* 插入的文本是否替换旧有的内容
*/
@property (nonatomic) BOOL clearsOnInsertion;

/**
能否选择文本,编辑文本
*/
@property (nonatomic, getter=isSelectable) BOOL selectable;

/**
是否可以高亮文本
*/
@property (nonatomic, getter=isHighlightable) BOOL highlightable;

/**
是否可编辑
*/
@property (nonatomic, getter=isEditable) BOOL editable;

/**
是否允许粘贴图片
*/
@property (nonatomic) BOOL allowsPasteImage;

/**
是否允许粘贴富文本
*/
@property (nonatomic) BOOL allowsPasteAttributedString;

/**
是否允许复制富文本
*/
@property (nonatomic) BOOL allowsCopyAttributedString;

/**
是否允许重做和撤销
*/
@property (nonatomic) BOOL allowsUndoAndRedo;

/**
撤销步骤数
*/
@property (nonatomic) NSUInteger maximumUndoLevel;


/**
* 在textview获得焦点的时候取代系统键盘的自定义输入view
*/
@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputView;

/**
*
* 在textview获得焦点的时候显示在键盘上方的附加view
*/
@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputAccessoryView;

/**
* 如果不使用inputAccessoryView属性指定自定义附加view,可以使用它来指定附加view的高度
*/
@property (nonatomic) CGFloat extraAccessoryViewHeight;

一些交互事件

/**
点击文本的时候响应Block
*/
@property (nullable, nonatomic, copy) YYTextAction textTapAction;
/**
长按文本的时候响应的Block
*/
@property (nullable, nonatomic, copy) YYTextAction textLongPressAction;
/**
* 点击高亮文本的时候执行的动作
*/
@property (nullable, nonatomic, copy) YYTextAction highlightTapAction;
/**
* 长按高亮文本时候执行的动作
*/
@property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction;

/**
文本内间距
*/
@property (nonatomic) UIEdgeInsets textContainerInset;

高级用法

/**
用于控制布局和渲染代码是否运行在后台线程,默认是不运行在后台线程
*/
@property (nonatomic) BOOL displaysAsynchronously;

/**
当异步显示打开的情况下,在后台渲染进程结束后,layer的内容将会更新,但是如果渲染进程在一个垂直同步时间内
不能完成渲染工作,将会保持显示显示旧的内容。我们可以在更新label属性后通过手动将layer.contents设置为nil,或者将这个值设置为YES.

*/
@property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay;

/**
在文本内容发生改变的时候会有一个动画
*/
@property (nonatomic) BOOL fadeOnAsynchronouslyDisplay;

/**
在某些文本高亮的时候呈现一个动画
*/
@property (nonatomic) BOOL fadeOnHighlight;

/**
忽略text, font, textColor, attributedText...这些通用的属性,只使用textLayout来显示文本内容
*/
@property (nonatomic) BOOL ignoreCommonProperties;

YYText中给定的建议如下:

1. If you only need a UILabel alternative to display rich text and receive link touch event, 
you do not need to adjust the display mode properties.

如果你只想要作为UILabel来显示富文本或者接受链接点击事件,我们不需要设置上面的显示模式属性。

2. If you have performance issues, you may enable the asynchronous display mode
by setting the `displaysAsynchronously` to YES.

如果我们遇到了性能问题需要提高文本的显示性能,那么可以将displaysAsynchronously设置为YES使用异步显示模式

3. If you want to get the highest performance, you should do text layout with
`YYTextLayout` class in background thread. Here's an example:

如果我们想获得最高的性能,我们需要在后台使用YYTextLayout来做布局

异步绘制会在后面给出例子:

1. 属性文本

属性文本样式设置大家可以查看NSAttributedString+YYText.h文件,我们用得比较多的是NSMutableAttributedString,下面只列出用得比较多的,大家使用的时候可以查看NSAttributedString+YYText.h:

基本文本属性:

/**
设置全部文本的样式
*/
- (void)yy_setAttribute:(NSString *)name value:(nullable id)value;

/**
设置指定范围的文本样式
*/
- (void)yy_setAttribute:(NSString *)name value:(nullable id)value range:(NSRange)range;

/**
The font of the text.
*/
@property (nullable, nonatomic, strong, readwrite) UIFont *yy_font;
- (void)yy_setFont:(nullable UIFont *)font range:(NSRange)range;

/**
The foreground color.
*/
@property (nullable, nonatomic, strong, readwrite) UIColor *yy_color;
- (void)yy_setColor:(nullable UIColor *)color range:(NSRange)range;

/**
The background color.
*/
@property (nullable, nonatomic, strong, readwrite) UIColor *yy_backgroundColor;
- (void)yy_setBackgroundColor:(nullable UIColor *)backgroundColor range:(NSRange)range;

/**
The stroke width.
*/
@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_strokeWidth;
- (void)yy_setStrokeWidth:(nullable NSNumber *)strokeWidth range:(NSRange)range;

/**
The stroke color.
*/
@property (nullable, nonatomic, strong, readwrite) UIColor *yy_strokeColor;
- (void)yy_setStrokeColor:(nullable UIColor *)strokeColor range:(NSRange)range;

/**
The text shadow.
*/
@property (nullable, nonatomic, strong, readwrite) NSShadow *yy_shadow;
- (void)yy_setShadow:(nullable NSShadow *)shadow range:(NSRange)range;

/**
The strikethrough style.
*/
@property (nonatomic, readwrite) NSUnderlineStyle yy_strikethroughStyle;
- (void)yy_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range;

/**
The strikethrough color.
*/
@property (nullable, nonatomic, strong, readwrite) UIColor *yy_strikethroughColor;
- (void)yy_setStrikethroughColor:(nullable UIColor *)strikethroughColor range:(NSRange)range NS_AVAILABLE_IOS(7_0);

/**
The underline style.
*/
@property (nonatomic, readwrite) NSUnderlineStyle yy_underlineStyle;
- (void)yy_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range;

/**
The underline color.
*/
@property (nullable, nonatomic, strong, readwrite) UIColor *yy_underlineColor;
- (void)yy_setUnderlineColor:(nullable UIColor *)underlineColor range:(NSRange)range;

段落属性


/**
An NSParagraphStyle object which is used to specify things like
line alignment, tab rulers, writing direction, etc.
*/
@property (nullable, nonatomic, strong, readwrite) NSParagraphStyle *yy_paragraphStyle;
- (void)yy_setParagraphStyle:(nullable NSParagraphStyle *)paragraphStyle range:(NSRange)range;

/**
* 文本对齐方式
*/
@property (nonatomic, readwrite) NSTextAlignment yy_alignment;
- (void)yy_setAlignment:(NSTextAlignment)alignment range:(NSRange)range;

/**
* 断行模式
*/
@property (nonatomic, readwrite) NSLineBreakMode yy_lineBreakMode;
- (void)yy_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range;

/**
行间距
*/
@property (nonatomic, readwrite) CGFloat yy_lineSpacing;
- (void)yy_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range;

/**
段落间距
*/
@property (nonatomic, readwrite) CGFloat yy_paragraphSpacing;
- (void)yy_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range;

/**
段落前间距
*/
@property (nonatomic, readwrite) CGFloat yy_paragraphSpacingBefore;
- (void)yy_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range;

/**
首行间距
*/
@property (nonatomic, readwrite) CGFloat yy_firstLineHeadIndent;
- (void)yy_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range;

/**
最小高度
*/
@property (nonatomic, readwrite) CGFloat yy_minimumLineHeight;
- (void)yy_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range;

/**
最大高度
*/
@property (nonatomic, readwrite) CGFloat yy_maximumLineHeight;
- (void)yy_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range;

简单的一个例子:

 //1.创建一个属性文本
NSMutableAttributedString *attributeStr = [[NSMutableAttributedString alloc] initWithString:@"测试文本,测试文本测试文本测试文本测试文本测试文本测试文本测试文本"];

//2.为文本设置属性
attributeStr.yy_font = [UIFont boldSystemFontOfSize:18.0];
[attributeStr yy_setColor:[UIColor blueColor] range:NSMakeRange(0, 50)];
attributeStr.yy_lineSpacing = 20.0;

//3.赋值给YYLabel
YYLabel *yyLabel = [[YYLabel alloc] init];
yyLabel.attributedText = attributeStr;
yyLabel.numberOfLines = 0;
yyLabel.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, 400.0);
[self.view addSubview:yyLabel];

2. 文本高亮

使用YYText还可以为某些文本指定高亮文本样式,默认的是浅蓝色的文本,如果是常见的比如电话号码之类的可以使用UIDataDetectorTypes来快速指定,但是如果是自定义的则可以有两种方式可以指定高亮文本:
一种是通过yy_setTextHighlightRange来指定,但是这种方式能设置的样式比较有限,只能设置前景色和背景色,但是对于一般的需求是足够用了:

/**
Convenience method to set text highlight
@param range text range
@param color text color (pass nil to ignore)
@param backgroundColor text background color when highlight
@param userInfo user information dictionary (pass nil to ignore)
@param tapAction tap action when user tap the highlight (pass nil to ignore)
@param longPressAction long press action when user long press the highlight (pass nil to ignore)
*/
- (void)yy_setTextHighlightRange:(NSRange)range
color:(nullable UIColor *)color
backgroundColor:(nullable UIColor *)backgroundColor
userInfo:(nullable NSDictionary *)userInfo
tapAction:(nullable YYTextAction)tapAction
longPressAction:(nullable YYTextAction)longPressAction;

另一种是借助YYTextHighlight,下面是YYTextHighlight所能设置的属性:

+ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color;
- (void)setFont:(nullable UIFont *)font;
- (void)setColor:(nullable UIColor *)color;
- (void)setStrokeWidth:(nullable NSNumber *)width;
- (void)setStrokeColor:(nullable UIColor *)color;
- (void)setShadow:(nullable YYTextShadow *)shadow;
- (void)setInnerShadow:(nullable YYTextShadow *)shadow;
- (void)setUnderline:(nullable YYTextDecoration *)underline;
- (void)setStrikethrough:(nullable YYTextDecoration *)strikethrough;
- (void)setBackgroundBorder:(nullable YYTextBorder *)border;
- (void)setBorder:(nullable YYTextBorder *)border;
- (void)setAttachment:(nullable YYTextAttachment *)attachment;
@property (nullable, nonatomic, copy) YYTextAction tapAction;
@property (nullable, nonatomic, copy) YYTextAction longPressAction;

这里涉及到了YYTextBorder,它其实就是一个文本的外框,可以给选中的文本添加外框样式:

@interface YYTextBorder : NSObject <NSCoding, NSCopying>
+ (instancetype)borderWithLineStyle:(YYTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color;
+ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius;
@property (nonatomic) YYTextLineStyle lineStyle; ///< border line style
@property (nonatomic) CGFloat strokeWidth; ///< border line width
@property (nullable, nonatomic, strong) UIColor *strokeColor; ///< border line color
@property (nonatomic) CGLineJoin lineJoin; ///< border line join
@property (nonatomic) UIEdgeInsets insets; ///< border insets for text bounds
@property (nonatomic) CGFloat cornerRadius; ///< border corder radius
@property (nullable, nonatomic, strong) YYTextShadow *shadow; ///< border shadow
@property (nullable, nonatomic, strong) UIColor *fillColor; ///< inner fill color
@end

简单的例子:

//1.创建一个属性文本
NSMutableAttributedString *attributeStr = [[NSMutableAttributedString alloc] initWithString:@"旧时月色,算几番照我,梅边吹笛?唤起玉人,不管清寒与攀摘.何逊而今渐老,都忘却,春风词笔.但怪得,竹外疏花,春冷入瑶席.江国,正寂寂.叹寄与路遥,夜雪初积.翠尊易泣,红萼无言耿相忆.常记曾携手处,千树压.梅湖寒碧,又片片吹尽也,何时得见? "];

//2.为文本设置属性
attributeStr.yy_font = [UIFont boldSystemFontOfSize:18.0];
attributeStr.yy_lineSpacing = 20.0;

//3.创建高亮属性
YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor magentaColor] cornerRadius:4];

YYTextHighlight *highlight = [[YYTextHighlight alloc] init];
[highlight setColor:[UIColor blueColor]];
[highlight setBackgroundBorder:border];
highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {

};

//4.设置文本的高亮属性
[attributeStr yy_setTextHighlight:highlight range:NSMakeRange(0, 150)];

//5.赋值给YYLabel
YYLabel *yyLabel = [[YYLabel alloc] init];
yyLabel.attributedText = attributeStr;
yyLabel.numberOfLines = 0;
yyLabel.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, 400.0);
[self.view addSubview:yyLabel];

3. 图文混排

YYText对我个人来说最大的用途除了高亮文本外就应该属图文混排了。YYText支持将UIView,CALayer,UIImage作为富文本添加到其他富文本中。用得比较多的是如下的接口:

/**
Creates and returns an attachment.
Example: ContentMode:bottom Alignment:Top.
The text The attachment holder
↓ ↓
─────────┌──────────────────────┐───────
/ \ │ │ / ___|
/ _ \ │ │| |
/ ___ \ │ │| |___ ←── The text line
/_/ \_\│ ██████████████ │ \____|
─────────│ ██████████████ │───────
│ ██████████████ │
│ ██████████████ ←───────────────── The attachment content
│ ██████████████ │
└──────────────────────┘

@param content The attachment (UIImage/UIView/CALayer).
@param contentMode The attachment's content mode in attachment holder
@param attachmentSize The attachment holder's size in text layout.
@param fontSize The attachment will align to this font.
@param alignment The attachment holder's alignment to text line.

@return An attributed string, or nil if an error occurs.
@since YYText:6.0
*/
+ (NSMutableAttributedString *)yy_attachmentStringWithContent:(nullable id)content
contentMode:(UIViewContentMode)contentMode
attachmentSize:(CGSize)attachmentSize
alignToFont:(UIFont *)font
alignment:(YYTextVerticalAlignment)alignment;

下面是一个简单例子:

NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"图片前面的文本"];
UIFont *font = [UIFont systemFontOfSize:18];
// 嵌入 UIImage
UIImage *image = [UIImage imageNamed:@"image"];
NSMutableAttributedString *attachment = nil;
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];

NSMutableAttributedString *text1 = [[NSMutableAttributedString alloc] initWithString:@"图片后面的文本"];
[text appendAttributedString: text1];

YYLabel *yyLabel = [[YYLabel alloc] init];
yyLabel.attributedText = text;
yyLabel.numberOfLines = 0;
yyLabel.textColor = [UIColor blueColor];
yyLabel.font = [UIFont boldSystemFontOfSize:20.0];
yyLabel.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, 400.0);
[self.view addSubview:yyLabel];

4. 文本容器


/**
The YYTextContainer class defines a region in which text is laid out.
YYTextLayout class uses one or more YYTextContainer objects to generate layouts.

A YYTextContainer defines rectangular regions (`size` and `insets`) or
nonrectangular shapes (`path`), and you can define exclusion paths inside the
text container's bounding rectangle so that text flows around the exclusion
path as it is laid out.

All methods in this class is thread-safe.

Example:

┌─────────────────────────────┐ <------- container
│ │
│ asdfasdfasdfasdfasdfa <------------ container insets
│ asdfasdfa asdfasdfa │
│ asdfas asdasd │
│ asdfa <----------------------- container exclusion path
│ asdfas adfasd │
│ asdfasdfa asdfasdfa │
│ asdfasdfasdfasdfasdfa │
│ │
└─────────────────────────────┘
*/

我们的布局都是基于文本容器的,YYTextContainer可以用size和inset指定规则的文本容器,可以使用path来指定不规则的文本容器,还可以通过exclusion在已有的容器中挖去一个形状。
我们用得比较多的是规则的矩形:

可以通过下面来获得对应的YYTextContainer:

/// Creates a container with the specified size and insets. @param size The size. @param insets The text insets.
+ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets;

/// Creates a container with the specified path. @param size The path.
+ (instancetype)containerWithPath:(nullable UIBezierPath *)path;

5. 文本布局计算

YYTextLayout是负责YYText的布局的类,它可以基于上面介绍的YYTextContainer还可以基于CGSize进行布局,除了少量绘制方法外大多数的属性是只读的线程安全的。

/**
YYTextLayout class is a readonly class stores text layout result.
All the property in this class is readonly, and should not be changed.
The methods in this class is thread-safe (except some of the draw methods).
example: (layout with a circle exclusion path)

┌──────────────────────────┐ <------ container
│ [--------Line0--------] │ <- Row0
│ [--------Line1--------] │ <- Row1
│ [-Line2-] [-Line3-] │ <- Row2
│ [-Line4] [Line5-] │ <- Row3
│ [-Line6-] [-Line7-] │ <- Row4
│ [--------Line8--------] │ <- Row5
│ [--------Line9--------] │ <- Row6
└──────────────────────────┘
*/

下面是最常用的两个布局方法:

/**
Generate a layout with the given container size and text.

@param size The text container's size
@param text The text (if nil, returns nil).
@return A new layout, or nil when an error occurs.
*/
+ (nullable YYTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text;

/**
Generate a layout with the given container and text.

@param container The text container (if nil, returns nil).
@param text The text (if nil, returns nil).
@return A new layout, or nil when an error occurs.
*/
+ (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text;

第一个基于规定尺寸的,第二个是基于文本容器的。

布局后用得比较多的属性如下:

///< Bounding rect (glyphs)
@property (nonatomic, readonly) CGRect textBoundingRect;
///< Bounding size (glyphs and insets, ceil to pixel)
@property (nonatomic, readonly) CGSize textBoundingSize;
///< Number of rows
@property (nonatomic, readonly) NSUInteger rowCount;

第一个是布局后文本的位置,第二个是文本的尺寸,最后一个是行数。下面是一个简单的用法:

CGSize size = CGSizeMake(300, CGFLOAT_MAX);
YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text];
YYLabel *label = [[YYLabel alloc] init];
label.textColor = [UIColor blueColor];
label.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, 400.0);
label.attributedText= text;
label.textLayout = layout;
[self.view addSubview:label];

6. 文本行位置调整

由于中文、英文、Emoji 高度不一致,或者富文本中出现了不同字号的字体;可能会造成每行文字的高度不一致。可以添加一个修改器来解决这个问题:


UILabel *label = [[UILabel alloc] init];
label.text = @"fsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdffsfdsfsdfsdf我们women!!!☆★☆★☆★(=@__@=) O(∩_∩)O~我们women!!!我们women!!!我们women!!! (*^__^*)☆★☆★☆★我们women!!!我们women!!!我们women!!!我们women!!!我们women!!!";
label.numberOfLines = 0;
label.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, 400.0);
label.textColor = [UIColor blueColor];
label.font = [UIFont boldSystemFontOfSize:20.0];
[self.view addSubview:label];

7. 异步排版和渲染

下面是官方给出的一个例子:

YYLabel *label = [YYLabel new];
label.displaysAsynchronously = YES;
label.ignoreCommonProperties = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Create attributed string.
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"];
text.yy_font = [UIFont systemFontOfSize:16];
text.yy_color = [UIColor grayColor];
[text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
// Create text container
YYTextContainer *container = [YYTextContainer new];
container.size = CGSizeMake(100, CGFLOAT_MAX);
container.maximumNumberOfRows = 0;
// Generate a text layout.
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
dispatch_async(dispatch_get_main_queue(), ^{
label.size = layout.textBoundingSize;
label.textLayout = layout;
});
});

8. 文本解析

YYTextParser用于text的解析,YYText默认内置了YYTextSimpleMarkdownParserYYTextSimpleEmoticonParser两种文本解析器。分别用于解析Markdown和Emoj表情。#pragma endregion
如果想要自定义文本解析器则只要遵循YYTextParser协议,并覆写如下方法:

/**
当文本改变的时候会调用这个方法
@param text 最初的富文本,这个方法会解析这个文本,并且修改它的内容
@param selectedRange 当前选择的文本内容
如果文本修改了就返回YES,如果没有修改则返回NO
*/
- (BOOL)parseText:(nullable NSMutableAttributedString *)text selectedRange:(nullable NSRangePointer)selectedRange;
// 内置简单的表情解析
YYTextSimpleEmoticonParser *parser = [YYTextSimpleEmoticonParser new];
NSMutableDictionary *mapper = [NSMutableDictionary new];
mapper[@":smile:"] = [UIImage imageNamed:@"smile.png"];
mapper[@":cool:"] = [UIImage imageNamed:@"cool.png"];
mapper[@":cry:"] = [UIImage imageNamed:@"cry.png"];
mapper[@":wink:"] = [UIImage imageNamed:@"wink.png"];
parser.emoticonMapper = mapper;

// 内置简单的 markdown 解析
YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new];
[parser setColorWithDarkTheme];

// 实现 `YYTextParser` 协议的自定义解析器
MyCustomParser *parser = ...

// 2. 把解析器添加到 YYLabelYYTextView
YYLabel *label = ...
label.textParser = parser;

YYTextView *textView = ...
textView.textParser = parser;

9. YYTextBinding

YYTextBinding 用于将某些文本绑定在一起,在文本选择或者编辑的时候看作一个字符整体对待。

UIFont *font = [UIFont boldSystemFontOfSize:16];
for (int i = 0; i < tags.count; i++) {
NSString *tag = tags[i];
NSMutableAttributedString *tagText = [[NSMutableAttributedString alloc] initWithString:tag];
tagText.yy_font = font;
tagText.yy_color = [UIColor whiteColor];
[tagText yy_setTextBinding:[YYTextBinding bindingWithDeleteConfirm:NO] range:tagText.yy_rangeOfAll];

YYTextBorder *border = [YYTextBorder new];
border.strokeWidth = 1.5;
border.strokeColor = tagStrokeColors[i];
border.fillColor = tagFillColors[i];
border.cornerRadius = 100;
border.lineJoin = kCGLineJoinBevel;
border.insets = UIEdgeInsetsMake(-2, -5.5, -2, -8);

[tagText yy_setTextBackgroundBorder:border range:[tagText.string rangeOfString:tag]];
[text appendAttributedString:tagText];
}
text.yy_lineSpacing = 10;
text.yy_lineBreakMode = NSLineBreakByWordWrapping;
[text yy_appendString:@"\n"];
[text appendAttributedString:text];
_textView.attributedText = text;

10. YYTextShadow

YYTextShadow 是给富文本添加阴影用的:

添加外阴影:

NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Shadow"];
one.yy_font = [UIFont boldSystemFontOfSize:30];
one.yy_color = [UIColor whiteColor];
YYTextShadow *shadow = [YYTextShadow new];
shadow.color = [UIColor colorWithWhite:0.000 alpha:0.490];
shadow.offset = CGSizeMake(0, 1);
shadow.radius = 5;
one.yy_textShadow = shadow;

添加内阴影:

NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Inner Shadow"];
one.yy_font = [UIFont boldSystemFontOfSize:30];
one.yy_color = [UIColor whiteColor];
YYTextShadow *shadow = [YYTextShadow new];
shadow.color = [UIColor colorWithWhite:0.000 alpha:0.40];
shadow.offset = CGSizeMake(0, 1);
shadow.radius = 1;
one.yy_textInnerShadow = shadow;

添加子阴影:

NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Multiple Shadows"];
one.yy_font = [UIFont boldSystemFontOfSize:30];
one.yy_color = [UIColor colorWithRed:1.000 green:0.795 blue:0.014 alpha:1.000];

YYTextShadow *shadow = [YYTextShadow new];
shadow.color = [UIColor colorWithWhite:0.000 alpha:0.20];
shadow.offset = CGSizeMake(0, -1);
shadow.radius = 1.5;
YYTextShadow *subShadow = [YYTextShadow new];
subShadow.color = [UIColor colorWithWhite:1 alpha:0.99];
subShadow.offset = CGSizeMake(0, 1);
subShadow.radius = 1.5;
shadow.subShadow = subShadow;
one.yy_textShadow = shadow;

给高亮文本添加内外阴影:

NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Yet Another Link"];
one.yy_font = [UIFont boldSystemFontOfSize:30];
one.yy_color = [UIColor whiteColor];

YYTextShadow *shadow = [YYTextShadow new];
shadow.color = [UIColor colorWithWhite:0.000 alpha:0.490];
shadow.offset = CGSizeMake(0, 1);
shadow.radius = 5;
one.yy_textShadow = shadow;

YYTextShadow *shadow0 = [YYTextShadow new];
shadow0.color = [UIColor colorWithWhite:0.000 alpha:0.20];
shadow0.offset = CGSizeMake(0, -1);
shadow0.radius = 1.5;
YYTextShadow *shadow1 = [YYTextShadow new];
shadow1.color = [UIColor colorWithWhite:1 alpha:0.99];
shadow1.offset = CGSizeMake(0, 1);
shadow1.radius = 1.5;
shadow0.subShadow = shadow1;

YYTextShadow *innerShadow0 = [YYTextShadow new];
innerShadow0.color = [UIColor colorWithRed:0.851 green:0.311 blue:0.000 alpha:0.780];
innerShadow0.offset = CGSizeMake(0, 1);
innerShadow0.radius = 1;

YYTextHighlight *highlight = [YYTextHighlight new];
[highlight setColor:[UIColor colorWithRed:1.000 green:0.795 blue:0.014 alpha:1.000]];
[highlight setShadow:shadow0];
[highlight setInnerShadow:innerShadow0];
[one yy_setTextHighlight:highlight range:one.yy_rangeOfAll];

11. YYTextBorder

YYTextBorder用于为文本添加边框或者背景

YYTextBorder *border = [YYTextBorder new];
border.strokeColor = [UIColor colorWithRed:1.000 green:0.029 blue:0.651 alpha:1.000];
border.strokeWidth = 3;
border.lineStyle = YYTextLineStylePatternCircleDot;
border.cornerRadius = 3;
border.insets = UIEdgeInsetsMake(0, -4, 0, -4);
one.yy_textBackgroundBorder = border;

作为高亮文本的填充:

YYTextBorder *border = [YYTextBorder new];
border.cornerRadius = 50;
border.insets = UIEdgeInsetsMake(0, -10, 0, -10);
border.strokeWidth = 0.5;
border.strokeColor = one.yy_color;
border.lineStyle = YYTextLineStyleSingle;
one.yy_textBackgroundBorder = border;

YYTextBorder *highlightBorder = border.copy;
highlightBorder.strokeWidth = 0;
highlightBorder.strokeColor = one.yy_color;
highlightBorder.fillColor = one.yy_color;

YYTextHighlight *highlight = [YYTextHighlight new];
[highlight setColor:[UIColor whiteColor]];
[highlight setBackgroundBorder:highlightBorder];
highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
[_self showMessage:[NSString stringWithFormat:@"Tap: %@",[text.string substringWithRange:range]]];
};
[one yy_setTextHighlight:highlight range:one.yy_rangeOfAll];

12. Debug

YYTextDebugOption *debugOptions = [YYTextDebugOption new];
[YYTextDebugOption setSharedDebugOption:debugOptions];

下面是YYTextDebugOption的配置

@property (nullable, nonatomic, strong) UIColor *baselineColor;      ///< baseline color
@property (nullable, nonatomic, strong) UIColor *CTFrameBorderColor; ///< CTFrame path border color
@property (nullable, nonatomic, strong) UIColor *CTFrameFillColor; ///< CTFrame path fill color
@property (nullable, nonatomic, strong) UIColor *CTLineBorderColor; ///< CTLine bounds border color
@property (nullable, nonatomic, strong) UIColor *CTLineFillColor; ///< CTLine bounds fill color
@property (nullable, nonatomic, strong) UIColor *CTLineNumberColor; ///< CTLine line number color
@property (nullable, nonatomic, strong) UIColor *CTRunBorderColor; ///< CTRun bounds border color
@property (nullable, nonatomic, strong) UIColor *CTRunFillColor; ///< CTRun bounds fill color
@property (nullable, nonatomic, strong) UIColor *CTRunNumberColor; ///< CTRun number color
@property (nullable, nonatomic, strong) UIColor *CGGlyphBorderColor; ///< CGGlyph bounds border color
@property (nullable, nonatomic, strong) UIColor *CGGlyphFillColor; ///< CGGlyph bounds fill color

13. 其他

用YYTextWeakProxy避免被NSTimer或者CADisplayLink持有导致循环引用:

@implementation MyView {
NSTimer *_timer;
}

- (void)initTimer {
YYTextWeakProxy *proxy = [YYTextWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}

- (void)tick:(NSTimer *)timer {...}
@end

1. 有帮助的链接
2. 开始前的叨叨

项目中最早用的是Masonry,它用起来确实十分方便,但是它是基于AutoLayout的,所以到了项目中期替换替换成了给予frame的布局方式,那么为什么要在自己的练习项目中使用Yoga呢?理由有如下几方面:

  1. Yoga是跨平台的在Android,Reactive Native,iOS 都有对应的版本,这样方便自己后续使用Android,RN时,能够少学点,统一下技术栈(还是懒 ^V^)。
  2. Yoga是基于Flex布局的,Flutter,以及Web框架,小程序也都是采用这种布局方式,还是想一劳永逸,并且是基于frame的性能上面是可以接受的,何乐而不为。

这篇主要关注的是Flex 以及 YogaKit,大家如果想了解frame布局,AutoLayout布局原理,以及目前iOS比较主流的布局框架,我后面还会另起一个博客来介绍。

3. Flex布局概念
3.1 Flex 容器布局属性

盒子模型:

* position 在当前盒子中item 的定位
* margin item 的外边距。
* border item的边框。
* padding item内边距。
* width & height,当 box-sizing 为 content-box,指内部蓝色的区域。当 box-siziong 为 boder-box时,指含 border 以内的区域。Yoga默认是 border-box
* Flex direction

Flex 布局是有两类子对象:flex item 和 flex container。flex item 和 flex container组成一个布局层级树。还包括主轴和交叉轴的概念,要解释主轴和交叉轴的概念必须知道flex direction这个概念。

一般如果需要使用flex布局,那么需要使用display属性来开启:

display: flex

我们先来看下 flex direction及主轴交叉轴:
flex direction 就是布局方向,一般支持row,row reverse,column,clumn reverse. 也就是横轴,横轴逆向,纵轴,纵轴逆向。如果flex direction指定的是row,row reverse 那么主轴就是横轴,如果为column,clumn reverse那么主轴就是纵轴。

flex-direction: row / column / row-reverse / column-reverse




有了主轴交叉轴的概念后就可以进行后续的概念的介绍了:

* Justify-content

Justify-content 是flex item 沿着主轴方向上的布局方式:

flex-start:所有的flex item 沿着主轴布局的开始方向进行布局
flex-end:所有的flex item 从主轴结尾开始沿着主轴逆向布局
center: 所有flex item 居中布局
space-between:所有flex item 在容器内被空白空间均匀间隔开,第一个项目在开端位置,最后一个项目在末端位置。
space-around:所有flex item周围以同等空间均匀间隔,它和space-between的区别是开始和结束有空白区域,并且为中间区域的一半。






* Align-items

指定item在交叉轴的对齐方式


align-items: stretch / flex-start / flex-end / center / baseline

stretch(默认)每个项目进行拉伸,直到所有item大小占满父容器
flex-start 对齐交叉轴的起点
flex-end 对齐交叉轴的终点
center 交叉轴内居中
baseline 在一行中,所有item以首个item的文字排版为基线对齐,仅在 flex-direction: row / row-reverse 生效





* Flex-wrap

Flex-wrap指明了当可用排版空间不足时,是否允许换行,以及换行后的顺序。

flex-wrap: wrap(默认) / nowrap / wrap-reverse

wrap (默认)空间不足时,进行换行
nowrap 不换行
wrap-reverse 换行后,第一行在最下方,行排版方向向上



* Align-content

指定container中存在多行情况下,在交叉轴上的布局方式,这里需要注意的是与Align-items的区别,Align-items 是单行上各个元素的对齐方式,是站在item元素的角度,Align-content是多行的情况下,整个内容在交叉轴上的分布关系。是站在交叉轴整体布局角度。

align-content: stretch / flex-start / flex-end / center / space-between / space-around

stretch(默认)在交叉轴上的大小进行拉伸,铺满容器
flex-start 行向交叉轴起点对齐
flex-end 行向交叉轴终点对齐
center 行在交叉轴上居中
space-between 均匀排列每一行,第一行放置于起点,最后一行放置于终点
space-around 均匀排列每一行,每一行周围分配相同的空间






3.2 Flex item布局属性
* Align-self

align-self 属性可重写flex container 的 align-items 属性。

align-self: auto / stretch / flex-start / flex-end / center / base-line







* Order

指定项目的排列顺序,值越大排在越后面

order: (默认 0

* Flex-grow

指定item的放大比例,默认为0,即如果存在剩余空间,也不进行放大。

flex-grow: (默认 0

* Flex-shrink

指定item的缩小比例,默认为1,即在空间不足(仅当不换行时候起效),所有item等比缩小,当设置为0,该item不进行缩小。

flex-shrink: number (默认 1

* Flex-basis

指定item的主轴的初始大小,auto 的含义是参考 width 或 height 的大小,

flex-basis: number / auto(默认 auto

width * height && max-width * max-height && min-width * min-height

item 的尺寸参数,最大尺寸,最小尺寸

Aspect Ratio

item 的宽高比

4. YogaKit 使用

YogaKit 有如下属性,有了上面的讲解估计都比较熟悉了

@property (nonatomic, readwrite, assign) YGDirection direction;
@property (nonatomic, readwrite, assign) YGFlexDirection flexDirection;
@property (nonatomic, readwrite, assign) YGJustify justifyContent;
@property (nonatomic, readwrite, assign) YGAlign alignContent;
@property (nonatomic, readwrite, assign) YGAlign alignItems;
@property (nonatomic, readwrite, assign) YGAlign alignSelf;
@property (nonatomic, readwrite, assign) YGPositionType position;
@property (nonatomic, readwrite, assign) YGWrap flexWrap;
@property (nonatomic, readwrite, assign) YGOverflow overflow;
@property (nonatomic, readwrite, assign) YGDisplay display;

@property (nonatomic, readwrite, assign) CGFloat flex;
@property (nonatomic, readwrite, assign) CGFloat flexGrow;
@property (nonatomic, readwrite, assign) CGFloat flexShrink;
@property (nonatomic, readwrite, assign) YGValue flexBasis;

@property (nonatomic, readwrite, assign) YGValue left;
@property (nonatomic, readwrite, assign) YGValue top;
@property (nonatomic, readwrite, assign) YGValue right;
@property (nonatomic, readwrite, assign) YGValue bottom;
@property (nonatomic, readwrite, assign) YGValue start;
@property (nonatomic, readwrite, assign) YGValue end;

@property (nonatomic, readwrite, assign) YGValue marginLeft;
@property (nonatomic, readwrite, assign) YGValue marginTop;
@property (nonatomic, readwrite, assign) YGValue marginRight;
@property (nonatomic, readwrite, assign) YGValue marginBottom;
@property (nonatomic, readwrite, assign) YGValue marginStart;
@property (nonatomic, readwrite, assign) YGValue marginEnd;
@property (nonatomic, readwrite, assign) YGValue marginHorizontal;
@property (nonatomic, readwrite, assign) YGValue marginVertical;
@property (nonatomic, readwrite, assign) YGValue margin;

@property (nonatomic, readwrite, assign) YGValue paddingLeft;
@property (nonatomic, readwrite, assign) YGValue paddingTop;
@property (nonatomic, readwrite, assign) YGValue paddingRight;
@property (nonatomic, readwrite, assign) YGValue paddingBottom;
@property (nonatomic, readwrite, assign) YGValue paddingStart;
@property (nonatomic, readwrite, assign) YGValue paddingEnd;
@property (nonatomic, readwrite, assign) YGValue paddingHorizontal;
@property (nonatomic, readwrite, assign) YGValue paddingVertical;
@property (nonatomic, readwrite, assign) YGValue padding;

@property (nonatomic, readwrite, assign) CGFloat borderLeftWidth;
@property (nonatomic, readwrite, assign) CGFloat borderTopWidth;
@property (nonatomic, readwrite, assign) CGFloat borderRightWidth;
@property (nonatomic, readwrite, assign) CGFloat borderBottomWidth;
@property (nonatomic, readwrite, assign) CGFloat borderStartWidth;
@property (nonatomic, readwrite, assign) CGFloat borderEndWidth;
@property (nonatomic, readwrite, assign) CGFloat borderWidth;

@property (nonatomic, readwrite, assign) YGValue width;
@property (nonatomic, readwrite, assign) YGValue height;
@property (nonatomic, readwrite, assign) YGValue minWidth;
@property (nonatomic, readwrite, assign) YGValue minHeight;
@property (nonatomic, readwrite, assign) YGValue maxWidth;
@property (nonatomic, readwrite, assign) YGValue maxHeight;
//只要 width 或者 height 确定,就能确定另外一个变量。
@property (nonatomic, readwrite, assign) CGFloat aspectRatio;

我们看下,一个布局实例,整个步骤分成如下4步:

1. 设置view的layout
configureLayoutWithBlock:(YGLayoutConfigurationBlock)block
2. 将layout应用到view
applyLayoutPreservingOrigin:(BOOL)preserveOrigin
3. 计算布局
calculateLayoutWithSize
4. 将布局应用到view层级上
YGApplyLayoutToViewHierarchy

其中步骤1.和步骤2是我们来完成的,3,4是yoga完成的。

-(void)configSubViewsLayout {

[self.view configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = YES;
layout.width = YGPointValue(self.view.bounds.size.width);
layout.height = YGPointValue(self.view.bounds.size.height);
layout.alignItems = YGAlignCenter;
}];

UIView *contentView = [[UIView alloc]init];
contentView.backgroundColor = [UIColor lightGrayColor];
[contentView configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = true;
layout.flexDirection = YGFlexDirectionRow;
layout.width = YGPointValue(320);
layout.height = YGPointValue(80);
layout.marginTop = YGPointValue(100);
layout.padding = YGPointValue(10);
}];

UIView *child1 = [[UIView alloc]init];
child1.backgroundColor = [UIColor redColor];
[child1 configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = YES;
layout.width = YGPointValue(80);
layout.marginRight = YGPointValue(10);
}];

UIView *child2 = [[UIView alloc]init];
child2.backgroundColor = [UIColor blueColor];
[child2 configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = YES;
layout.width = YGPointValue(80);
layout.flexGrow = 1;
layout.height = YGPointValue(20);
layout.alignSelf = YGAlignCenter;
}];

[contentView addSubview:child1];
[contentView addSubview:child2];
[self.view addSubview:contentView];
}

- (void)layoutSubViews {
[super layoutSubViews];
[self.view.yoga applyLayoutPreservingOrigin:YES];
}

是不是很像Masonry? 下一节我们将通过源码分析来看看它具体的工作原理。

5. YogaKit 源码解析

当我们使用view.yoga的时候,YOGA会通过关联属性来为view添加一个YGLayout的属性,这个属性中存放的是yoga的布局约束

- (YGLayout *)yoga {
YGLayout *yoga = objc_getAssociatedObject(self, kYGYogaAssociatedKey);
if (!yoga) {
yoga = [[YGLayout alloc] initWithView:self];
objc_setAssociatedObject(self, kYGYogaAssociatedKey, yoga, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return yoga;
}

调用 configureLayoutWithBlock的时候,我们会将为当前view添加的yoga关联属性引用传出去,在bock中我们为它设置布局约束。

- (void)configureLayoutWithBlock:(YGLayoutConfigurationBlock)block {
if (block != nil) {
block(self.yoga);
}
}

我们来看下YGLayout,,它有两个开关属性,isIncludedInLayout,isEnabled一般我们都是需要将其打开的。

/**
进行一次布局运算并更新view的frame。如果不保留原点,根视图的布局结果将会从{0,0}开始
*/
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin;

- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin
dimensionFlexibility:(YGDimensionFlexibility)dimensionFlexibility;
/**
在没有约束条件的情况下返回view的固有尺寸,这相当于调用了[self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
*/
@property (nonatomic, readonly, assign) CGSize intrinsicSize;

/**
根据给定的约束条件,返回view的尺寸
*/
- (CGSize)calculateLayoutWithSize:(CGSize)size;

/**
返回在使用Flex的子view数量
*/
@property (nonatomic, readonly, assign) NSUInteger numberOfChildren;

/**
当前视图是否有子视图
*/
@property (nonatomic, readonly, assign) BOOL isLeaf;

/**
将当前视图是否为脏视图,这些视图将会在下一次布局中进行重新布局
*/
@property (nonatomic, readonly, assign) BOOL isDirty;

/**
将当前视图标记为脏视图
*/
- (void)markDirty;

上面只是通过block将通过关联属性添加的ASLayout类型的yoga属性传递出去,让我们在block里面设置,但是这些属性还没有应用到view上面,所以必须调用applyLayoutPreservingOrigin方法应用这些设置,我们接下来看下applyLayoutPreservingOrigin方法。


- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin {
[self calculateLayoutWithSize:self.view.bounds.size];
YGApplyLayoutToViewHierarchy(self.view, preserveOrigin);
}

这里很明确分成两步:

  1. measure 指测量item所需要的大小,以及子节点的大小,也就是确定width 和 height
  2. layout 指将item确定的放置在具体的 (x, y) 点,也就是确定对应的posion

二者构成了UIView的frame

我们来看下第一步 – 测量

这里首先构建出当前view为起点的节点树,然后通过YGNodeCalculateLayout来算出宽高传递出去。这里关键的是YGNodeCalculateLayout这个方法,它是yoga的算法,是用C++实现的,由于代码很长,其中核心的Flex布局算法实现函数长达几千行,占Yoga.c的 2/3,第一次看很没有勇气看下去。所以后续会另起一篇博客来详细分析Yoga的底层实现。所以这里只要简单知道这些方法是干啥的就好。

- (CGSize)calculateLayoutWithSize:(CGSize)size {

NSAssert([NSThread isMainThread], @"Yoga calculation must be done on main.");
NSAssert(self.isEnabled, @"Yoga is not enabled for this view.");

//构建当前view为起点的节点树
YGAttachNodesFromViewHierachy(self.view);

const YGNodeRef node = self.node;
//计算出节点的宽高
YGNodeCalculateLayout(
node,
size.width /*约束宽度*/,
size.height /*约束高度*/,
YGNodeStyleGetDirection(node))/*方向*/;
//将宽高封装成CGSize 传递出去
return (CGSize) {
.width = YGNodeLayoutGetWidth(node),
.height = YGNodeLayoutGetHeight(node),
};
}

通过上面的计算我们获得了节点的详细参数,所以根据这些参数就可以很容易计算出对应的frame数值。下面是布局的具体代码。

static void YGApplyLayoutToViewHierarchy(UIView *view, BOOL preserveOrigin)
{

NSCAssert([NSThread isMainThread], @"Framesetting should only be done on the main thread.");

const YGLayout *yoga = view.yoga;

if (!yoga.isIncludedInLayout) {
return;
}

YGNodeRef node = yoga.node;

//获得左上角的坐标
const CGPoint topLeft = {
YGNodeLayoutGetLeft(node),
YGNodeLayoutGetTop(node),
};

//计算右下角的坐标
const CGPoint bottomRight = {
topLeft.x + YGNodeLayoutGetWidth(node),
topLeft.y + YGNodeLayoutGetHeight(node),
};

//如果preserveOrigin为true那么会保留原来的节点坐标
const CGPoint origin = preserveOrigin ? view.frame.origin : CGPointZero;
//布局该节点
view.frame = (CGRect) {
.origin = {
.x = YGRoundPixelValue(topLeft.x + origin.x),
.y = YGRoundPixelValue(topLeft.y + origin.y),
},
.size = {
.width = YGRoundPixelValue(bottomRight.x) - YGRoundPixelValue(topLeft.x),
.height = YGRoundPixelValue(bottomRight.y) - YGRoundPixelValue(topLeft.y),
},
};

if (!yoga.isLeaf) {
//对子节点进行布局
for (NSUInteger i=0; i<view.subviews.count; i++) {
YGApplyLayoutToViewHierarchy(view.subviews[i], NO);
}
}
}

到目前位置我们分析了整个YogaKit的源码,其实最核心最重要的还是Yoga底层的代码,这里由于篇幅原因将其放到下一篇博客中。

开篇叨叨

Masonry 是iOS目前用得比较多的一个布局框架,被广泛应用与较为简单的界面布局中,它是对iOS AutoLayout的一个封装,如果有使用过AutoLayout 原生API进行布局过我相信很难摆脱它给我们iOS职业生涯带来的阴影。经过封装过的Masonry提供了一种链式方式来描述NSLayoutConstraints,可以说经过封装后的Masonry有一种从地狱到天堂的感受。

我们来对比下原生AutoLayout以及使用Masonry实现同一个布局的代码效果。下面是实现在一个view中包含另外一个view,并且内部view和外部view的各个边界padding均为10

AutoLayout 版本

UIView *superview = self.view;

UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[superview addConstraints:@[

//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],

]];
Masonry 版本

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];

但是碍于性能的问题在项目中还是舍弃了,但是作为对AutoLayout的学习还是值得对其进行深入分析下的。

Masonry 源码解析

Masonry 提供了mas_makeConstraints,mas_updateConstraints,mas_remakeConstraints 三个入口。

  • mas_makeConstraints:

mas_makeConstraints 是最常用的通过它我们可以很方便得为一个UIView添加各种约束,mas_makeConstraints一般在viewDidLoad方法中调用来为view添加布局约束。

  • mas_updateConstraints

有时候我们只需要修改某个属性的值那么就需要用到mas_updateConstraints,mas_updateConstraints 一般在 updateConstraints方法中调用,这是苹果推荐的更新布局约束的地方。它会在调用setNeedsUpdateConstraints的时候被间接调用。

- (void)updateConstraints {

[self.button mas_updateConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.width.equalTo(@(self.buttonSize.width)).priorityLow();
make.height.equalTo(@(self.buttonSize.height)).priorityLow();
make.width.lessThanOrEqualTo(self);
make.height.lessThanOrEqualTo(self);
}];

//according to apple super should be called at end of method
[super updateConstraints];
}
  • mas_remakeConstraints

mas_remakeConstraints 和mas_updateConstraints类似,也是用于改变已经拥有的布局约束,但是不同的是它会在应用约束前线移除已经应用的约束。

mas_makeConstraints && mas_updateConstraints && mas_remakeConstraints
1. 约束条件的构建
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

和YogaKit类似它会在内部初始化一个MASConstraintMaker然后通过block传递出去,便于在外部对MASConstraintMaker进行设值,最后通过MASConstraintMaker的install方法应用约束。
代码的开头有个

self.translatesAutoresizingMaskIntoConstraints = NO;

在translatesAutoresizingMaskIntoConstraints设置为YES的情况下会把autoresizingMask 转换为 Constraints,也就是把 frame ,bouds,center 方式布局的视图自动转化为约束形式,这在使用AutoLayout很容易引入不需要的约束,甚至带来约束冲突。在使用frame形式布局的时候不需要设置该值,使用默认的YES.但是对于AutoLayout记得要把这个值设置为no

在MASConstraintMaker创建方法中会创建一个constraints数组用于存放约束

- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}

我们接下来看下我们怎么使用通过block抛出来的MASConstraintMaker,假设我们设置的约束如下:

make.top.equalTo(superview.mas_top).with.offset(padding.top);

MASConstraintMaker中包含了一系列的只读MASConstraint属性:

@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;
@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;
@property (nonatomic, strong, readonly) MASConstraint *leftMargin;
@property (nonatomic, strong, readonly) MASConstraint *rightMargin;
@property (nonatomic, strong, readonly) MASConstraint *topMargin;
@property (nonatomic, strong, readonly) MASConstraint *bottomMargin;
@property (nonatomic, strong, readonly) MASConstraint *leadingMargin;
@property (nonatomic, strong, readonly) MASConstraint *trailingMargin;
@property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *edges;
@property (nonatomic, strong, readonly) MASConstraint *size;
@property (nonatomic, strong, readonly) MASConstraint *center;

我们这里以top为例:

- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

我们这里constraint传入的为nil 所以代码的前半部分没有走,直接将self这是为newConstraint的delegate,并将其添加到constraints数组中,并返回newConstraint。

MASViewAttribute这个类是用于存储view以及它相关的布局属性NSLayoutAttribute,而MASViewConstraint则是一条布局约束规则,它包含着MASViewAttribute。
那么这个delegate作用是啥,留到后面介绍。

到目前为止,我们创建了一个MASConstraintMaker,MASConstraintMaker 包含一个constraints数组,用于存储这个view相关的约束,然后将MASConstraintMaker 通过block传递出去,我们在block中为MASConstraintMaker添加各个约束,一个约束是一个MASViewConstraint对象,它是由一个包含着NSLayoutAttribute以及关联的view的MASViewAttribute对象。并将新建的newConstraint返回。

MASConstraint有两个子类:MASViewConstraint 和 MASCompositeConstraint。 MASViewConstraint 为单条约束,而MASCompositeConstraint是类似于edges,size, center 的约束,它是单条约束的集合,比如size 是 width和height的集合,

我们接下来看下equalTo方法:

- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}

MASConstraint 是一个基类这里的部分实现都是直接抛出异常,需要子类来覆写它们。

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }

#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]

我们看下它的子类MASViewConstraint和MASCompositeConstraint中的实现

MASViewConstraint中的实现

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}

我们调用的时候传递进来了两个参数attribute 和 NSLayoutRelationEqual一个是关系,一个是属性。也就是说每个MASViewConstraint都有两个属性firstViewAttribute以及secondViewAttribute以及它们之间的关系layoutRelation。

typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,/*小于或者等于*/
NSLayoutRelationEqual = 0, /*等于*/
NSLayoutRelationGreaterThanOrEqual = 1,/*大于等于*/
};

这里得重点看下secondViewAttribute的设置过程

- (void)setSecondViewAttribute:(id)secondViewAttribute {
//1.
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
//2.
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
//3.
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}

这里有三个分支对应如下三种情况:

1. make.left.equalTo(@150); 传入 NSValue 的时, 会直接设置 constraint
2. make.left.equalTo(view); 这时, 就会初始化一个 layoutAttribute 属性与 firstViewArribute 相同的 MASViewAttribute
3. make.left.equalTo(view.mas_right); 传入一个MASViewAttribute

我们再回过头以下面约束为例子看下整个流程:

make.top.equalTo(superview.mas_top).with.offset(padding.top);

在调用make.top的时候会往通过block抛出来的MASConstraintMaker中添加一个MASViewConstraint,并将(mas_top)这个属性作为MASViewConstraint的firstViewAttribute,在调用equalTo的时候{superview.mas_top}将作为secondViewAttribute添加到刚刚创建的MASViewConstraint,并将MASViewConstraint中的layoutRelation标记为NSLayoutRelationEqual。这时候返回的是self。也就是MASViewConstraint类型。

调用.with的时候只是一种美观写法,实际上还是转调了self。没啥用处可以不写。

- (MASConstraint *)with {
return self;
}

我们再接着往下面看.offset

- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}

- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}

这里设置了layoutConstant的值。到目前位置应该可以梳理下各个对象之间到关系了,整个关系如下图所示:

每个view都有一个MASConstraintMaker,MASConstraintMaker里面有个数组constraints用于存放我们为view添加的各个约束条件,每个约束条件都对应一条MASConstraint,MASConstraint有两个子类MASViewConstraint和MASCompositeConstraint,.top .left 这种约束属于MASViewConstraint。.size .center一般是多个约束的简化属于MASViewConstraint。
我们会将mas_top和subview.mas_top分别作为firstViewAttribute和secondViewAttribute,并用layoutRelation标明之间的关系。对于offset等布局常量都存放在layoutConstant,除了这些属性外还有layoutMultiplier,layoutPriority这些重要属性。

2. 约束条件的应用

到目前位置我们构建了当前view到约束条件,接下来我们就看如何将这些约束条件应用到view上:

- (NSArray *)install {
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
for (MASConstraint *constraint in constraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}

首先先判断removeExisting字段,这个字段在mas_remakeConstraints的时候设置为YES.也就是说在mas_remakeConstraints中应用新的约束之前会将当前view上的所有的约束给清除后再应用新的。
在install方法中会对constraints的每个约束调用install方法进行应用。

- (void)install {
//在install之前,会做一次判断,看是否已经被install过
if (self.hasBeenInstalled) {
return;
}

//如果layoutConstraint支持active方法则将self.layoutConstraint.active标记为YES.
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
//并将当前约束添加到mas_installedConstraints,mas_installedConstraints这里记录着已经安装的约束。
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}

// 否则,使用传统方式,把约束添加到对应位置
//取出firstLayoutAttribute和secondLayoutAttribute对应的数据
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
//对于对齐属性必须要有secondViewAttribute,也即是参照的对象。如果self.secondViewAttribute是空的
//这里会将参照的对象设置为当前对象的父对象。
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}

MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];

layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;

if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}


MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}
}

我们一步步来看下:

  1. 先检查下是否已经应用过了,如果已经应用过了就直接返回
if (self.hasBeenInstalled) {
return;
}
  1. 如果layoutConstraint支持active方法则将self.layoutConstraint.active标记为YES.

iOS 8版本之后,Auto Layout推出新的接口。NSLayoutConstraint多了一个active属性,用于激活、失效一个约束。不需要再考虑约束安装位置。原本用于添加、移除约束的接口addConstraint/addConstraints、removeConstraint/removeConstraints,在后续的版本升级将会过期,这里是对iOS 8版本之后做的适配。


if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
//并将当前约束添加到mas_installedConstraints,mas_installedConstraints这里记录着已经安装的约束。
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}

  1. 取出参数并对需要的参数做修正:在应用之前会将firstLayoutAttribute和secondLayoutAttribute的对应的数据取出。如果当前元素为对齐属性那么必须要有secondViewAttribute,也即是参照的对象。如果这种情况下self.secondViewAttribute是空的这里会将参照的对象设置为当前对象的父对象。

  2. 将MASViewConstraint转换成原生的AutoLayout约束条件:
    也就是把我们在block中设置的一条条MASViewConstraint生成一个个约束

    MASLayoutConstraint *layoutConstraint
    = [MASLayoutConstraint constraintWithItem:firstLayoutItem
    attribute:firstLayoutAttribute
    relatedBy:self.layoutRelation
    toItem:secondLayoutItem
    attribute:secondLayoutAttribute
    multiplier:self.layoutMultiplier
    constant:self.layoutConstant];

    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
  3. 确定installedView

// 例如make.left.equalTo(view) 情况
if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
// 例如make.left.equalTo(@40) 情况
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}

这部分首先会看下self.secondViewAttribute.view是否有值,如果有的话,会通过mas_closestCommonSuperview来查找self.firstViewAttribute.view 和self.secondViewAttribute.view最近的公共父view作为self.installedView,
如果当前约束是尺寸约束,那么将self.firstViewAttribute.view作为self.installedView,否则将self.firstViewAttribute.view.superview作为self.installedView

  1. 应用或者更新installedView上的对应约束
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}

updateExisting是在mas_updateConstraints方法中设置的,也就是说如果是更新某个约束条件的话先通过layoutConstraintSimilarTo找到对应的约束,如果找到了就将找到的约束赋值给self.layoutConstraint,如果没有相似的就调用addConstraint来应用约束。到此整个流程结束。

最后用一张图来结束这篇博客: