Masonry鏈式程式設計思想的基本思路以及KVO底層的響應式程式設計

Deft_MKJing宓珂璟發表於2016-12-31

Masonry基本使用

[self.enterButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.moviePlayer.view);
        make.centerY.equalTo(self.moviePlayer.view);
        make.height.mas_equalTo(@(50));
        make.width.mas_equalTo(@(0));
    }];



我們平時已經用習慣了物件導向的程式設計思想,基本上理解為萬物皆物件的概念,那麼如果用Autolayout佈局的話,Masonry就肯定會用過,每個優秀的框架都有一套自己的程式設計思想,這就是經典的鏈式程式設計思想,既然都會用了,那麼學習下人家的程式設計思想也是非常好的提高。


上述程式碼思想是通過.符號把多行程式碼連結成一句程式碼,增強了可讀性,例如a(1).b(2).c(3)一系列操作,那麼在OC中要實現a(引數)這種呼叫方式,那麼必然就是Block了。沒錯,鏈式程式設計的方法呼叫返回的是Block,而且呼叫完Block之後必然會有返回值(該返回值就是操作物件本身),這樣才能鏈式呼叫,而且Block的引數就是需要內部物件操作的值

1.通過鏈式來實現簡單的計算器

從後面往前面推,先來看看我們最終需要的呼叫鏈式程式碼manager.add(10).add(20).add(30).add(40),只要這句程式碼,我們就能完成加法運算,一步一步來分析,首先呼叫的是manager.add(10),這是第一個加法運算區塊,這裡還能分兩個呼叫,第一個是manager.add,後面緊跟著是(10),這種呼叫方式必然是Block,因此add方法返回值就是一個Block,該block的引數就是括號裡面的值,那麼假定沒有返回值,呼叫完第一波區塊的時候,就不能再呼叫add 了,因此,該Block既有引數,又有返回值,返回值就是呼叫的物件

程式碼如下
1.建立CalculateManager物件和方法

#define 加法
#define 減法
#define 乘法
#define 除法
@interface CalculateManager : NSObject

// 內部用來儲存計算的結果
@property (nonatomic,assign) int result;

加法
- (CalculateManager *(^)(int num))add;
乘法
- (CalculateManager *(^)(int num))multiply;
減法
- (CalculateManager *(^)(int num))minus;
除法
- (CalculateManager *(^)(int num))div;



2.實現暴露出來的方法

- (CalculateManager *(^)(int num))add
{
    // 把block返回出去,在外面進行資料處理,呼叫完之後返回物件
    return ^CalculateManager *(int num){

        self.result += num;
        return self;
    };
}


- (CalculateManager *(^)(int))minus
{
    return ^CalculateManager *(int num){
        self.result -= num;
        return self;
    };
}

- (CalculateManager * (^)(int num))div{
    return ^CalculateManager *(int num){
        self.result /= num;
        return self;
    };
}

- (CalculateManager * (^)(int num))multiply{
    return ^CalculateManager *(int num){
        self.result *= num;
        return self;
    };
}



3.最基礎的呼叫

CalculateManager *manager = [[CalculateManager alloc] init];
    int result = [manager.add(10).add(20).add(30).add(40) result];
    NSLog(@"result = %d",result);



可以看到上面的基本呼叫已經完成了,雖然也是鏈式的,結果也有,但是總感覺和Masonry不一樣,OK,在來一層

@interface NSObject (Calculate)

+ (int)mkj_makeCalculates:(void(^)(CalculateManager *))block;

@end
+ (int)mkj_makeCalculates:(void(^)(CalculateManager *))block {

    CalculateManager *manager = [[CalculateManager alloc] init];
    block(manager);
    return manager.result;
}



4.封裝後的呼叫,現在看起來和Masonry有點像了

int result = [CalculateManager mkj_makeCalculates:^(CalculateManager *manager) {

        manager.add(10).add(10).add(10);
        manager.add(30).add(40);
        manager.div(2);
        manager.multiply(4).minus(12);

    }];
    NSLog(@"result111 = %d",result);



稍微概括下,首先呼叫Category的類方法,引數是Block,該Block根據實際呼叫地方的引數進行方法鏈式呼叫,Category裡面先建立一個計算物件,然後呼叫Block,把物件當做引數進行Block回撥,在外部進行一套鏈式呼叫,鏈式的思想是,物件的方法呼叫沒有引數返回值必須是Block,然後呼叫Block,Block的返回值就是物件,迴圈進行再次呼叫,最終內部傳參的物件把結果儲存在屬性欄位中,可以根據函式返回值返回出來,列印最終需要的結果

2.通過KVO研究下響應式程式設計思路

鏈式程式設計思想就是,不考慮呼叫順序,只在乎結果,事件分發出去,只要有人監聽,就能進行下一步操作,KVO就是這樣,稍微分析下

簡單的KVO如下

    // KVO內部原理
    CalculateManager *manager1 = [[CalculateManager alloc] init];
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager1,object_getClass(manager1));
    [manager1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager1,object_getClass(manager1));
    manager1.name = @"mikejing";
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager1,object_getClass(manager1));
    [manager1 removeObserver:self forKeyPath:@"name"];
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager1,object_getClass(manager1));
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@",change);
}

列印結果如下

2016-12-31 11:21:11.182 鏈式程式設計[43681:1390993] 列印類<CalculateManager: 0x60000002e800>,通過runtime獲取類資訊CalculateManager
2016-12-31 11:21:11.183 鏈式程式設計[43681:1390993] 列印類<CalculateManager: 0x60000002e800>,通過runtime獲取類資訊NSKVONotifying_CalculateManager
2016-12-31 11:21:11.183 鏈式程式設計[43681:1390993] {
    kind = 1;
    new = mikejing;
}
2016-12-31 11:21:11.184 鏈式程式設計[43681:1390993] 列印類<CalculateManager: 0x60000002e800>,通過runtime獲取類資訊NSKVONotifying_CalculateManager
2016-12-31 11:21:11.184 鏈式程式設計[43681:1390993] 列印類<CalculateManager: 0x60000002e800>,通過runtime獲取類資訊CalculateManager

這裡寫圖片描述


這裡寫圖片描述


注意看:在給物件新增觀察者之前,物件的class、runtime真實class和isa指標都是CalculateManager 物件,這很正常,但是增加觀察者之後呢,注意看,class還是CalculateManager ,但是isa指標和runtime的getclass暴露了一切,已經變了一個子類NSKVONotifying_CalculateManager 這裡好像在哪看到過,叫做門面設計模式,對外隱藏了一切類資訊,就連直接列印class都被重寫了,還是返回原本的父類,但是內在確確實實被動態改變了,當我們監聽的屬性改變的時候,其實就是監聽到set方法的呼叫,重寫動態建立那個子類的set方法,然後由子類通知觀察者,我已經被改變,呵呵,你該幹嘛幹嘛
這裡寫圖片描述


既然監聽的是屬性的set方法,那來驗證下

@interface CalculateManager : NSObject
// 暴露一個外部變數直接訪問
{
    @public
    NSString *_name;
}
// 然後把屬性的變化換個方法,直接訪問
//    manager1.name = @"mikejing";
    manager1->_name = @"mikejing";

// 然後這個觀察者就不會被呼叫了
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    // 不再被列印,說明沒監聽到,有興趣可以試試
    NSLog(@"%@",change);
}

3.模仿下KVO自己實現下響應式程式設計思路

回顧下KVO實現的思路
1.動態建立NSKVONotifying_類名,該類是原來那個類的子類,用來最終同時觀察者
2. 修改當前物件的isa指標,當呼叫set方法的時候先去建立的子類找,而且子類重寫了class方法,返回原來父類,來掩蓋犯罪證據
3.子類重寫set,當外部呼叫set,就會進來子類的方法
4.重寫set方法裡面先呼叫super set: 方法,然後通知觀察者

第一步,category一個方法

@interface NSObject (Calculate)

- (void)mkj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;



第二步動態修改類的isa指標,並且儲存觀察者物件和呼叫者繫結

- (void)mkj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    // 修改呼叫者的isa指標為動態建立的子類,能讓set方法改變的時候,去子類進行操作
    object_setClass(self, [MKJKVONotifying_CalculateManager class]);

    // 由於observr沒有任何儲存,子類set方法改變之後如何呼叫observe實現的方法?runtime
    // 動態給物件關聯一個屬性
    objc_setAssociatedObject(self, "hehe", observer, OBJC_ASSOCIATION_RETAIN);
}



第三步重寫class,隱藏子類,通過runtime拿出對應的觀察者,通知觀察者改變

- (Class)class
{
    return [CalculateManager class];
}

- (void)setName:(NSString *)name
{
    // 呼叫父類
    [super setName:name];
    // 根據關聯欄位拿出關聯的觀察者
    id obj = objc_getAssociatedObject(self, "hehe");
    // 子類最終通知呼叫觀察者的observer方法
    [obj observeValueForKeyPath:@"name" ofObject:self change:@{@"change":name} context:nil];
}



第四步外部呼叫自己實現的類似的KVO

 // 自己實現KVO
    CalculateManager *manager2 = [[CalculateManager alloc] init];
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager2,object_getClass(manager2));
    [manager2 mkj_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"列印類%@,通過runtime獲取類資訊%@",manager2,object_getClass(manager2));
    manager2.name = @"jiaojiao";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"%@",change);
}
// 列印資訊如下
2016-12-31 12:10:02.409 鏈式程式設計[44785:1429519] 列印類<CalculateManager: 0x60800003eba0>,通過runtime獲取類資訊CalculateManager
2016-12-31 12:10:02.410 鏈式程式設計[44785:1429519] 列印類<CalculateManager: 0x60800003eba0>,通過runtime獲取類資訊MKJKVONotifying_CalculateManager
2016-12-31 12:10:02.410 鏈式程式設計[44785:1429519] {
    change = jiaojiao;
}



通過上述列印可以看出,實際的呼叫是在子類進行的,而且,外部呼叫還只是能看到父類的資訊



上面的只是一些簡單的思路而已,記錄一下,內部實現其實還需要進一步學習,畢竟i love study,it make me happy,哈哈哈,其實思路這種東西,研究久了,只是提供了很多解決問題的方案和設計思路,專案中就能自然的想到解決方案,畢竟好的程式設計可讀性非常高,而且能做到牛B的高聚合,低耦合,讓讀程式碼的人深深感嘆,大哥你好牛B啊,就像讀SDWebImage,Masonry等優質框架,你肯定會覺得,臥槽,大哥,你好牛b啊,這個時候,如果你是作者,你就能淡定的丟擲:略懂,略懂!!!
這裡寫圖片描述

相關文章