(轉載)鏈式檔案生成器原理分析(一)

weixin_34208283發表於2017-04-19

此文章由熱心網友 ccSundayChina授權轉載

在OC裡面實現鏈式程式設計,可以使用返回撥用者自身來實現。但是類有很多,每個類也有很多方法,假如要實現鏈式程式設計,則需要每一個方法進行命令與實現,工作量之大可想而知。

事實是雖然工作量巨大,但是卻充滿了吸引力。

之前見過有人將UIKitFoundation框架中的大部分類一個個的都通過手工的方式新增上了鏈式程式設計的功能。洋洋灑灑的寫了很多,不得不佩服這種愚公精神,然而仔細看程式碼的話會發現其實有很多一樣的地方,而這些一樣的地方是可以提煉出來的。

懶惰是一個程式設計師的美德。

今天要來介紹一個非常有意思的框架,這個框架會幫助我們自動的生成那些鏈式檔案,不需要我們有多勤奮,挨個的去寫實現,只需要我們傳入我們想要進行轉換的類名陣列就可以了。

首先老規矩,框架地址

關於怎麼使用可以具體看作者的readme,裡面講的比較詳細了,然而就像作者其他的作品一樣,程式碼裡面註釋依然是少得可憐。筆者也是斷斷續續的看了兩個星期,才對這個程式碼的大致流程梳理的比較清晰了。

下面是筆者整理的程式流程圖,水平有限,不準確之處還望指出。

2200314-4bc1b93dc3a7bac4.png
part1-整體流程圖.png

流程圖中可以看出,當我們輸入我們的類陣列後,首先會進到一個迴圈裡去,如果這個類是OC類的話那麼就會被加入到新的陣列中,並且也會將它的父類加入到新陣列中,總之第一步就是對要處理的類進行處理。

然後輸出一個NSMutableSet,我們要生成的鏈式類,就是這裡面的。

緊接著就是開始根據類生成鏈式檔案了,並寫到了mac的桌面上。並且生成的檔案主要是兩類,一類是MLChainObject型別的,另一類是Object+MLChain型別的。這兩類的作用不同,但又是互相緊密聯絡的。

由於程式碼量巨大,邏輯複雜,所以在一章篇幅裡是很難將它解釋的明白的。(也可以解釋,但可能會丟掉好多有營養的東西)。今天我們主要看看在mac桌面生成普通鏈式檔案的大致過程。

還是先看下筆者畫的流程圖。(強烈建議原作者做一點程式碼的解釋工作。。。)

2200314-5b0efd415c43a7e0.png
生成普通的鏈式檔案.png

可以看到我們將處理後的類名陣列傳進去,就會以類名作為條件生成對應的標頭檔案(.h)、實現檔案(.m)、橋樑類名、以及父類名。然後根據這些條件建立我們專門用來生成程式碼檔案的模型CodeModel,最後通過NSFileManager檔案管理工具,將檔案的程式碼字串寫到mac桌面檔案中。

        [[NSFileManager defaultManager] writefileString:model.hFileResultString ToFileWithDiretory:XcodeCreateCodeDirectory fileName:chainClassName fileType:kML_CreateCodeFileType_h moveToTrashWhenFileExists:YES];
        
        [[NSFileManager defaultManager] writefileString:model.mFileResultString ToFileWithDiretory:XcodeCreateCodeDirectory fileName:chainClassName fileType:kML_CreateCodeFileType_m moveToTrashWhenFileExists:YES];

下面來看一下普通鏈式標頭檔案(.h檔案)是如何生成的,還是看流程圖:

2200314-e87c8d1edda81f71.png
生成普通.h檔案流程圖.png

首先判斷該類是不是NSObject基類,然後對應生成不同的屬性,屬性的目的是為了獲取當前鏈式物件所繫結的的原生物件。同時運用runtime獲取傳入類的所有方法,並對方法進行過濾,主要就是去掉私有方法、轉換set、get方法、過濾鏈式方法等,然後開始迴圈遍歷方法陣列中的每一個方法,運用介面卡將方法都轉換成鏈式方法,其實主要就是加了一個字首,用以區別。其中比較難理解的就是生成鏈式巨集定義(ChainMacroDefines)的過程,生成了鏈式的巨集定義,然後再將該巨集定義與諸如(MLChainClass*(^)())chainSelName的鏈式方法進行拼接。比如這樣:

#ifndef numberOfLines                                  
#define numberOfLines(...)  numberOfLines(@"setNumberOfLines:", (long long)metamacro_at(0, __VA_ARGS__))                                  
#endif
/**     ClassName-> UILabel                                
SEL:   setNumberOfLines: 'q'
     */
- (MLChain4UILabel *(^)())numberOfLines;

按照此種方式對該類所有的方法都進行處理,並加入到methodAndMacro陣列中,等遍歷完該類中的所有的方法時,就將該陣列中的所有元素進行拼接,返回該類所有符合條件方法的巨集定義與對應的鏈式方法名的拼接字串。

到這裡.h檔案的主要內容基本就完成了。然後就是再讓它拼接屬性字串,以構造完整的content。

總之.h檔案裡生成了該類及其父類方法所對應的鏈式方法,並定義了用來接收可變引數的巨集定義。巨集定義的名字跟鏈式方法名一樣,這樣我們在使用點語法呼叫鏈式方法的時候,實際上就會進入到巨集中,而在這個巨集中會接收一個可變的引數列表。如下面程式碼所指示的:

#ifndef addTarget_action_forControlEvents                                  
#define addTarget_action_forControlEvents(...)  addTarget_action_forControlEvents(@"addTarget:action:forControlEvents:", metamacro_at(0, __VA_ARGS__), metamacro_at(1, __VA_ARGS__), (long long)metamacro_at(2, __VA_ARGS__))                                  
#endif
/**     ClassName-> UIButton                                
SEL:   addTarget: '@'
  action: ':'
  forControlEvents: 'Q'
     */
- (MLChain4UIButton *(^)())addTarget_action_forControlEvents;

當呼叫obj. mlc_make.addTarget_action_forControlEvents(self,SEL action, UIControlEventTouchUpInside)的時候,其實最終會進入NSObject+ChainInvocation.h分類中的方法- (instancetype (^)(NSString *selName, ...))mlc_rootChainMethod,在這裡我們會獲取到鏈式物件所繫結的原生物件與傳入的可變引數,最後轉換為原生物件呼叫原生的方法。(更加詳細的解析會在下一篇文章中)

如何生成檔案,那就是事先你需要將該檔案的具體內容,按照規則,拼接成一個具體的字串,然後在通過NSFileManager寫到桌面就可以了。

既然鏈式類中呼叫所有的方法最終都會轉換為原生類呼叫原生的方法,所以在鏈式類的實現檔案中,就沒有必要寫具體的實現了。這就是這個框架設計的巧妙之處。

看了普通鏈式標頭檔案中的內容,下面簡單的看一下普通鏈式實現檔案中的程式碼:

/**
 m檔案內容
 
 @param className <#className description#>
 @return <#return value description#>
 */
+ (NSString *)mlc_mFileContentStrWithClassName:(NSString *)className{
    NSMutableArray *resultStrs = [[NSMutableArray alloc] init];
 
    if ([className isEqualToString:@"NSObject"]) {
        NSString *  mfileContentString = @"+ (void)load{\n\
        \n  [self mlc_setUpMethodDynamically];\
        \n}";
         [resultStrs addObject:mfileContentString];
    }else{
      NSString *  mfileContentString = @"+ (void)load{\n\
        \n  [self mlc_setUpMethodDynamically];\
        \n}";
        NSString *chainObjectMethod =
        [NSString stringWithFormat:
         @"- (%@ *)chainObject{\
         \n    return (id)[super chainObject];\
         \n}", className];
        [resultStrs addObject:mfileContentString];
        [resultStrs addObject:chainObjectMethod];
    }
    return [resultStrs componentsJoinedByString:@"\n"];
    
}

實現檔案的程式碼比較簡單,簡單講就是新增了兩個方法,一個是+ (void)load方法,另一個是獲取該鏈式類所繫結的原生類的get方法。
load方法會在main函式前就呼叫,給鏈式類中的所有方法做一個動態新增方法的操作,並且以- (instancetype (^)(NSString *selName, ...))mlc_rootChainMethod作為它們的方法實現,也就是說當呼叫鏈式方法的時候,就會進到- (instancetype (^)(NSString *selName, ...))mlc_rootChainMethod中,而在這個方法裡,我們會讓鏈式物件所繫結的原生物件呼叫鏈式方法所對應的原生方法。

關於OBJ+MLChain分類檔案的生成

分類裡面的程式碼是比較少的,也是比較容易理解的。主要就是為了當呼叫obj.mlc_make的時候能夠返回一個橋樑物件,同時也是為了模仿Masonry的設定方式,作者在這裡新增了類方法與例項方法。(這裡就不做過多的解釋了。)
標頭檔案程式碼如下:

+ (NSString *)mlc_methodStringInCategory
{
    NSMutableString *resultString = [[NSMutableString alloc] init];
    NSString *methodString1 = [NSString stringWithFormat:@"+ (MLChain4%@ *)mlc_make;\n\n", NSStringFromClass(self)];
    NSString *methodString2 = [NSString stringWithFormat:@"- (MLChain4%@ *)mlc_make;\n\n", NSStringFromClass(self)];
    NSString *methodString3 = [NSString stringWithFormat:@"+ (MLChain4%@ *)mlc_makeConfigs:(void(^)(MLChain4%@ *maker))block;\n\n", NSStringFromClass(self), NSStringFromClass(self)];
    NSString *methodString4 = [NSString stringWithFormat:@"- (MLChain4%@ *)mlc_makeConfigs:(void(^)(MLChain4%@ *maker))block;\n\n", NSStringFromClass(self), NSStringFromClass(self)];
    [resultString appendString:methodString1];
    [resultString appendString:methodString2];
    [resultString appendString:methodString3];
    [resultString appendString:methodString4];
    return resultString;
    
}

以上所講的就是普通鏈式檔案與分類檔案的大致生成過程,並簡要的講了下程式碼是如何進行呼叫的。下篇文章會對這個框架的幾個難點進行分析,一個是方法巨集定義的生成、另一個是鏈式方法的呼叫過程,其中關於呼叫過程裡作者用到了YYKit中的一個重要的方法,下篇文章也會進行闡述。

最後建議大夥自己去demo中檢視。可以說這是一個設計非常巧妙的架構,可以學到很多的東西,筆者也會持續的對它進行分析並繪製相關的程式流程圖,希望能幫助小夥伴理解,當然有錯誤的話還望指正出來。

相關文章