iOS布局與Masnory使用實(shí)踐

前言

UI布局對(duì)于iOS開發(fā)者來(lái)說(shuō)并不陌生,在iOS6之前,大家都是通過(guò)UI控件的Frame屬性和Autoresizing Mask來(lái)進(jìn)行UI布局的(簡(jiǎn)稱為手動(dòng)布局)。AutoLayout則是蘋果公司在iOS6推出的一種基于約束的,描述性的布局系統(tǒng)(簡(jiǎn)稱為自動(dòng)布局),這里主要從四個(gè)方面來(lái)闡述iOS布局及實(shí)踐。

  • 手動(dòng)布局和自動(dòng)布局
  • AutoLayout原理
  • AutoLayout的性能
  • Masnory的使用

首先對(duì)手動(dòng)布局和自動(dòng)布局做一個(gè)簡(jiǎn)單的介紹:

手動(dòng)布局和自動(dòng)布局
  • 手動(dòng)布局:指的是通過(guò)直接修改視圖的frame屬性的方式對(duì)界面進(jìn)行布局。

對(duì)于IOSapp開發(fā)者來(lái)說(shuō),不會(huì)像Android開發(fā)者一樣為很多的屏幕尺寸來(lái)做界面適配,因此手動(dòng)調(diào)整 frame的方式來(lái)布局也能工作良好。但是還是會(huì)有一些問(wèn)題,如設(shè)備發(fā)生旋轉(zhuǎn)、適配ipad等,并且保證視圖原來(lái)之間的相對(duì)關(guān)系,則以上的方法都是無(wú)法解決的。如果要做這些適配,在AutoLayout未出來(lái)之前需要編寫大量的代碼,并且花費(fèi)大量的調(diào)試適配時(shí)間。

  • 自動(dòng)布局:指的是使用AutoLayout的方式對(duì)界面進(jìn)行布局。

AutoLayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率,但是 AutoLayout對(duì)于復(fù)雜視圖來(lái)說(shuō)常常會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題。隨著視圖數(shù)量的增長(zhǎng),AutoLayout 帶來(lái)的 CPU 消耗會(huì)呈指數(shù)級(jí)上升。 如果對(duì)界面流暢度要求較高(如微博界面),可以通過(guò)提前計(jì)算好布局,在需要時(shí)一次性調(diào)整好對(duì)應(yīng)屬性 ,或者使用 ComponentKit、AsyncDisplayKit 等框架來(lái)處理界面布局。

下面,我們來(lái)分析下 AutoLayout的原理。

AutoLayout的原理

這里通過(guò)使用Masonry來(lái)進(jìn)行布局,從而來(lái)分析AutoLayout的原理,先簡(jiǎn)要了解下Masonry。
Masonry是一個(gè)輕量級(jí)的布局框架,擁有自己的描述語(yǔ)法,采用更優(yōu)雅的鏈?zhǔn)秸Z(yǔ)法封裝自動(dòng)布局,簡(jiǎn)潔明了,并具有高可讀性,而且同時(shí)支持 iOSMax OS X。
Masnory支持的常用屬性如下:

@property (nonatomic, strong, readonly) MASConstraint *left;     //左側(cè)
@property (nonatomic, strong, readonly) MASConstraint *top;      //上側(cè)
@property (nonatomic, strong, readonly) MASConstraint *right;   //右側(cè)
@property (nonatomic, strong, readonly) MASConstraint *bottom;   //下側(cè)
@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;  //橫向中點(diǎn)
@property (nonatomic, strong, readonly) MASConstraint *centerY;  //縱向中點(diǎn)
@property (nonatomic, strong, readonly) MASConstraint *baseline; //文本基線 

其中leadingleft,trailingright 在正常情況下是等價(jià)的,但是當(dāng)一些布局是從右至左時(shí)(比如阿拉伯語(yǔ)) 則會(huì)對(duì)調(diào)。
同時(shí),在Masonry中能夠添加AutoLayout約束有三個(gè)函數(shù):

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;//只負(fù)責(zé)新增約束` AutoLayout`不能同時(shí)存在兩條針對(duì)于同一對(duì)象的約束,否則會(huì)報(bào)錯(cuò)
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;//針對(duì)上面的情況 會(huì)更新在block中出現(xiàn)的約束 不會(huì)導(dǎo)致出現(xiàn)兩個(gè)相同約束的情況
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;//則會(huì)清除之前的所有約束 僅保留最新的約束

我們?cè)诖a中,經(jīng)常會(huì)使用到equalTomas_equalTo,那它們的區(qū)別是什么呢?從代碼中找到他們的定義如下:

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
...
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

可以看到 mas_equalTo只是對(duì)其參數(shù)進(jìn)行了一個(gè)BOX操作(裝箱) ,所支持的類型,除了NSNumber支持的那些數(shù)值類型之外,還支持CGPointCGSizeUIEdgeInsets類型。
下面,我們通過(guò)一個(gè)例子,一步步來(lái)看下界面是怎么布局的,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
    
    UIView *v1 = [[UIView alloc] init];
    v1.backgroundColor = [UIColor orangeColor];
    [v1 showPlaceHolder];
    
    UIView *v2 = [[UIView alloc] init];
    v2.backgroundColor = [UIColor orangeColor];
    [v2 showPlaceHolder];
    
    UIView *v3 = [[UIView alloc] init];
    v3.backgroundColor = [UIColor orangeColor];
    [v3 showPlaceHolder];
    
    [self.view addSubview:v1];
    [self.view addSubview:v2];
    [self.view addSubview:v3];
    
    [v1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(100);
        make.leading.mas_equalTo(100);
        make.width.mas_equalTo(70);
        make.height.mas_equalTo(65);
    }];
    
    [v2 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(v1.mas_top);
        make.leading.mas_equalTo(v1.mas_trailing).offset(20);
        make.width.equalTo(v1.mas_width);
        make.height.equalTo(v1.mas_height);
    }];
    
    [v3 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(v1.mas_bottom).offset(20);
        make.leading.equalTo(v1.mas_leading);
        make.trailing.equalTo(v2.mas_trailing);
        make.height.equalTo(v1.mas_height);
    }];
} 

界面運(yùn)行結(jié)果如下圖:

CD52E302-FAFD-4D6E-9DFF-F5DB44C6098B.png

下面,我們將界面中的左上角的視圖視為視圖1,右上角的視圖視為視圖2,底部視圖視為視圖3,使用x1、y1、m1、n1來(lái)標(biāo)識(shí)視圖1的left、top、widthheight,以此類推。
通過(guò)以上舉例抽象出自動(dòng)布局?jǐn)?shù)學(xué)公式:
1C7344E7-1ED1-421A-B84E-ACBD70F98859.png

將以上等式變形為:

8AAD54BE-0157-4591-A117-0094F69BE6E7.png

此時(shí),以上方程組,大家肯定很熟悉了,也就是《線性代數(shù)》中的線性方程組,現(xiàn)在將以上線性方程組抽象為:

B5E84F57-07DE-4A15-9D06-3D0ADD7E6EBD.png

上圖表示“等式”方程組,那么是否還可以繼續(xù)抽象?也就是說(shuō)上述方程組能否完全表示未知元素之間與已知元素之間的關(guān)系,顯然還不全面,因?yàn)檫€有(<,>,<=,>=)不等關(guān)系,因此將“=”等號(hào)抽象為關(guān)系"R",在數(shù)學(xué)上關(guān)系R也就包括了“=”,"<",">","<=",">="等關(guān)系。上述線程方程組變形為:(實(shí)質(zhì)上,AutoLayout中所有的約束確實(shí)都是用數(shù)學(xué)關(guān)系式y(tǒng) R ax + b描述)

9A877277-5DEB-437C-AEF6-D6530AB6FE6F.png

現(xiàn)在已經(jīng)將自動(dòng)布局一步步抽象為數(shù)學(xué)公式,那么對(duì)視圖的布局其實(shí)就是對(duì)線性方程組的求解。線性方程組解的情況有三種,實(shí)質(zhì)上也對(duì)應(yīng)著自動(dòng)布局對(duì)視圖的三種布局方案:

  • 唯一解:所有方程中的未知數(shù)能夠解出唯一解。 充分約束:給一個(gè)視圖添加的約束必須是充分的,才能正確布局一個(gè)視圖;
  • 多個(gè)解:未知數(shù)不能求解出準(zhǔn)確的唯一解,即未知數(shù)可能存在多個(gè)或者無(wú)限個(gè)解滿足線性方程組。 欠約束:給視圖所添加的約束不能夠充分的表達(dá)視圖的準(zhǔn)確位置,在這種情況下自動(dòng)布局會(huì)隨意給視圖一個(gè)布局方案,也就是自動(dòng)布局中視圖不能夠正確布局或者視圖丟失的情況。
  • 無(wú)解:不存在滿足線性方程組的解。 沖突約束:給視圖添加的約束表達(dá)視圖布局出現(xiàn)了沖突,比如同時(shí)滿足同一個(gè)視圖寬度即為100又為200,這是不可能存在的。此時(shí)程序會(huì)出現(xiàn)崩潰。

通過(guò)以上描述,將AutoLayout系統(tǒng)的作用描述如圖所示:

FBB5F53F-0B23-48B2-B150-39B405BFB335.png
AutoLayout的性能

AutoLayout的原理,我們可以得出布局系統(tǒng)最后仍然需要通過(guò)frame來(lái)進(jìn)行布局,相比原有的布局系統(tǒng)加入了從約束計(jì)算 出frame 的過(guò)程,那么這個(gè)過(guò)程對(duì)性能是否會(huì)影響呢?
你可以在 這里 找到這次對(duì) Layout 性能測(cè)量使用的代碼。
代碼分別使用Auto Layout、嵌套視圖層級(jí)中使用 Auto Layoutframe對(duì) N 個(gè)視圖進(jìn)行布局,測(cè)算其運(yùn)行時(shí)間。

對(duì)視圖數(shù)量在 1~35 之間布局時(shí)間進(jìn)行測(cè)量,結(jié)果如下:

視圖數(shù)量范圍為 1~35.png

對(duì)視圖數(shù)量在 10~500 之間布局時(shí)間進(jìn)行測(cè)量,結(jié)果如下:

視圖數(shù)量范圍為 10~500.png

從上述的測(cè)試數(shù)據(jù)可以看出,使用frame、AutoLayout和嵌套視圖層級(jí)中使用 Auto Layout進(jìn)行布局、對(duì)應(yīng)的視圖數(shù)量分別為50個(gè)、6個(gè)和12個(gè),所需要的時(shí)間就會(huì)在 16.67 ms左右。,而想要讓 iOS 應(yīng)用的視圖保持 60 FPS 的刷新頻率,我們必須在 1/60 = 16.67 ms 之內(nèi)完成包括布局、繪制以及渲染等操作。
綜上所述,雖然說(shuō) Auto Layout 為開發(fā)者在多尺寸布局上提供了遍歷,而且支持跨越視圖層級(jí)的約束,但是由于其實(shí)現(xiàn)原理導(dǎo)致其時(shí)間復(fù)雜度為多項(xiàng)式時(shí)間,其性能損耗是僅使用 frame 的十幾倍,所以在處理龐大的 UI界面時(shí)表現(xiàn)差強(qiáng)人意。

Masnory的使用

下面,我們通過(guò)4個(gè)實(shí)例,來(lái)了解下Masnory的使用。

  • case 1: 并排顯示兩個(gè)label,寬度由內(nèi)容決定。父視圖寬度不夠時(shí),優(yōu)先顯示右邊label的內(nèi)容。

在默認(rèn)情況下,我們沒(méi)有設(shè)置各個(gè)布局的優(yōu)先級(jí),那么他就會(huì)優(yōu)先顯示左邊的label,左邊的完全顯示后剩余的空間都是右邊的label,如果整個(gè)空間寬度都不夠左邊的label的話,那么右邊的label就沒(méi)有顯示的機(jī)會(huì)了。
如果我們現(xiàn)在的需求是優(yōu)先顯示右邊的label,左邊的label內(nèi)容超出的省略,這時(shí)就需要我們調(diào)整約束的優(yōu)先級(jí)了。
UIView中關(guān)于Content HuggingContent Compression Resistance的方法有:

- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

那么這兩個(gè)東西到底是什么呢?可以這樣形象的理解一下:

  • contentHugging: 抱住使其在“內(nèi)容大小”的基礎(chǔ)上不能繼續(xù)變大,這個(gè)屬性的優(yōu)先級(jí)越高,就要越“抱緊”視圖里面的內(nèi)容。也就是視圖的大小不會(huì)隨著父視圖的擴(kuò)大而擴(kuò)大。
  • contentCompression: 撐住使其在在其“內(nèi)容大小”的基礎(chǔ)上不能繼續(xù)變小,這個(gè)屬性的優(yōu)先級(jí)越高,越不“容易”被壓縮。也就是說(shuō),當(dāng)整體的空間裝不下所有的視圖時(shí),Content Compression Resistance優(yōu)先級(jí)越高的,顯示的內(nèi)容越完整。
    這兩個(gè)屬性分別可以設(shè)置水平方向和垂直方向上的,而且一個(gè)默認(rèn)優(yōu)先級(jí)是250, 一個(gè)默認(rèn)優(yōu)先級(jí)是750. 因?yàn)檫@兩個(gè)很有可能與其他Constraint沖突,所以優(yōu)先級(jí)較低。
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; // A required constraint.  Do not exceed this.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; // This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; // This is the priority level at which a button hugs its contents horizontally.
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50; 
- (void)layoutPageSubViews {
    
    [self.leftLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.contentView1.mas_top).with.offset(5);
        make.left.equalTo(self.contentView1.mas_left).with.offset(2);
        make.height.equalTo(@40);
    }];
    
    [self.rightLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.leftLabel.mas_right).with.offset(2);
        make.top.equalTo(self.contentView1.mas_top).with.offset(5);
        make.right.lessThanOrEqualTo(self.contentView1.mas_right).with.offset(-2);
        make.height.equalTo(@40);

    }];
    
    [self.leftLabel setContentHuggingPriority:UILayoutPriorityRequired
                               forAxis:UILayoutConstraintAxisHorizontal];
    [self.leftLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
                                             forAxis:UILayoutConstraintAxisHorizontal];
    
    [self.rightLabel setContentHuggingPriority:UILayoutPriorityRequired
                               forAxis:UILayoutConstraintAxisHorizontal];
    [self.rightLabel setContentCompressionResistancePriority:UILayoutPriorityRequired
                                             forAxis:UILayoutConstraintAxisHorizontal];
}
  • case 2: 四個(gè)ImageView整體居中,可以任意顯示、隱藏。
blog_autolayout_example_with_masonry_3.png

下面的四個(gè)Switch控件分別控制上面對(duì)應(yīng)位置的圖片是否顯示。

分析:首先就是整體居中,為了實(shí)現(xiàn)這個(gè),最簡(jiǎn)單的辦法就是將四個(gè)圖片“裝進(jìn)”一個(gè)容器View里面,然后讓這個(gè)容器View在整個(gè)頁(yè)面中居中即可。這樣就不用控制每個(gè)圖片的居中效果了。
然后就是顯示與隱藏。在這里我直接控制圖片ImageView的寬度,寬度為0的時(shí)候不就“隱藏”了嗎。

具體代碼如下:

- (void)layoutPageSubViews {
    
    [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(IMAGE_SIZE);
        make.centerX.equalTo(self.view.mas_centerX);
        make.top.equalTo(self.view.mas_top).offset(200);
    }];
    
    //分別設(shè)置每個(gè)imageView的寬高、左邊、垂直中心約束,注意約束的對(duì)象
    //每個(gè)View的左邊約束和左邊的View的右邊相等
    __block UIView *lastView = nil;
    __block MASConstraint *widthConstraint = nil;
    NSUInteger arrayCount = self.imageViews.count;
    [self.imageViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
        [view mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(lastView ? lastView.mas_right : view.superview.mas_left);
            make.centerY.equalTo(view.superview.mas_centerY);
            if (idx == arrayCount - 1) {
                make.right.equalTo(view.superview.mas_right);
            }
            
            widthConstraint = make.width.mas_equalTo(IMAGE_SIZE);
            make.height.mas_equalTo(IMAGE_SIZE);
            
            [self.widthConstraints addObject:widthConstraint];
            lastView = view;
        }];
    }];
}

#pragma mark - event response
//點(diǎn)擊switch按鈕,如果打開,對(duì)應(yīng)視圖的寬約束設(shè)置為32,否則,設(shè)置為0
- (IBAction)showOrHideImage:(UISwitch *)sender {
    NSUInteger index = (NSUInteger) sender.tag;
    MASConstraint *width = self.widthConstraints[index];

    if (sender.on) {
        width.mas_equalTo(IMAGE_SIZE);
    } else {
        width.mas_equalTo(0);
    }
}
  • case 3: 子視圖的寬度始終是父視圖的四分之三(或者任意百分比)

  //寬度為父view的寬度的四分之三 
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
        //上下左貼邊
        make.left.equalTo(_containerView.mas_left);
        make.top.equalTo(_containerView.mas_top);
        make.bottom.equalTo(_containerView.mas_bottom);
        //寬度為父view的寬度的一半
        make.width.equalTo(_containerView.mas_width).multipliedBy(0.75);
    }];
  • case 4 給同一個(gè)屬性添加多重約束,實(shí)現(xiàn)復(fù)雜關(guān)系

- (void)layoutPageSubviews {
    
    [self.greenLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.containerView);
        make.right.lessThanOrEqualTo(self.containerView);
        make.left.greaterThanOrEqualTo(self.containerView.mas_right).multipliedBy((CGFloat)(1.0f / 3.0f));
        for (UILabel *label in self.leftLabels) {
            make.left.greaterThanOrEqualTo(label.mas_right).offset(8);
        }
    }];
    
    [self.greenLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
}
總結(jié)

通過(guò)上述分析,我們可以發(fā)現(xiàn):

  • AutoLayout的原理就是對(duì)線性方程組或者不等式的求解,最終使用frame來(lái)繪制視圖;
  • 使用AutoLayout進(jìn)行布局時(shí), 由于其實(shí)現(xiàn)原理導(dǎo)致其時(shí)間復(fù)雜度為多項(xiàng)式時(shí)間,其性能損耗是僅使用 frame 的十幾倍,所以在處理龐大的 UI界面時(shí)表現(xiàn)差強(qiáng)人意。

本文已經(jīng)同步到我的個(gè)人技術(shù)博客: leverTsui ,歡迎常來(lái)^^。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容