1. 程式人生 > >iOS Masonry原始碼架構解讀一

iOS Masonry原始碼架構解讀一

原始碼思想解讀系列一:Masonry

本文由本人原創,轉載請註明。
更多詳細程式碼請移步我的github:PeipeiQ

在網上能夠找到很多關於一些熱門庫的用法和詳細解讀,所以也就能很容易並且快速的解讀其中的原理。

本倉庫更專注於這些庫中一些ios寫法的技巧,以及一些架構思想設計的總結。如果想要從頭到位詳細瞭解原始碼的實現,可以重新查詢資料,或者在本倉庫的原始碼中找到 一些關於熱門庫的註釋進行解讀。

一、鏈式語法的使用。

  其實就是get方法與block的結合。有些人說這種寫法不好記住,我們進行一個逆推。要想實現objc.aProperty這種語法,就要使用到get方法,返回一個aProperty的例項。要想實現someOperate(para)

這種語法,則需要使用block。兩者結合一下,定義一個block屬性並實現他的get方法,則可以實現objc.aProperty(para)這種形式。而鏈式程式設計的做法,比如objc.aProperty(para).aProperty(para)...則只需要在block
中return一個自身的例項即可。
以下通過一個例子舉例。

typedef NSNumber* (^addBlcok)(int);
//nsnumber擴充套件
@interface NSNumber (num)
-(addBlcok)add;
@end

@implementation NSNumber (num)
-(addBlcok)add{ addBlcok addblock = ^(int a){ return @([self intValue]+a); }; return addblock; } @end //鏈式語法 NSNumber *num = @(20); NSNumber *res = num.add(120); //輸出@140

一些思考:

1.什麼時候使用鏈式程式設計?
  從上面的例子還有Masonry可以看出,在面向一些過程化處理的時候(拼接SQL、給View加約束,都可以看成需要一步步完成的過程),需要將這些“過程”拆分,然後在“組合”這些“過程”的時候,就可以使用鏈式程式設計,使得程式碼更加清晰,增加閱讀性。

2.鏈式程式設計的核心實現
  實現鏈式程式設計的關鍵就是宣告一個block的屬性,而這個block返回值必須還是一個物件(根據業務需求不同,可以返回的是這個物件例項本身,也可以是這個類的另一個例項,更可以是另一個類的例項物件)。而block中內部的邏輯就是專案的業務邏輯。

二、抽象基類的運用

  大部分的OOP語言都有明確一種抽象基類的寫法。Object-C中沒有明確的抽象基類。於是我們可以採用一些型別判斷去幫助我們建立一個抽象基類去使用。
Masonry中MASConstraint是一個很典型的抽象基類,裡面聲明瞭許多有關於NSLayoutConstraint的屬性,並且聲明瞭一些基類方法,並且不提供實現。我們
進入原始碼看這一過程。
在初始化方法中,通過斷言,不提供init方法的實現,也就是說無法獲得一個MASConstraint型別的例項。

- (id)init {
    //抽象基類,不能直接被例項化
    NSAssert(![self isMemberOfClass:[MASConstraint class]], @"MASConstraint is an abstract class, you should not 
  instantiate it directly.");
    return [super init];
}

一些自身的例項方法,通過一個巨集來拒絕本身例項去實現它的方法。

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

- (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); }

- (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); }

- (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); }

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

...

- (void)uninstall { MASMethodNotImplemented(); }

關於抽象基類和類族,其實在oc的一些系統類中有很好的運用。最常見的可能就是NSArray,NSArray作為一個抽象基類,本身不會去實現一些陣列的方法例如
objectAtIndex:等等,它會根據你的初始化的資料而產生不同的例項。

NSArray *array0 = [NSArray new];  
NSArray *array = @[@1,@2,@3];  
NSMutableArray *array2 = [[NSMutableArray alloc]init];  
NSMutableArray *array3 = [NSMutableArray arrayWithObjects:@2,@3, nil];  
Class z = object_getClass(array0);    //輸出__NSArrayO  
Class a = object_getClass(array);    //輸出__NSArrayI  
Class b = object_getClass(array2);    //輸出__NSArrayM  
Class c = object_getClass(array3);    //輸出__NSArrayM  

從而去隱藏一些私有API的實現細節。關於類族的一些討論,可以移步我的這一篇部落格,有更加詳細的討論。
由objectAtIndex引發的陣列越界的思考

三、架構思考

  關於Masonry的一些方法的組織,這裡簡要梳理一下。並且貼出一些自己的思考。先看一下總的標頭檔案,這裡介紹了各個類的功能。

#import "MASUtilities.h"            ->masonry的一些公共的工具類
#import "View+MASAdditions.h"       ->view的類擴充套件,常用的方法就是定義在這裡
#import "View+MASShorthandAdditions.h".   ->Shorthand view additions without the 'mas_' prefixes,
#import "ViewController+MASAdditions.h".  
#import "NSArray+MASAdditions.h"         ->一組views進行約束,沒什麼特別
#import "NSArray+MASShorthandAdditions.h"   ->同上
#import "MASConstraint.h"                ->Constraint的抽象基類。其子類有MASViewConstraint和MASCompositeConstraint
#import "MASCompositeConstraint.h"       ->包裝多個MASViewConstraint例項。(例如size和center)
#import "MASViewAttribute.h"             ->用來包裝一個view的功能類,讓一個view和NSLayoutAttribute產生關聯
#import "MASViewConstraint.h"            ->包裝一些用於設定NSLayoutConstraint的屬性
#import "MASConstraintMaker.h"           ->顧名思義,用來製造Constraint
#import "MASLayoutConstraint.h"          ->NSLayoutConstraint的子類,僅僅添加了一個mas_key,作為標誌屬性
#import "NSLayoutConstraint+MASDebugAdditions.h"

入口很簡單,通過一個類擴充套件

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    //將view的translatesAutoresizingMaskIntoConstraint關掉
    self.translatesAutoresizingMaskIntoConstraints = NO;
    //初始化一個maker並將view傳進去
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    //通過block去設定constraintMaker。一般一條語句對應一個或者兩個MASLayoutConstraint。然後儲存在一個可變陣列@property (nonatomic, strong) NSMutableArray *constraints;
    block(constraintMaker);
    //取出constraintMaker中的constraints,遍歷後根據constraint生成MASLayoutConstraint並新增至每個檢視的約束。
    return [constraintMaker install];
}

關鍵方法有兩個,block(constraintMaker)和constraintMaker install。在block中,我們通常會去設定一些layout的屬性,然後儲存在一個可變陣列中,最終通過install方法去完成約束的新增。

簡單總結:

這種鏈式語法可以避免我們寫過多的膠水程式碼,增強了程式碼的可讀性,其實系統的NSLayoutConstraint確實不友好,導致我們一個view的約束會寫出這一串程式碼:

[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],

 ]];

其中確實有太多屬性我們要一直重複去寫,而且attribute的列舉名詞過長,導致寫起來也十分冗雜,所以鏈式語法的簡介性也就體現出來。其實在使用系統的NSLayoutConstraints還是極容易出現佈局錯誤導致app崩潰。所以masonry也做了很多邊界值處理和防止重複layout的機制。 其實masonry的原始碼要封裝的系統的方法不多,可以說看了所有程式碼就發現關鍵方法也就封裝了這裡。

//使用系統NSLayoutConstraint的方法(相當於二次封裝)
    //通過一個for迴圈,每有一個NSLayoutConstraint就install一次。
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

    //MASLayoutConstraint這個類就是為了新增一個屬性mas_key,在debug中可以使用到,對具體工程幫助不大。
    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];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, 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];
    }

其他上萬行程式碼都是為了這個方法服務的。這也就看出oc程式碼的一個靈活性。masonry的原始碼不難看懂,而且程式碼量比較少,所以還是比較容易瞭解它的核心。
關鍵還是要想得懂那些block方法的回撥。用block作為返回值,其實就是把一個函式作為返回值,這種做法在js中比較常見,而且js中寫法也更加簡潔易懂。後面有機會
會寫一寫這兩方面的對比。