1. 程式人生 > >iOS開發之Masonry框架原始碼解析

iOS開發之Masonry框架原始碼解析

Masonry是iOS在控制元件佈局中經常使用的一個輕量級框架,Masonry讓NSLayoutConstraint使用起來更為簡潔。Masonry簡化了NSLayoutConstraint的使用方式,讓我們可以以鏈式的方式為我們的控制元件指定約束。本篇部落格的主題不是教你如何去使用Masonry框架的,而是對Masonry框架的原始碼進行解析,讓你明白Masonry是如何對NSLayoutConstraint進行封裝的,以及Masonry框架中的各個部分所扮演的角色是什麼樣的。在Masonry框架中,仔細的品味乾貨還是很多的。Masonry框架是Objective-C版本的,如果你的專案是Swift語言的,那麼就得使用SnapKit

佈局框架了。SnapKit其實就是Masonry的Swift版本,兩者雖然實現語言不同,但是實現思路大體一致。

今天部落格對Masonry框架原始碼的解析思路是先對比給一個View新增同樣的約束時,使用Masonry與系統原生的區別。然後就開門見山之間給出Masonry框架主要部分的類圖,從類圖中我們來整體的分析Masonry框架的結構。然後再由整體到部分逐漸的細化,窺探其內部的實現細節。通過上述步驟,我們將對Masonry框架的內部實現進行詳細的瞭解。其實Masonry框架是輕量級的,總共的原始碼也沒有多上行,但是仔細的閱讀其實現細節,還是可以吸取很多實用的東西的。

首先Masonry在github上的地址是 你可以通過上述連結Clone到Masonry框架,其中有Masonry框架介紹以及一些Masonry的使用示例。關於Masonry的使用方式在今天的部落格中就不做過多的贅述了,其具體的使用方式請參考上述github上的連結。今天我們就剖析一下Masonry框架的原始碼。

一、Masonry框架與NSLayoutConstraint呼叫方式的對比

首先我們NSLayoutConstraint為我們的View新增一個約束,然後再給出Masonry的程式碼。當然在此我們就不說Masonry新增約束的簡潔行了,當然好東西是不需要宣傳的。進入該部分的主題,我們要對一個View新增一個top約束,這個約束關係我們用表示式來表示就是“subView.top = superView.top + 10”。也就是子檢視的top與父檢視的top中間隔著10個pt。

1. 使用NSLayoutConstraint新增約束

下方這段程式碼就是給subView添加了一個相對於superView的Top約束。一個View要想確定位置一個約束是不夠的,所以可想而知,我們要寫多個下方的這樣的約束來確定一個View的相對位置。其實下方就是一個表示式,NSLayoutConstraint構造器中每個引數構成這個表示式的一個組成部分。由上到下我們隊引數個個引數進行解析,引數constraintWithItem用來指定所約束的物件,在此就是subView。第一個attribute引數則指定約束該物件的那個屬性,在此就是subView的Top屬性。引數relatedBy用來指定約束關係,比如大於等於,小於等於或者等於某個約束值。引數toItem則指定的是約束相對的物件,在此是相對superView的,所以此處的引數是superView。第二個attribute引數就是指定superView的Top屬性。multiplier指定相對約束的倍數關係,constant則是約束的偏移量。

由上到下,NSLayoutConstraint的構造器中的引數會構成一個數學表示式,那就是subView.top = superView.top * 1 + 10,該表示式就直觀的給出了subView.topsuperView.top的關係。經下方的程式碼我們就為subView添加了一個相對於superView的Top約束,約束的偏移量是10。

 2.使用Masonry新增上述約束

接下來就是Masonry出場的時刻了,我們將使用Masonry新增上述約束,其程式碼如下。下方給出了三種設定方式,下方三種方式是等價的,當然在Masonry中不知下方三種實現方式。下方Block中的每句話都代表著subView.top = superView.top * 1 + 10的意思,也就是說我們只需要寫這三行程式碼中的其中一種即可。使用Masonry的好處一目瞭然,讓你的程式碼更為簡潔。

Masonry框架中支援約束的新增,約束的更新,約束的重建以及基本動畫的實現等等。功能還是蠻強大的。在Masonry框架主要中採用了鏈式呼叫和匿名閉包的方式來簡化約束的新增。有關Masonry更為詳細的使用方式請參見上述Masonry框架的Github連結,具體使用方式在此就不做過多的贅述了。

二、Masonry框架的類結構

通過上述的Masonry的使用方式我們可以看出,UIView的物件可以直接呼叫mas_makeConstraints方法來為相應的View物件新增約束。因為mas_makeConstraints方法位於UIView的View+MASAdditions類目中,所以UIView的物件可以直接呼叫。同樣在View+MASAdditions類目還有其他方法供UIView的物件使用,稍後會進行詳細的介紹。

下方就是Masonry框架核心類以及類目之間的關係,下方的類圖是在閱讀Masonry原始碼時畫的,僅此一份,如有雷同純屬巧合。如果下圖中的文字比較小的話,你可以圖片另存到本地,然後放大後進行檢視,廢話少說,進入我們類圖的主題。下方的類圖中沒有包括Masonry框架中的所有的類,不過所有核心的類都在下方了。我們從左往右依次對下方的類圖進行解說。

1.View+MASAdditions類目介紹(左邊紅框中的部分)

最左邊那一坨大類,也就是綠框中的部分,就是Masonry框架對UIView的公有類目,也就是原始檔中的View+MASAdditions的部分,在該類目中為添加了型別為MASViewAttribute的成員屬性(稍後會介紹MASViewAttribute是個神馬東西)。除了新增一系列的成員屬性外,還添加了四個公有的方法:mas_closestCommonSuperview方法負責尋找兩個檢視的最近的公共父檢視(類比兩個數字的最小公倍數)、mas_makeConstraints方法負責建立安裝約束、mas_updateConstraints負責更新已經存在的約束(若約束不存在就Install)、mas_remakeConstraints方法則負責移除原來已經建立的約束並新增上新的約束。上述方式是UIView物件設定約束主要呼叫的方法,稍後會詳細介紹其實現方式。

2.MASViewAttribute類的介紹(右邊黃框中的部分)

介紹完使用者直接使用的UIView的公共類目,接下來我們來看一下使用者看不到的部分,那就是下方類圖中右邊的那一撮類。右邊的四個小類的耦合性比較高,我們先看一下MASViewAttribute類。MASViewAttribute類的結構比較簡單,主要包括三個屬性,三個方法。從MASViewAttribute這個類名中我們就能看出,這個類是對UIView和NSLayoutAttribute的封裝。使用等式來表示就是MASViewAttribute = UIView + NSLayoutAttribute + item。在MASViewAttribute類中的view屬性表示所約束的物件,而item就是該物件上可以被約束的部分。

此處的item成員屬性我們稍後要作為NSLayoutConstriant構造器中的constraintWithItem與toItem的引數。當然對於UIView來說該item就是UIView本身。而對於UIViewController,該出Item就topLayoutGuide,bottomLayoutGuide稍後會給出詳細的介紹。該類中除了兩個構造器外還有一個isSizeAttribute方法,該方法用來判斷MASViewAttribute類中的layoutAttribute屬性是否是NSLayoutAttributeWidth或者NSLayoutAttributeHeight,如果是Width或者Height的話,那麼約束就新增到當前View上,而不是新增在父檢視上。

3.MASViewConstraint的介紹(右邊黃框中的部分)

接著我們看一下MASViewConstraint類,該類是對NSLayoutConstriant類的進一步封裝。MASViewConstraint做的最核心的一件事情就是初始化NSLayoutConstriant物件,並將該物件新增在相應的檢視上。因為NSLayoutConstriant在初始化時需要NSLayoutAttribute和所約束的View,而MASViewAttribute正是對View與NSLayoutAttribute進行的封裝,所以MASViewConstraint類要依賴於MASViewAttribute類,兩者的關係如下所示。

由下方的類圖我們可以看出MASConstraint是MASViewConstraint的父類,MASConstraint是一個抽象類,不可被例項化。我們可以將MASConstraint看做是一個介面或者協議。MASConstraint抽象類還有一個子類,也就是MASViewConstraint的兄弟類MASCompositeConstraint,從MASCompositeConstraint的命名中我們就可以看出來MASCompositeConstraint是約束的一個組合,也就是其中儲存的是一系列的約束。MASCompositeConstraint類的結構比較簡單,其核心就是一個儲存MASViewConstraint物件的陣列,MASCompositeConstraint就是對該陣列的一個封裝而已。

4.工廠類MASConstraintMaker(中間綠框中的部分)

兩邊的看完了,接下來我們來看一下中間的部分,也就是MASConstraintMaker類。該類就是一個工廠類,負責建立MASConstraint型別的物件(依賴於MASConstraint介面,而不依賴於具體實現)。在UIView的View+MASAdditions類目中就是呼叫的MASConstraintMaker類中的一些方法。上述我們在使用Masonry給subView新增約束時,mas_makeConstraints方法中的Block的引數就是MASConstraintMaker的物件。使用者可以通過該Block回撥過來的MASConstraintMaker物件給View指定要新增的約束以及該約束的值。該工廠中的constraints屬性陣列就記錄了該工廠建立的所有MASConstraint物件。

Masonry框架中的核心類以及類目間的關係就介紹完了,下方就是核心類和類目的類圖。下方將會逐步的窺探其程式碼實現。 

三、View+MASAdditions原始碼解析

我們先對UIView的公共類目View+MASAdditions中的原始碼進行解析,也就是對應著上方紅框中的部分。使用者是通過 View+MASAdditions中的東西來為View新增約束的,View+MASAdditions也就是Masonry框架與外界互動的通道。該部分主要對View+MASAdditions原始碼進行解析,先介紹其成員屬性,然後介紹主要的方法。進入該部分的主題。

1.View+MASAdditions主要成員屬性及getter方法

下方截圖中是View+MASAdditions類目中的部分成員屬性,其他的也與下方類似,這些屬性都是MASViewAttribute型別的。以下方的mas_left成員屬性為例,因為MASViewAttribute是View與NSLayoutAttribute的合體,所以mas_left就代表著當前View的NSLayoutAttributeLeft屬性,也就是mas_left儲存的是當前View的NSLayoutAttributeLeft屬性。同理,mas_top就代表著當前View的NSLayoutAttributeTop屬性,其他成員屬性也是一樣。

     

通過上述成員屬性所對應的getter方法,我們可以對其中所儲存的內容一目瞭然。下方是mas_left、mas_top和mas_right成員屬性所對應的getter方法,其中所做的事情就是對MASViewAttibute進行例項化,在例項化時指定當前檢視所對應的LayoutAttribute。也就是mas_left = self + NSLayoutAttributeLeft, mas_top = self +NSLayoutAttributeTop, 當然此處的self就代表當前檢視。

    

2.mas_makeConstraints方法解析

上面在介紹類圖的時候也提到了,使用者是通過呼叫mas_makeConstraints方法來為當前檢視新增約束的。下方程式碼就是mas_makeConstraints函式的程式碼實現,根據個人理解,對每行程式碼進行了中文註釋,接下來我們來好好的看一下該函式的結構.mas_makeConstraints方法的返回值是一個數組(NSArray),陣列中所存放的就是當前檢視中所新增的所有約束。因為Masonry框架對NSLayoutConstraint封裝成了MASViewConstraint,所有此處陣列中儲存的是MASViewConstraint物件。

接下來來看mas_makeConstraints的引數,mas_makeConstraints測引數是一個型別為void(^)(MASConstraintMaker *)的匿名Block(也就是匿名閉包),該閉包的返回值為Void, 並且需要一個MASConstraintMaker工廠類的一個物件。該閉包的作用就是可以讓mas_makeConstraints方法通過該block給MASConstraintMaker工廠類物件中的MAConstraint屬性進行初始化。請參加下方block的使用。

mas_makeConstraints方法體中,首先將當前View的translatesAutoresizingMaskIntoConstraints屬性設定成No, 然後建立了一個MASConstraintMaker工廠類物件constraintMaker,然後通過block將constraintMaker物件回撥給使用者讓使用者對constraintMaker中的MAConstraint型別的屬性進行初始化。換句話說block中所做的事情就是之前使用者設定約束是所新增的程式碼,比如make.top(@10) == ( constraintMaker.top = 10 )。最後呼叫constraintMaker的install方法對使用者指定的約束進行安裝。

  

3.mas_updateConstraints與mas_remakeConstraints函式的解析

這兩個函式內部的實現與mas_makeConstraints類似,就是多了一個屬性的設定。mas_updateConstraints中將constraintMaker中的updateExisting設定為YES, 也就是說當新增約束時要先檢查約束是否已經被安裝了,如果被添加了就更新,如果沒有被新增就新增。而mas_remakeConstraints中所做的事情是將removeExisting屬性設定成YES, 表示將當前檢視上的舊約束進行移除,然後新增上新的約束。

  

  

4、mas_closestCommonSuperview方法解析

mas_closestCommonSuperview方法負責計算出兩個檢視的公共父檢視,這個類似求兩個數字的最小公倍數。下方的程式碼就是尋找兩個檢視的公共父檢視,當然是最近的那個公共父檢視。如果找到了就返回,如果找不到就返回nil。尋找兩個檢視的公共父檢視對於約束的新增來說是非常重要的,因為相對的約束是新增到其公共父檢視上的。比如舉個列子 viewA.left = viewB.right + 10, 因為是viewA與viewB的相對約束,那麼約束是新增在viewA與viewB的公共父檢視上的,如果viewB是viewA的父檢視,那麼約束就新增在viewB上從而對viewA起到約束作用。

  

四、順藤摸瓜,解析約束工廠類MASConstraintMaker

上一個部分我們分析了View+MASAdditions類目,在該類目中主要使用到了約束的工廠類MASConstraintMaker,接下我們就來窺探一下MASConstraintMaker中的內容。MASConstraintMaker之所以成為約束工廠類,因為MASConstraintMaker賦值建立NSLayoutConstraint物件,因為Masonry將NSLayoutConstraint類進一步封裝成了MASViewConstraint,所以MASConstraintMaker是負責建立MASViewConstraint的物件,並呼叫MASViewConstraint物件的Install方法將該約束新增到相應的檢視中。

1.MASConstraintMaker中的核心公有屬性。

下方截圖是MASConstraintMaker中的部分屬性,可以看出下方的屬性都是MSAConstriant型別,MSAConstriant是抽象類,所以下方成員變數儲存的實質上是MSAConstriant子類MASViewConstraint的物件。MASConstraintMaker就負責對MASViewConstraint進行例項化。一句話解釋MASViewConstraint,MASViewConstraint = View + NSLayoutConstraint + Install。稍後會給出MASViewConstraint具體技術細節的實現。在MASConstraintMaker還有一個私有陣列constraints,該陣列就用來記錄以及建立的Constraint物件。

  

2.MASConstraintMake中的工廠方法解析

工廠類肯定有工廠方法,接下來我們來介紹MASConstraintMaker中的工廠方法方法,上面每個MASConstraint型別的屬性都對應一個getter方法,在getter方法中都會呼叫addConstraintWithLayoutAttribute方法,而addConstraintWithLayoutAttribute會呼叫第二個截個圖中的方法,而截圖中的這個方法就是MASConstraintMaker工廠類的工廠方法,根據提供的引數建立MSAViewConstraint物件,如果該函式的第一個引數不為空的話就會將新建立的MSAViewConstraint物件與引數進行合併組合成MASCompositeConstraint類(MASCompositeConstraint本質上是MSAViewConstraint物件的陣列)的物件。

   

下方就是MASConstraintMaker工廠類的工廠方法,負責建立MASConstraint類的物件。下方的方法可以建立MASCompositeConstraintMASViewConstraint物件,上面也說了,MASCompositeConstraint物件就是MASViewConstraint物件的陣列。下方建立完MASConstraint類的相應的物件後,會把該建立的物件新增進MASConstraintMaker工廠類的私有constraints陣列,來記錄該工廠物件建立的所有約束。newConstraint.delegate = self這句話是非常重要的,由於為MASConstraint物件設定了代理,所以才支援鏈式呼叫(例如:maker.top.left.right.equalTo(@10))。

關於鏈式呼叫咱就以maker.top.left.right為例。此處的maker, 就是我們的MASConstraintMaker工廠物件,maker.top會返回帶有NSLayoutAttributeTop屬性的MASViewConstraint類的物件,我們先做一個轉換:newConstraint = maker.top。那麼maker.top.left 等價於newConstraint.left,需要注意的是此刻呼叫的left方法就不在是我們工廠MASConstraintMaker中的left的getter方法了,而是被換到MASViewConstraint類中的left屬性的getter方法了。給newConstraint設定代理就是為了可以在MASViewConstraint類中通過代理來呼叫MASConstraintMaker工廠類的工廠方法來完成建立。下方程式碼如果沒有newConstraint.delegate = self;代理的設定的話,那就不支援鏈式呼叫。

說了這麼多,總結一下,如果你呼叫maker.top, maker.left等等這些方法都會呼叫下方的工廠方法來建立相應的MASViewConstraint物件,並記錄在工廠物件的約束陣列中。之所以能鏈式呼叫,就是講當前的工廠物件指定為MASViewConstraint物件的代理,所以一個MASViewConstraint物件就可以通過代理來呼叫工廠方法來建立另一個新的MASViewConstraint物件了,此處用到了代理模式。

     

3. 工廠類中的install方法

雖然我們將MASConstraintMake視為工廠類,不過該工廠類的功能不僅僅建立MASConstraint的物件,還負責呼叫MASConstraint物件的install方法來將相應的約束安裝到想要的檢視上。在MASConstraintMake類中的install方法就是遍歷工廠物件所建立所有約束物件並呼叫每個約束物件的install方法來進行約束的安裝。下方就是該工廠類中的install方法。

在安裝約束時,如果self.removeExisting == Yes, 那麼使用者就通過mas_remakeConstraints方法呼叫的install方法,就先將原來的約束進行移除掉,然後新增上新的約束。在安裝約束時,將updateExisting賦值給每個約束,每個約束在呼叫本身的install方法時會判斷是否更新。下方就是MASConstraintMake的install方法的實現和註釋。

  

五、繼續順藤摸瓜,解析MASViewConstraint

MASConstraintMaker工廠類所建立的物件實質上是MASViewConstraint類的物件。而MASViewConstraint類實質上是對MASLayoutConstraint的封裝,進一步說MASViewConstraint負責為MASLayoutConstraint構造器組織引數並建立MASLayoutConstraint的物件,並將該物件新增到相應的檢視中。接下來我們將對MASViewConstraint類中的內容進行解析。

1.MASViewConstraint的物件鏈式呼叫探索

MASViewConstraint的物件是支援鏈式呼叫的,比如constraint.top.left.equalTo(superView).offset(10); 上面的這種方式就是鏈式呼叫,而且像equalTo(superView)這種形式也不是Objective-C中函式呼叫的方式,在Objective-C中是通過[]來呼叫函式的,而此處使用了()。接下來講分析這種鏈式的呼叫是如何實現的。

在MASViewConstraint類中的left, top等約束的getter方法都會呼叫下方的這個方法,而這個方法中所做的事情就是通過代理來呼叫工廠中的工廠方法來根據LayoutAttribute建立相應的MASConstraint物件。

    

而像offset(10)這種呼叫方式是如何實現的呢?我們知道在OC中是不能通過小括號來呼叫方法的,那邊閉包是可以的,不過offset()不是一個簡單的閉包。在offset()的程式碼分析後我們不難發現offset() = offset + (); offset的程式碼實現方式如下。offset是一個getter方法的名,offset函式的返回值是一個匿名Block, 也就是offset後邊的()。這個匿名閉包有一個CGFloat的引數,為了支援鏈式呼叫該匿名閉包返回一個MASConstraint的物件。

  

2.install方法解析

MASViewConstraint中install方法負責建立MASLayoutConstraint物件,並且將該物件新增到相應的View上。下方程式碼就是install中根據MASViewConstraint所收集的引數來建立NSLayoutConstraint物件,下方的MASLayoutConstraint其實就是NSLayoutConstraint的別名。下方就是呼叫系統的NSLayoutConstraint為建立相應的約束物件,下方的構造器與第一部分中的NSLayoutConstraint一致。

  

建立完約束物件後,我們要尋找該約束新增到那個View上。下方的程式碼段就是獲取接收該約束物件的檢視。如果是兩個檢視相對約束,就獲取兩種的公共父檢視。如果新增的是Width或者Height,那麼久新增到當前檢視上。如果既沒有指定相對檢視,也不是Size型別的約束,那麼就將該約束物件新增到當前檢視的父檢視上。程式碼實現如下:

    

建立完約束物件,並且找到承載約束的檢視後,接下來就是將該約束新增到該檢視上。子啊新增約束是我們要判斷是不是對約束的更新,如果是對約束的更新的話就先獲取已經存在的約束並對該約束進行更新,如果被更新的約束不存在就進行新增。新增成功後我們將通過mas_installedConstraints屬性記錄一下本安裝的約束。mas_installedConstraints是通過執行時為UIView關聯的一個NSMutable型別的屬性,用來記錄約束該檢視的所有約束。

  

3.UIView的私有類目UIView+MASConstraints

MASViewConstraint中定義了一個UIView的私有類目UIView+MASConstraints,該類目的功能為UIView通過執行時來關聯一個NSMutableSet型別的mas_installedConstraints屬性。該屬性中記錄了約束該View的所有約束。程式碼實現如下。

因為篇幅有限,今天的部落格就先到這兒。對Masonry框架中的程式碼不可能在本篇部落格中都進行一一介紹。不過在github上分享了一個Masonry的一個使用Demo以及原始碼解析的工程。其中對Masonry的關鍵程式碼都進行了說明與註釋。下方是其github分享連結。