若是你以爲 UITableViewDelegate 和 UITableViewDataSource 這兩個協議中有大量方法每次都是複製粘貼,實現起來大同小異;若是你以爲發起網絡請求並解析數據須要一大段代碼,加上刷新和加載後簡直複雜度爆表,若是你想知道爲何下面的代碼能夠知足上述全部要求:php
解耦後的VC面試
繫好安全帶,上車!json
MVC在討論解耦以前,咱們要弄明白 MVC 的核心:控制器(如下簡稱 C)負責模型(如下簡稱 M)和視圖(如下簡稱 V)的交互。設計模式
這裏所說的 M,一般不是一個單獨的類,不少狀況下它是由多個類構成的一個層。最上層的一般是以 Model 結尾的類,它直接被 C 持有。Model 類還能夠持有兩個對象:api
常見的誤區:數組
在 C 中,咱們建立 UITableView 對象,而後將它的數據源和代理設置爲本身。也就是本身管理着 UI 邏輯和數據存取的邏輯。在這種架構下,主要存在這些問題:緩存
爲了解決這些問題,咱們首先弄明白,數據源和代理分別作了那些事。安全
它有兩個必須實現的代理方法:網絡
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
簡單來講,只要實現了這個兩個方法,一個簡單的 UITableView 對象就算是完成了。架構
除此之外,它還負責管理 section 的數量,標題,某一個 cell 的編輯和移動等。
代理主要涉及如下幾個方面的內容:
最經常使用的也是兩個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
提醒:絕大多數代理方法都有一個 indexPath 參數
優化數據源最簡單的思路是單獨把數據源拿出來做爲一個對象。
這種寫法有必定的解耦做用,同時能夠有效減小 C 中的代碼量。然而總代碼量會上升。咱們的目標是減小沒必要要的代碼。
好比獲取每個 section 的行數,它的實現邏輯老是高度相似。然而因爲數據源的具體實現方式不統一,因此每一個數據源都要從新實現一遍。
首先咱們來思考一個問題,數據源做爲 M,它持有的 Item 長什麼樣?答案是一個二維數組,每一個元素保存了一個 section 所須要的所有信息。所以除了有本身的數組(給cell用)外,還有 section 的標題等,咱們把這樣的元素命名爲 SectionObject:
@interface KtTableViewSectionObject : NSObject@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 協議中的 titleForHeaderInSection 方法可能會用到@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 協議中的 titleForFooterInSection 方法可能會用到@property (nonatomic, retain) NSMutableArray *items;- (instancetype)initWithItemArray:(NSMutableArray *)items;@end
其中的 items 數組,應該存儲了每一個 cell 所須要的 Item,考慮到 Cell 的特色,基類的 BaseItem能夠設計成這樣:
@interface KtTableViewBaseItem : NSObject@property (nonatomic, retain) NSString *itemIdentifier;@property (nonatomic, retain) UIImage *itemImage;@property (nonatomic, retain) NSString *itemTitle;@property (nonatomic, retain) NSString *itemSubtitle;@property (nonatomic, retain) UIImage *itemAccessoryImage;- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage;@end
規定好了統一的數據存儲格式之後,咱們就能夠考慮在基類中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法爲例,它能夠這樣實現:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (self.sections.count > section) { KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section]; return sectionObject.items.count; } return 0;}
比較困難的是建立 cell,由於咱們不知道 cell 的類型,天然也就沒法調用 alloc 方法。除此之外,cell 除了建立,還須要設置 UI,這些都是數據源不該該作的事。
這兩個問題的解決方案以下:
通過這一番折騰,好處是至關明顯的:
對照 demo(SHA-1:6475496),感覺一下效果。
優化代理咱們以以前所說的,代理協議中經常使用的兩個方法爲例,看看怎麼進行優化與解耦。
首先是計算高度,這個邏輯並不必定在 C 完成,因爲涉及到 UI,因此由 Cell 負責實現便可。而計算高度的依據就是 Object,因此咱們給基類的 Cell 加上一個類方法:
+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;
另一類問題是以處理點擊事件爲表明的代理方法, 它們的主要特色是都有 indexPath 參數用來表示位置。然而實際在處理過程當中,咱們並不關係位置,關心的是這個位置上的數據。
所以,咱們對代理方法作一層封裝,使得 C 調用的方法中都是帶有數據參數的。由於這個數據對象能夠從數據源拿到,因此咱們須要可以在代理方法中獲取到數據源對象。
爲了實現這一點, 最好的辦法就是繼承 UITableView:
@protocol KtTableViewDelegate<UITableViewDelegate>@optional- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;// 未來能夠有 cell 的編輯,交換,左滑等回調// 這個協議繼承了UITableViewDelegate ,因此本身作一層中轉,VC 依然須要實現某@end@interface KtBaseTableView : UITableView<UITableViewDelegate>@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;@end
cell 高度的實現以下,調用數據源的方法獲取到數據:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath { id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource; KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath]; Class cls = [dataSource tableView:tableView cellClassForObject:object]; return [cls tableView:tableView rowHeightForObject:object];}
經過對 UITableViewDelegate 的封裝(其實主要是經過 UITableView 完成),咱們得到了如下特性:
對照 demo(SHA-1:ca9b261),感覺一下效果。
更加 MVC,更加簡潔在上面的兩次封裝中,其實咱們是把 UITableView 持有原生的代理和數據源,改爲了 KtTableView持有自定義的代理和數據源。而且默認實現了不少系統的方法。
到目前爲止,看上去一切都已經完成了,然而實際上仍是存在一些能夠改進的地方:
基於以上考慮, 咱們實現一個 UIViewController 的子類,而且把數據源和代理封裝到 C 中。
@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>@property (nonatomic, strong) KtBaseTableView *tableView;@property (nonatomic, strong) KtTableViewDataSource *dataSource;@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用來建立 tableView- (instancetype)initWithStyle:(UITableViewStyle)style;@end
爲了確保子類建立了數據源,咱們把這個方法定義到協議裏,而且定義爲 required。
成果與目標如今咱們梳理一下通過改造的 TableView 該怎麼用:
首先你須要建立一個繼承自 KtTableViewController 的視圖控制器,而且調用它的 initWithStyle方法。
objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
在子類 VC 中實現 createDataSource 方法,實現數據源的綁定。
* (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步建立了數據源 } ```
在數據源中,須要指定 cell 的類型。
* (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } ```
在 Cell 中,須要經過解析數據,來更新 UI 並返回本身的高度。
* (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法。 ```
到目前爲止,咱們實現了對 UITableView 以及相關協議、方法的封裝,使它更容易使用,避免了不少重複、無心義的代碼。
在使用時,咱們須要建立一個控制器,一個數據源,一個自定義 Cell,它們正好是基於 MVC 模式的。所以,能夠說在封裝與解耦方面,咱們已經作的至關好了,即便再花大力氣,也很難有明顯的提升。
但關於 UITableView 的討論遠遠沒有結束,我列出瞭如下須要解決的問題
關於第一個問題,實際上是普通的 MVC 模式中 V 和 C 的交互問題,能夠在 Cell(或者其餘類) 中添加 weak 屬性達到直接持有的目的,也能夠定義協議。
問題二和三是另外一大塊話題,網絡請求你們都會實現,但如何優雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深刻思考,研究的問題了。接下來咱們就重點討論網絡請求。
爲什麼建立網絡層一個 iOS 的網絡層框架該如何設計?這是一個很是寬泛,也超出我能力範圍以外的問題。業內已有一些優秀的,成熟的思路和解決方案,因爲能力,角色所限,我決定從一個普通開發者而不是架構師的角度來講說,一個普通的、簡單的網絡層該如何設計。我相信再複雜的架構,也是由簡單的設計演化而來的。
對於絕大多數小型應用來講,集成 AFNetworking 這樣的網絡請求框架就足以應付 99% 以上的需求了。可是隨着項目的擴大,或者用長遠的眼光來考慮,直接在 VC 中調用具體的網絡框架(下面以 AFNetworking 爲例),至少存在如下問題:
一旦往後 AFNetworking 中止維護,並且咱們須要更換網絡框架,這個成本將沒法想象。全部的 VC 都要改動代碼,並且絕大多數改動都是雷同的。
這樣的例子真實存在,好比咱們的項目中就依然使用早已中止維護的 ASIHTTPRequest,能夠預見,這個框架早晚要被替換。
現有的框架可能沒法實現咱們的需求。以 ASIHTTPRequest 爲例,它的底層用 NSOperation 來表示每個網絡請求。衆所周知,一個 NSOperation 的取消,並非簡單調用 cancel 方法就能夠的。在不修改源碼的前提下,一旦它被放入隊列,實際上是沒法取消的。
有時候咱們的需求僅僅是進行網絡請求,還會對這個請求進行各類自定義的拓展。好比咱們可能要統計請求的發起和結束時間,從而計算網絡請求,數據解析的步驟的耗時。有時候,咱們但願設計一個通用組件,而且支持由各個業務部門去自定義具體的規則。好比可能不一樣的部門,會爲 HTTP 請求添加不一樣的頭部。
網絡請求還有可能有其餘普遍須要添加的需求,好比請求失敗時的彈窗,請求時的日誌記錄等等。
參考當前代碼(SHA-1:a55ef42)感覺一下沒有任何網絡層時的設計。
如何設計網絡層其實解決方案很是簡單:
全部的計算機問題,均可以經過添加中間層來解決
讀者能夠自行思考,爲何添加中間層能夠解決上述三個問題。
對於一個網絡框架來講,我認爲主要有三個方面值得去設計:
一個完整的網絡請求通常由以上三個模塊組成,咱們逐一分析每一個模塊實現時的注意事項:
發起請求時,通常有兩種思路,第一種是把全部要配置的參數寫到同一個方法中,借用 與時俱進,HTTP/2下的iOS網絡層架構設計 一文中的代碼表示:
+ (void)networkTransferWithURLString:(NSString *)urlString andParameters:(NSDictionary *)parameters isPOST:(BOOL)isPost transferType:(NETWORK_TRANSFER_TYPE)transferType andSuccessHandler:(void (^)(id responseObject))successHandler andFailureHandler:(void (^)(NSError *error))failureHandler { // 封裝AFN }
這種寫法的好處在於全部參數一目瞭然,並且簡單易用,每次都調用這個方法便可。可是缺點也很明顯,隨着參數和調用次數的增多,網絡請求的代碼很快多到爆炸。
另外一組方法則是將 API 設置成一個對象,把要傳入的參數做爲這個對象的屬性。在發起請求時,只要設置好對象的相關屬性,而後調用一個簡單的方法便可。
@interface DRDBaseAPI : NSObject@property (nonatomic, copy, nullable) NSString *baseUrl;@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject, NSError * _Nullable error);- (void)start;- (void)cancel;...@end
根據前文提到的 Model 和 Item 的概念,那麼應該能夠想到:這個用於訪問網絡的 API 對象,實際上是做爲 Model 的一個屬性。
Model 負責對外暴露必要的屬性和方法,而具體的網絡請求則由 API 對象完成,同時 Model 也應該持有真正用來存儲數據的 Item。
一次網絡請求的返回結果應該是一個 JSON 格式的字符串,經過系統的或者一些開源框架能夠將它轉換成字典。
接下來咱們須要使用 runtime 相關的方法,將字典轉換成 Item 對象。
最後,Model 須要將這個 Item 賦值給本身的屬性,從而完成整個網絡請求。
若是從全局角度來講,咱們還須要一個 Model 請求完成的回調,這樣 VC 纔能有機會作相應的處理。
考慮到 Block 和 Delegate 的優缺點,咱們選擇用 Block 來完成回調。
這一部分主要是利用 runtime 將字典轉換成 Item,它的實現並不算難,可是如何隱藏好實現細節,使上層業務不用過多關心,則是咱們須要考慮的問題。
咱們能夠定義一個基類的 Item,而且爲它定義一個 parseData 函數:
// KtBaseItem.m- (void)parseData:(NSDictionary *)data { // 解析 data 這個字典,爲本身的屬性賦值 // 具體的實現請見後面的文章}
首先,咱們封裝一個 KtBaseServerAPI 對象,這個對象的主要目的有三個:
具體的實現請參考 Git 提交歷史:SHA-1:76487f7
Model 主要須要負責發起網絡請求,而且處理回調,來看一下基類的 Model 如何定義:
@interface KtBaseModel// 請求回調@property (nonatomic, copy) KtModelBlock completionBlock;//網絡請求@property (nonatomic,retain) KtBaseServerAPI *serverApi;//網絡請求參數@property (nonatomic,retain) NSDictionary *params;//請求地址 須要在子類init中初始化@property (nonatomic,copy) NSString *address;//model緩存@property (retain,nonatomic) KtCache *ktCache;
它經過持有 API 對象完成網絡請求,能夠定製本身的存儲邏輯,控制請求方式的選擇(長、短連接,JSON或protobuf)。
Model 應該對上層暴露一個很是簡單的調用接口,由於假設一個 Model 對應一個 URL,其實每次請求只須要設置好參數,就能夠調用合適的方法發起請求了。
因爲咱們不能預知請求什麼時候結束,因此須要設置請求完成時的回調,這也須要做爲 Model 的一個屬性。
基類的 Item 主要是負責 property name 到 json path 的映設,以及 json 數據的解析。最核心的字典轉模型實現以下:
- (void)parseData:(NSDictionary *)data { Class cls = [self class]; while (cls != [KtBaseItem class]) { NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls]; for (NSString *key in [propertyList allKeys]) { NSString *typeString = [propertyList objectForKey:key]; NSString* path = [self.jsonDataMap objectForKey:key]; id value = [data objectAtPath:path]; [self setfieldName:key fieldClassName:typeString value:value]; } cls = class_getSuperclass(cls); }}
完整代碼參考 Git 提交歷史:SHA-1:77c6392
在實際使用時,首先要建立子類的 Modle 和 Item。子類的 Model 應該持有 Item 對象,而且在網絡請求回調時,將 API 中攜帶的 JSON 數據賦值給 Item 對象。
這個 JSON 轉對象的過程在基類的 Item 中實現,子類的 Item 在建立時,須要指定屬性名和 JSON 路徑之間的對應關係。
對於上層來講,它須要生成一個 Model 對象,設置好它的路徑以及回調,這個回調通常是網絡請求返回時 VC 的操做,好比調用 reloadData 方法。這時候的 VC 能夠肯定,網絡請求的數據就存在 Model 持有的 Item 對象中。
具體代碼參考 Git 提交歷史:SHA-1:8981e28
下拉刷新不少應用的 UITableview 都具備下拉刷新和上拉加載的功能,在實現這個功能時,咱們主要考慮兩點:
第一點已是老生常談,參考 SHA-1 61ba974 就能夠看到如何實現一個簡單的封裝。
重點在於對於 Model 和 Item 的改造。
這個 Item 沒有什麼別的做用,就是定義了一個屬性 pageNumber,這是須要與服務端協商的。Model 將會根據這個屬性這個屬性判斷有沒有所有加載完。
// In .h@interface KtBaseListItem : KtBaseItem@property (nonatomic, assign) int pageNumber;@end// In .m- (id)initWithData:(NSDictionary *)data { if (self = [super initWithData:data]) { self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue]; } return self;}
對於 Server 來講,若是每次都返回 page_number 無疑是很是低效的,由於每次參數均可能不一樣,計算總數據量是一項很是耗時的工做。所以在實際使用中,客戶端能夠和 Server 約定,返回的結果中帶有 isHasNext 字段。經過這個字段,咱們同樣能夠判斷是否加載到最後一頁。
它持有一個 ListItem 對象, 對外暴露一組加載方法,而且定義了一個協議 KtBaseListModelProtocol,這個協議中的方法是請求結束後將要執行的方法。
@protocol KtBaseListModelProtocol <NSObject>@required- (void)refreshRequestDidSuccess;- (void)loadRequestDidSuccess;- (void)didLoadLastPage;- (void)handleAfterRequestFinish; // 請求結束後的操做,刷新tableview或關閉動畫等。@optional- (void)didLoadFirstPage;@end@interface KtBaseListModel : KtBaseModel@property (nonatomic, strong) KtBaseListItem *listItem;@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;@property (nonatomic, assign) BOOL isRefresh; // 若是爲是,表示刷新,不然爲加載。- (void)loadPage:(int)pageNumber;- (void)loadNextPage;- (void)loadPreviousPage;@end
實際上,當 Server 端發生數據的增刪時,只傳 nextPage 這個參數是不能知足要求的。兩次獲取的頁面並不是徹底沒有交集,頗有可能他們具備重複元素,因此 Model 還應該肩負起去重的任務。爲了簡化問題,這裏就不完整實現了。
它實現了 ListMode 中定義的協議,提供了一些通用的方法,而具體的業務邏輯則由子類實現。
#pragma -mark KtBaseListModelProtocol- (void)loadRequestDidSuccess { [self requestDidSuccess];}- (void)refreshRequestDidSuccess { [self.dataSource clearAllItems]; [self requestDidSuccess];}- (void)handleAfterRequestFinish { [self.tableView stopRefreshingAnimation]; [self.tableView reloadData];}- (void)didLoadLastPage { [self.tableView.mj_footer endRefreshingWithNoMoreData];}#pragma -mark KtTableViewDelegate- (void)pullUpToRefreshAction { [self.listModel loadNextPage];}- (void)pullDownToRefreshAction { [self.listModel refresh];}
在一個 VC 中,它只須要繼承 RefreshTableViewController,而後實現 requestDidSuccess 方法便可。下面展現一下 VC 的完整代碼,它超乎尋常的簡單:
- (void)viewDidLoad { [super viewDidLoad]; [self createModel]; // Do any additional setup after loading the view, typically from a nib.}- (void)createModel { self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"]; self.listModel.delegate = self;}- (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這一步建立了數據源}- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated.}- (void)requestDidSuccess { for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) { KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init]; item.itemTitle = book.bookTitle; [self.dataSource appendItem:item]; }}
其餘的判斷,好比請求結束時關閉動畫,最後一頁提示沒有更多數據,下拉刷新和上拉加載觸發的方法等公共邏輯已經被父類實現了。
具體代碼見 Git 提交歷史:SHA-1:0555db2
寫在結尾網絡請求的設計架構到此就所有結束了,它還有不少值的拓展的地方。仍是那句老話,沒有通用的架構,只有最適合業務的架構。
個人 Demo 爲了方便演示和閱讀,一般都是先實現底層的類和方法,而後再由上層調用。但實際上這種作法在實際開發中是不現實的。咱們老是在發現大量冗餘,無心義的代碼後,纔開始設計架構。
所以在我看來,真正的架構過程是當業務發生變動(一般是變複雜了)時,咱們開始應該思考當前哪些操做是能夠省略的(由父類或代理實現),最上層應該以何種方式調用底層的服務。一旦設計好了最上層的調用方式,就能夠逐步向底層實現了。
因爲做者水平有限,本文的架構並不優秀,但願在深刻理解設計模式,積累更多經驗後,再與你們分享收穫。
做爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:1012951431 無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!
另附上一份各好友收集的大廠面試題,進羣可自行下載!