iOS開發過程中很大一部分內容就是界面佈局和跳轉,iOS的佈局方式也經歷了 顯式坐標定位方式 --> autoresizingMask --> iOS 6.0推出的自動佈局(Auto Layout)的逐步優化,至於為什麼推出自動佈局,肯定是因為之前的方法不好用(哈哈 簡直是廢話),具體如何不好用以及 ...
iOS開發過程中很大一部分內容就是界面佈局和跳轉,iOS的佈局方式也經歷了 顯式坐標定位方式 --> autoresizingMask --> iOS 6.0推出的自動佈局(Auto Layout)的逐步優化,至於為什麼推出自動佈局,肯定是因為之前的方法不好用(哈哈 簡直是廢話),具體如何不好用以及怎麼變化大家可以瞅瞅 這篇文章。iOS6.0推出的自動佈局實際上用佈局約束(Layout Constraint)來實現,通過佈局約束(Layout Constraint)可以確定兩個視圖之間精確的位置的相對距離,為此,iOS6.0推出了NSLayoutConstraint來定義約束,使用方法如下:
[NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeRight multiplier:1 constant:10]; //翻譯過來就是:view1的左側,在,view2的右側,再多10個點,的地方。
佈局約束的添加規則:
(1)對於兩個同層級 view 之間的約束關係,添加到它們的父 view 上
(2)對於兩個不同層級 view 之間的約束關係,添加到他們最近的共同父 view 上
(3)對於有層次關係的兩個 view 之間的約束關係,添加到層次較高的父 view 上
(4)對於比如長寬之類的,只作用在該 view 自己身上的話,添加到該 view 自己上
具體關於NSLayoutConstraint的詳細使用方法參見:NSLayoutConstraint-代碼實現自動佈局。今天我們文章的主角——Masonry框架實際上是在NSLayoutConstraint的基礎上進行封裝的,這一點在後面的源碼分析中我們詳細解釋。
1 Masonry的佈局教程
當我們需要對控制項的top,bottom,left,right進行約束就特別麻煩,在OC中有一個庫Masonry
對NSLayoutConstraint
進行了封裝,簡化了約添加約束的方式和流程。用Masonry框架進行佈局非常簡單,主要特點是採用鏈式語法進行佈局,這一點使得我們在使用和代碼佈局上更為方便,利用Masonry進行佈局的前提條件之一是 佈局視圖必須先被添加到父視圖中。簡單示例如下代碼,關於Masonry框架的使用並不是本文的重點,詳情可以參見:Masonry介紹與使用實踐:快速上手Autolayout。如果你的項目是Swift語言的,那麼就得使用SnapKit佈局框架了,SnapKit其實就是Masonry的Swift版本,兩者雖然實現語言不同,但是實現思路大體一致。
UIView *sv1 = [UIView new]; //利用Masonry進行佈局的前提條件之一是 佈局視圖必須先被添加到父視圖中 [sv addSubview:sv1]; [sv1 mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(sv).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); /* 等價於 make.top.equalTo(sv).with.offset(10); make.left.equalTo(sv).with.offset(10); make.bottom.equalTo(sv).with.offset(-10); make.right.equalTo(sv).with.offset(-10); */ /* 也等價於 make.top.left.bottom.and.right.equalTo(sv).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); */ }];
2 Masonry框架源碼分析
Masonry框架是在NSLayoutConstraint的基礎上進行封裝的,其涉及到的內容也是非常繁多。在進行源碼剖析時我們從我們經常用到的部分出發,一層一層進行解析和研究。
2.1 調用流程分析
首先,我們先大體瞭解一下調用 mas_makeConstraints 進行佈局時的流程步驟,其實另外兩個 mas_updateConstraints 和 mas_remakeConstraints 的流程也基本上是一樣的。
- - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block 是Masonry框架中UIview + MASAdditions(UIview分類)中的方法,所以一般的控制項視圖都可以直接調用該方法,該方法傳入一個block函數作為參數(返回值為void,參數為
MASContraintMaker
的實例對象make) - 主要佈局方法 - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block 的源碼和解析如下,主要工作是創建一個約束創建器,並將其傳到block中(其實就是block中的make創建器)進行創建約束並返回
@implementation MAS_VIEW (MASAdditions) - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { //關閉AutoresizingMask的佈局方法是我們進行Auto Layout佈局的前提步驟 self.translatesAutoresizingMaskIntoConstraints = NO; //創建一個約束創建器 MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; //在block中配置constraintMaker對象,即將constraintMaker傳入block中(其實就是我們在block中用來添加約束的make)進行約束配置 block(constraintMaker); //約束安裝並以數組形式返回 return [constraintMaker install]; } ...
-
約束安裝方法 [constraintMaker install]; 的源代碼如下,這部分的代碼很簡單,主要就是對當前約束創建器中的約束進行更新,因為除了我們這個 mas_makeConstraints 方法中會調用該方法之外, mas_updateConstraints 和 mas_remakeConstraints 中都會調用該方法進行約束的安裝,所以在該約束安裝方法中考慮了約束的刪除和是否有更新等情況的處理。
//install方法主要就是對下麵這個約束數組進行維護 @property (nonatomic, strong) NSMutableArray *constraints; - (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; }
- 上面這段代碼中真正添加約束的方法其實是 [constraint install]; ,這裡我們要分析一下這個install到底調用的是哪個方法的install?因為這裡有好幾個類(MASConstraint、MASViewConstraint、MASCompositeConstraint)有install方法。要知道具體調用的是哪一個類的install方法,我們就要弄清楚這裡的約束constraint到底是什麼類型,這就需要我們瞭解約束創建器(MASConstraintMaker)中的約束數組constraints中添加的到底是什麼類型的約束,經過分析(分析過程在後面會講到)我們發現這裡添加的約束是MASViewConstraint類型的,根據面向對象的多態特性,所以我們這裡調用的其實就是MASViewConstraint的install方法,該方法關鍵代碼(代碼太長,只放關鍵性代碼)如下,我們可以看到其實就是通過iOS系統自帶的自動佈局約束佈局類NSLayoutConstraint進行佈局。
- (void)install { if (self.hasBeenInstalled) { return; } ... //MASLayoutConstraint其實就是在NSLayoutConstraint基礎上添加了一個屬性而已 //@interface MASLayoutConstraint : NSLayoutConstraint 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 (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]; } }
通過上面的分析和研究,我們基本上已經把Masonry框架中主要佈局方法的主流程瞭解清楚了。因為這是第一次學習iOS第三方框架的源碼,在這個學習過程中也走了很多彎路,最開始是從最基本的類開始看,後來發現越看越不懂,不知道這個屬性的定義在什麼時候用到,是什麼含義((ノへ ̄、)捂臉。。。)。後來通過摸索才知道源碼學習應該直接從用到的方法著手,然後一步一步深入分析源碼中每一步的目的和意義,順藤摸瓜,逐個擊破。
2.2 Masonry框架中的鏈式語法
下麵的代碼是比較常用的幾種Masonry的佈局格式,我們可以看到都是通過點語法的鏈式調用進行佈局的。之前在學習Java和Android的過程中接觸過鏈式語法,在Java中要實現這種鏈式語法很簡單,無非就是每個方法的返回值就是其本身,因為Java的方法調用是通過點語法調用的,所以很容易實現。但是在OC中,方法調用都是通過 [clazz method:parm]; 的形式進行調用的,那麼Masonry框架中是怎麼實現的呢?
make.top.equalTo(sv).with.offset(10); make.left.right.mas_equalTo(sv).mas_offset(0.0f); make.top.left.bottom.and.right.equalTo(sv).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));
同樣的學習方法,我們來看一下源碼中各個屬性或方法是怎麼實現的,最重要的原因就是getter方法和Objective-C 裡面,調用方法是可以使用點語法的,但這僅限於沒有參數的方法。
- 首先,我們在用Masonry進行佈局的時候最先用MASConstraintMaker調用一個方位屬性(在MASConstraintMaker中定義了許多方位屬性進行初始化調用,具體有哪些如下MASConstraintMaker.h文件中所示),用點語法調用,例如 make.top ,這時候其實是調用了其getter方法,然後在getter方法中對該約束的代理進行設置(見下MASConstraintMaker.m文件中標紅註釋處)
//MASConstraintMaker.h文件 @interface MASConstraintMaker : NSObject @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; #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) @property (nonatomic, strong, readonly) MASConstraint *firstBaseline; @property (nonatomic, strong, readonly) MASConstraint *lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) @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; #endif @property (nonatomic, strong, readonly) MASConstraint *edges; @property (nonatomic, strong, readonly) MASConstraint *size; @property (nonatomic, strong, readonly) MASConstraint *center; ... @end //MASConstraintMaker.m文件 @implementation MASConstraintMaker //每個方法返回的也是MASConstraint對象,實際上是MASViewConstraint、MASCompositeConstraint類型的對象,見最後的函數中標紅的註釋 - (MASConstraint *)top { //將對應的系統自帶的約束佈局的屬性NSLayoutAttributeTop傳入 return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop]; } //過渡方法 - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; } //最終的調用 - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; ... //添加約束 if (!constraint) { //設置約束的代理是self newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } //返回MASViewConstraint類型的對象 return newConstraint; } ... @end
- 然後通過第一步的初始化之後返回的就是一個MASViewConstraint對象了,後面的點語法主要就在於MASViewConstraint中的屬性和方法了,在MASViewConstraint的.h和.m文件中我們都沒有找到top等方位相關的屬性或方法,但是我們發現MASViewConstraint是繼承自MASConstraint的,然後我們發現在MASConstraint中定義了大量的方位相關方法(如下代碼所示),所以類似 make.top.left 前面一個top是調用MASConstraintMaker的方法,後面一個left則是通過點語法調用MASConstraint的方法。但是為什麼這些方法可以進行點語法調用呢?原因就是在Objective-C 裡面,調用方法是可以使用點語法的,但這僅限於沒有參數的方法。
@interface MASViewConstraint : MASConstraint <NSCopying>
//MASConstraint.h文件 @interface MASConstraint : NSObject /** * Creates a new MASCompositeConstraint with the called attribute and reciever */ - (MASConstraint *)left; - (MASConstraint *)top; - (MASConstraint *)right; - (MASConstraint *)bottom; - (MASConstraint *)leading; - (MASConstraint *)trailing; - (MASConstraint *)width; - (MASConstraint *)height; - (MASConstraint *)centerX; - (MASConstraint *)centerY; - (MASConstraint *)baseline; #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) - (MASConstraint *)firstBaseline; - (MASConstraint *)lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) - (MASConstraint *)leftMargin; - (MASConstraint *)rightMargin; - (MASConstraint *)topMargin; - (MASConstraint *)bottomMargin; - (MASConstraint *)leadingMargin; - (MASConstraint *)trailingMargin; - (MASConstraint *)centerXWithinMargins; - (MASConstraint *)centerYWithinMargins; #endif ... @end
//MASConstraint.m文件 - (MASConstraint *)top { //這裡會調用MASViewConstraint中的addConstraintWithLayoutAttribute:方法 return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop]; } //MASViewConstraint.m文件 - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation"); //調用代理的方法,之前我們說過設置的代理是MASConstraintMaker對象make,所以調用的實際上是MASConstraintMaker添加約束的方法,這就是我們再上面第一步講到的方法 return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute]; } //MASConstraintMaker.m文件 - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; //當傳入的constraint不為空時,即此調用不是第一個,make.toip.left在left時的調用 if ([constraint isKindOfClass:MASViewConstraint.class]) { //則用MASCompositeConstraint作為返回值,即組約束 NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; //設置代理 compositeConstraint.delegate = self; //重新設置 [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; //返回 MASCompositeConstraint對象return compositeConstraint; } if (!constraint) { //設置代理 newConstraint.delegate = self; //設置約束 [self.constraints addObject:newConstraint]; } return newConstraint; }
2.3 鏈式語法中傳參方法的調用
在上一小節我們提到了鏈式語法的主要原因在於在Objective-C 裡面,調用方法是可以使用點語法的,但這僅限於沒有參數的方法,但是類似mas_equalTo、mas_offset等帶參數傳遞的方法依舊可以用鏈式語法又是怎麼一回事呢?最關鍵的一環就是 block。block就是一個代碼塊,但是它的神奇之處在於在內聯(inline)執行的時候還可以傳遞參數。同時block本身也可以被作為參數在方法和函數間傳遞。block作為參數傳遞很常見,就是在我們的Masonry框架中添加約束的方法 - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block 中就是講一個block作為參數進行傳遞的。
同樣在MASConstraint中,我們可以看到mas_equalTo、mas_offset等帶參方法的定義如下,我們可以看到,方法的定義中並沒有參數,但是返回值是一個帶參的block,並且該block還返回一個MASConstraint對象(MASViewConstraint或者MASCompositeConstraint對象),所以方法的定義和使用都沒有什麼問題,和上一小節分析的內容差不多。最主要的區別就是這裡返回值為帶參數的block,並且該block的參數可以通過我們的方法進行傳值。關於帶參block作為返回值得用法可以參見 此鏈接的文章。
- (MASConstraint * (^)(MASEdgeInsets insets))insets; - (MASConstraint * (^)(CGFloat inset))inset; - (MASConstraint * (^)(CGSize offset))sizeOffset; - (MASConstraint * (^)(CGPoint offset))centerOffset; - (MASConstraint * (^)(CGFloat offset))offset; - (MASConstraint * (^)(NSValue *value))valueOffset; - (MASConstraint * (^)(CGFloat multiplier))multipliedBy; - (MASConstraint * (^)(CGFloat divider))dividedBy; - (MASConstraint * (^)(MASLayoutPriority priority))priority; - (MASConstraint * (^)(void))priorityLow; - (MASConstraint * (^)(void))priorityMedium; - (MASConstraint * (^)(void))priorityHigh; - (MASConstraint * (^)(id attr))equalTo; - (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
//MASConstraint.m文件 - (MASConstraint * (^)(id))mas_equalTo { return ^id(id attribute) { //多態調用子類MASViewConstraint或者MASCompositeConstraint的對應方法 return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; } //MASViewConstraint.m中對應的方法,MASCompositeConstraint其實也類似,只是迴圈調用每一個子約束的該方法 - (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; } }; }
3 Masonry框架的整體運用的理解
進過上面的原理分析,大體理解了其調用和實現眼裡,接下來,我們通過下麵的這句代碼在容器中的演變過程來進行整體感受一下,下麵的演變過程來自:Masonry源碼學習,原文有幾處有點小問題修改過,大家參考的時候註意甄別和判斷。
make.top.right.bottom.left.equalTo(superview)
- make.top
- 生成對象A:MASViewConstraint(view.top)
- 將A的delegate設為make
- 將A放入make的constraints中,此時make.constraints = [A]
- 返回A
- make.top.right
- 生成對象B:MASViewConstraint(view.right)
- 使用A和B生成MASCompositeConstraint對象C,將C的delegate設為make
- 將make.constraints中替換成C,此時make.constraints = [C],C.childConstraints = [A,B]
- 返回C
- make.top.right.bottom
- 生成對象D:MASViewConstraint(view.bottom),將D的delegate為C
- 將D放入C.childConstraints中,此時C.childConstraints = [A,B,D]
- 返回C
- make.top.right.bottom.left
- 生成對象E:MASViewConstraint(view.left),E的delegate為C
- 將E放入C.childConstraints中,此時C.childConstraints = [A,B,D,E]
- 返回C
- make.top.right.bottom.left.equalTo(superview)
- 會依次調用A,B,D,E的equalTo(superView)
在上面的過程中可以看到:
- 對make.constraints的添加和替換元素的操作
- 對MASCompositeConstraint對象的添加元素的操作(當.equalTo(@[view1,view2])時就有替換操作了,在裡面沒體現出)。
- 每個constraint的delegate為它的父容器,因為需要父容器來執行添加和替換約束的操作。
4 Masonry框架的整體架構
盜用iOS開發之Masonry框架源碼解析中的一張圖,這張圖將Masonry框架的架構闡述的很清晰,Masonry框架主要分為4個部分:
- View+MASAdditions:最左邊的紅色框的這個類,這是Masonry框架最主要的一個類,主要是最下麵的四個添加和修改約束的方法
- MASConstraintMaker:中間綠色框中的這個類,這是Masonry框架中的過渡類,鏈接最左邊和最右邊之間的關係,也是鏈式語法的發起點和添加約束的執行點。
MASConstraintMaker
類就是一個工廠類,負責創建和安裝MASConstraint
類型的對象(依賴於MASConstraint
介面,而不依賴於具體實現)。 - 核心類:最右邊的黃色框的這個類群,這是Masonry框架中的核心基礎類群,這個類群又分為兩個部分:
- 約束類群:黃色框上面三個類,其中MASConstraint是一個抽象類,不可被實例化。我們可以將MASConstraint是對NSLayoutConstriant的封裝,看做是一個介面或者協議。MASViewConstraint和MASCompositeConstraint都繼承自MASConstraint,其中MASViewConstraint用於定義一個單獨的約束,而MASCompositeConstraint則用於定義一組約束條件,例如定義size、insert等參數時返回的其實都是MASCompositeConstraint。
- 屬性類群:主要是指MASViewAttribute,主要是對NSLayoutAttribute的擴展,方便我們進行約束定義和修改
- 附屬類群:還有一些工具類沒有在這張圖中進行展示,例如NSArray+MASAdditions、NSLayoutConstraint+MASDebugAdditions、MASLayoutConstraint等,都定義了一些工具和簡化方法。