PureLayout 源码解析

在开始这篇文章之前,想必大家都应该使用过Autolayout方式的界面布局,相信大家都有过类似于如下这样的API调用:

[NSLayoutConstraint(item: self.viewA, attribute: .CenterY, relatedBy: .Equal, toItem: self.viewB, attribute: .CenterY, multiplier: 1.0, constant: 0.0)]

抑或是Visual Format Language

NSLayoutConstraint.constraintsWithVisualFormat("|-(leftPadding)-[imageView(imageViewWidth)]-(rigntPadding)-[labelA]-(4)-[labelB]-(>=44)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: methics, views: views)

这种冗长而又晦涩的代码,真是恶心人啊。因此在Github上涌现了一大堆简化布局的开源库,如SnapKit, Mansory以及今天我们要说的PureLayout。

在这之中,PureLayout是最轻量级的,它仅仅是对Autolayout现成的语法进行了一层封装,相较于Mansory引入的一些新概念,Purelayout更直接易懂。

源码解析

Purelayout的源码基本没什么难懂的地方,我们首先来看一下其项目结构:

  • PurelayoutDefines.h
  • ALView + PureLayout.h/.m
  • NSArray + PureLayout.h/.m
  • NSLayoutConstraint + Purelayout.h/.m

PurelayoutDefines

首先从PurelayoutDefines上入手,这个文件主要是进行一些类似Domain Specific Language定义的转化,如:

typedef NS_ENUM(NSInteger, ALEdge) {
    /** The left edge of the view. */
    ALEdgeLeft = NSLayoutAttributeLeft,
    /** The right edge of the view. */
    ALEdgeRight = NSLayoutAttributeRight,
    /** The top edge of the view. */
    ALEdgeTop = NSLayoutAttributeTop,
    /** The bottom edge of the view. */
    ALEdgeBottom = NSLayoutAttributeBottom,
    /** The leading edge of the view (left edge for left-to-right languages like English, right edge for right-to-left languages like Arabic). */
    ALEdgeLeading = NSLayoutAttributeLeading,
    /** The trailing edge of the view (right edge for left-to-right languages like English, left edge for right-to-left languages like Arabic). */
    ALEdgeTrailing = NSLayoutAttributeTrailing
};

上述这段代码,就是将传统的UIKit中的NSLayoutAttribute的枚举类型全部转换成对应的PureLayout中的定义,如ALEdgeRight对应到NSLayoutAttributeRight。

LayoutMargins
在这里补充一点题外知识,在iOS8中,苹果为Autolayout引入了LayoutMargins这一概念。这个概念乍一听可能都不了解,但是大家回忆下,比如在Storyboard中,我们拖拽一个UIView到ViewController的view并设置边距的时候,上边距和下边距对应的限制都是layout guide,如下图所示:


简单来说,在iOS7上就已经存在了LayoutMargin了,当时的作用是用来限制view的真实内容不会被UINavigationBar(上部)以及UIToolbar(下部)所遮盖。而从iOS8中开始,苹果将这一技术引入到了任意一个UIView中。

ALView + Purelayout

ALView实际上是UIView或者NSView的别名,通过添加ALView的分类,可以通过Define在编译期进行替换,避免为NSView和UIView各创建一份重复的代码。这个类中的API过多,因此我们以轴对齐为典型的例子来分解下源码:

  1. 轴对齐
    在PureLayout中,包括Vertical, Horizontal, Baseline等几种轴对齐方式,其中Baseline指的是View中潜在包含文字的Baseline。

好,我们来看看相关的API

/** Aligns an axis of the view to the same axis of another view. */
- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView;

从该API的名称,我们可以直观的感觉出其作用是用于将两个View按照同一个轴对齐。这个API是一个Convenience Init,其层层传递

- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView
{
    return [self autoAlignAxis:axis toSameAxisOfView:otherView withOffset:0.0];
}

- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView withOffset:(CGFloat)offset
{
    return [self autoConstrainAttribute:(ALAttribute)axis toAttribute:(ALAttribute)axis ofView:otherView withOffset:offset];
}

最后调用了

- (NSLayoutConstraint *)autoConstrainAttribute:(ALAttribute)attribute toAttribute:(ALAttribute)toAttribute ofView:(ALView *)otherView withOffset:(CGFloat)offset`

好,那就让我们来看看这个上述这个函数的实现,如下所示:

//1.
self.translatesAutoresizingMaskIntoConstraints = NO;

//2.
NSLayoutAttribute layoutAttribute = [NSLayoutConstraint al_layoutAttributeForAttribute:attribute];
NSLayoutAttribute toLayoutAttribute = [NSLayoutConstraint al_layoutAttributeForAttribute:toAttribute];

//3.
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self attribute:layoutAttribute relatedBy:relation toItem:otherView attribute:toLayoutAttribute multiplier:1.0 constant:offset];

//4.
[constraint autoInstall];
return constraint;
  • 1.首先将translatesAutoresizingMaskIntoConstraints设置为false,对于要使用autolayout的UIView,必须设置为false,也就是不将传统frame布局中的Autoresizing Mask转换成约束。
  • 2.根据传入的PureLayout属性转换成对应的NSLayoutAttribute
  • 3.调用冗长恶心的Autolayout API构建约束
  • 4.添加约束

在这里,我们需要注意一下这个[constraint autoInstall],让我们来探一探实现:

- (void)autoInstall
{
// 1. iOS8+
#if __PureLayout_MinBaseSDK_iOS_8_0 || __PureLayout_MinBaseSDK_OSX_10_10
    if ([self respondsToSelector:@selector(setActive:)]) {
        [NSLayoutConstraint al_applyGlobalStateToConstraint:self];
        // 1.1
        if ([NSLayoutConstraint al_preventAutomaticConstraintInstallation]) {         
            [[NSLayoutConstraint al_currentArrayOfCreatedConstraints] addObject:self];
        } else {
        // 1.2 
            self.active = YES;
        }
        return;
    }
#endif /* __PureLayout_MinBaseSDK_iOS_8_0 || __PureLayout_MinBaseSDK_OSX_10_10 */

// 2. iOS 7
    NSAssert(self.firstItem || self.secondItem, @"Can't install a constraint with nil firstItem and secondItem.");
    if (self.firstItem) {
        if (self.secondItem) {
            NSAssert([self.firstItem isKindOfClass:[ALView class]] && [self.secondItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if both items are views.");
            ALView *commonSuperview = [self.firstItem al_commonSuperviewWithView:self.secondItem];
            [commonSuperview al_addConstraint:self];
        } else {
            NSAssert([self.firstItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if the item is a view.");
            [self.firstItem al_addConstraint:self];
        }
    } else {
        NSAssert([self.secondItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if the item is a view.");
        [self.secondItem al_addConstraint:self];
    }
}

整个实现的部分被一分为二,上半部分专门针对iOS8+的,下半部分针对iOS7(事实上在整个PureLayout的设计中,大部分地方的处理方式都一分为二了

我们暂时也不管al_applyGlobalStateToConstraint:self 以及 al_preventAutomaticConstraintInstallation的作用,我们从1.2看起。

  • 在iOS8上,启用或者禁用一个AutoLayout的Constraint变得更加容易了,仅仅需要设置active即可
  • 在iOS7上,需要手动的addConstraint或者removeConstraint
  • 在处理iOS7的逻辑当中,需要判断当前这个Constraint是否是针对两个Item的,如果是,找到他们的公共父View,在父View在添加约束,比如添加View A和View B之间的间距;而如果是单一一个View,比如是设置高度或者宽度的,直接在当前View添加即可。
  • 通过调用al_addConstraint进行约束实际的添加。

al_addConstraint的实现则如下所示:

[NSLayoutConstraint al_applyGlobalStateToConstraint:constraint];
if ([NSLayoutConstraint al_preventAutomaticConstraintInstallation]) {
    [[NSLayoutConstraint al_currentArrayOfCreatedConstraints] addObject:constraint];
} else {
    [self addConstraint:constraint];
}

这里又出现了al_applyGlobalStateToConstraint:constraint以及al_preventAutomaticConstraintInstallation了,这次我们可不能再躲着它了,赶紧瞧一瞧。

首先是al_applyGlobalStateToConstraint:constraint,这个参数对应的是一个全局静态变量,用于判断:

if ([NSLayoutConstraint al_isExecutingPriorityConstraintsBlock]) {
    constraint.priority = [NSLayoutConstraint al_currentGlobalConstraintPriority];
}

而这个al_isExecutingPriorityConstraintsBlock则是用于如下这个函数:

+ (void)autoSetPriority:(ALLayoutPriority)priority forConstraints:(ALConstraintsBlock)block
{
    NSAssert(block, @"The constraints block cannot be nil.");
    if (block) {
        [[self al_globalConstraintPriorities] addObject:@(priority)];
        block();
        [[self al_globalConstraintPriorities] removeLastObject];
    }
}

这里可能大家有点晦涩,主要在于PureLayout对于给Constraint设置Priority定义了一个Block-based的方法,也就是autoSetPriority。在回调的Block中,可以对多个Constraint设置同一个大小的Priority。(其实我也不是很理解这个集体加Priority设计的目的

不过需要有一点可以肯定的是,设置Constraint的Priority的时机一定要在addConstraint或者active = true之前

而对于al_preventAutomaticConstraintInstallation这个变量,作者在API中描述了如下一段话:

Creates all of the constraints in the block, then installs (activates) them all at once.
All constraints created from calls to the PureLayout API in the block are returned in a single array.
This may be more efficient than installing (activating) each constraint one-by-one.

简而言之,一次性添加所有约束(实际上调用了UIKit的APIactivateConstraints),比一个个添加要有效率。然而,Purelayout的这个特性对于iOS7来说,用不上,只能通过addConstraint一个个装,哈哈,么么哒

NSArray + Purelayout

说完了ALView的layout,我们接下来说说另外的NSArray + Purelayout。顾名思义,该分类的主要目的就是给一个NSArray中的所有UIView添加约束。

比如这个API:

- (__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
                                                           alignedTo:(ALAttribute)alignment
                                                    withFixedSpacing:(CGFloat)spacing
                                                        insetSpacing:(BOOL)shouldSpaceInsets
                                                        matchedSizes:(BOOL)shouldMatchSizes

其实现如下:

NSAssert([self al_containsMinimumNumberOfViews:1], @"This array must contain at least 1 view to distribute.");

//1. 第一部分
    ALDimension matchedDimension;
    ALEdge firstEdge, lastEdge;
    switch (axis) {
        case ALAxisHorizontal:
        case ALAxisBaseline: // same value as ALAxisLastBaseline
#if __PureLayout_MinBaseSDK_iOS_8_0
        case ALAxisFirstBaseline:
#endif /* __PureLayout_MinBaseSDK_iOS_8_0 */
            matchedDimension = ALDimensionWidth;
            firstEdge = ALEdgeLeading;
            lastEdge = ALEdgeTrailing;
            break;
        case ALAxisVertical:
            matchedDimension = ALDimensionHeight;
            firstEdge = ALEdgeTop;
            lastEdge = ALEdgeBottom;
            break;
        default:
            NSAssert(nil, @"Not a valid ALAxis.");
            return nil;
    }
    CGFloat leadingSpacing = shouldSpaceInsets ? spacing : 0.0;
    CGFloat trailingSpacing = shouldSpaceInsets ? spacing : 0.0;

//2. 第二部分  
    __NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
    ALView *previousView = nil;
    for (id object in self) {
        if ([object isKindOfClass:[ALView class]]) {
            ALView *view = (ALView *)object;
            view.translatesAutoresizingMaskIntoConstraints = NO;
            if (previousView) {
                // Second, Third, ... View
                [constraints addObject:[view autoPinEdge:firstEdge toEdge:lastEdge ofView:previousView withOffset:spacing]];
                if (shouldMatchSizes) {
                    [constraints addObject:[view autoMatchDimension:matchedDimension toDimension:matchedDimension ofView:previousView]];
                }
                [constraints addObject:[view al_alignAttribute:alignment toView:previousView forAxis:axis]];
            }
            else {
                // First view
                [constraints addObject:[view autoPinEdgeToSuperviewEdge:firstEdge withInset:leadingSpacing]];
            }
            previousView = view;
        }
    }
    if (previousView) {
        // Last View
        [constraints addObject:[previousView autoPinEdgeToSuperviewEdge:lastEdge withInset:trailingSpacing]];
    }
    return constraints;            
  1. 这个API的目的是将一组UIView按照Spacing间距进行均分,同时每个UIView的宽度或者高度保持一致。
  2. 第一部分是根据传入的轴,进行判断,是在竖直方向均分还是水平方向均分,同时影响的还有是宽度一致还是高度一致。
  3. 第二部分是根据传入的轴(比如水平方向),将前一个View的右边距和后一个View的左边距添加间距,循环添加,直至最后一个View的右边距和父View的右边距添加完成约束。

其他方面,这个分类的作用基本和ALView + PureLayout一致,也就不再重复解释了。
至此,PureLayout的源码解析基本上差不多了,其余类似于边对齐的API,如:

- (NSLayoutConstraint *)autoPinEdge:(ALEdge)edge toEdge:(ALEdge)toEdge ofView:(ALView *)otherView;

又或者是约束尺寸的,如:

- (__NSArray_of(NSLayoutConstraint *) *)autoSetDimensionsToSize:(CGSize)size;

都大同小异,在此就不一一赘述了。

最后,强调一点

  1. PureLayout必须在主线程使用,其本身实现非常依赖于静态的全局变量。