設計一個App的思路:
原則:易讀 易維護 易擴展? ;技術儲備;語言選擇
組成:
1.應用入口(Appdelegate):存放推送 IM 支付回調(diào)等
2.功能模塊:根據(jù)業(yè)務進行劃分:可以靈活采用MVC MVVM MVP
3.管理模塊:登陸狀態(tài)信息 單例 網(wǎng)絡監(jiān)聽 廣告頁
4.工具類:自己寫的工具類
5.基類:一些定制化的內(nèi)容頁面 樣式 空數(shù)據(jù)頁面 無網(wǎng)絡提示頁面
6.分類:對系統(tǒng)類 自定義類增加的類別
7.宏定義文件:全局通用的宏定義 方法
8.資源文件:圖片 json xml test plist
9.第三方庫的封裝:
10.Cocoapods:
重構(gòu)需要考慮的因素:
1.明確重構(gòu)的目的和重用性
2.定義重構(gòu)完成的界限
3.持續(xù)漸進式重構(gòu)
4.確定當前的架構(gòu)狀態(tài)
5.不能忽略數(shù)據(jù)的重用性
6.管理好技術債務
7.遠離虛華的東西 追求實際
8.做好準備面對壓力,做好面對非技術的準備
9.了解當前業(yè)務
10.時刻注意代碼的質(zhì)量
11.團隊一致 做好準備
MVC:http://www.itdecent.cn/p/eedbc820d40a
MVC是軟件工程中的一種軟件架構(gòu)模式,它把軟件系統(tǒng)分為三個基本的部分:模型Model、視圖View以及控制器Controller;
數(shù)據(jù)Model: 負責封裝數(shù)據(jù)、存儲和處理數(shù)據(jù)運算等工作
視圖View: 負責數(shù)據(jù)展示、監(jiān)聽用戶觸摸等工作
控制器Controller: 負責業(yè)務邏輯、事件響應、數(shù)據(jù)加工等工作
在iOS中,M和V之間禁止通信,必須由C控制器層來協(xié)調(diào)M和V之間的變化,C對M和V的訪問是不受限的
Controller 可以直接與 Model 對話(讀寫調(diào)用 Model),Model 通過 Notification 和 KVO 機制與 Controller 間接通信
Controller 可以直接與 View 對話,通過 outlet,直接操作 View,outlet 直接對應到 View 中的控件,View 通過 action 向 Controller 報告事件的發(fā)生(如用戶 Touch 我了)。Controller 是 View 的直接數(shù)據(jù)源(數(shù)據(jù)很可能是 Controller 從 Model 中取得并經(jīng)過加工了)。Controller 是 View 的代理(delegate),以同步 View 與 Controller
MVC的缺點在于并沒有區(qū)分業(yè)務邏輯和業(yè)務展示, 這對單元測試很不友好
MVP:
MVP模式是MVC模式的一個演化版本(好像所有的模式都是出自于MVC~~),MVP全稱Model-View-Presenter;
MVP的 V 層是由UIViewController 和UIView 共同組成;
Model:與MVC中的model沒有太大的區(qū)別。主要提供數(shù)據(jù)的存儲功能,一般都是用來封裝網(wǎng)絡獲取的json數(shù)據(jù)的集合
Presenter:作為model和view的中間人,從model層獲取數(shù)據(jù)之后傳給view,使得View和model沒有耦合
view 將委托presenter 對它自己的操作,(簡單來說就是presenter發(fā)命令來控制view的交互,要你隱藏就隱藏,叫你show 你就乖乖的show)
presenter擁有對 view交互的邏輯(就是上面說的意思)
presenter跟model層通信,"Present"一方面通過Service層調(diào)用接口獲取數(shù)據(jù)給Model層,并將數(shù)據(jù)轉(zhuǎn)化成對適應UI的數(shù)據(jù)并更新view
presenter不需要依賴UIKit
view層是單一,因為它是被動接受命令,沒有主動能力

presenter 作為業(yè)務邏輯的處理者,首先要向Service層拿數(shù)據(jù)賦值給model,所以它將可以向model層通信。其次,UI的處理權(quán)移交給了它,所以它需要與view成通訊,發(fā)送命令更新UI。同時,UI的響應將觸發(fā)業(yè)務邏輯的處理,所以view 層向presenter層通訊,告訴他用戶做了什么操作,需要你反饋對應的數(shù)據(jù)來更新UI。這樣就完成了從用戶交互獲得交互反饋到整個業(yè)務邏輯。
關于C端和P端的循環(huán)引用的問題, 直接用weak關鍵字就可以解決了
總得來說MVP的好處就是解除view與model的耦合,使得view或model有更強的復用性
只需要初始化P層, 然后調(diào)P層的接口就可以了. 至于P層內(nèi)部的邏輯, 我不需要知道
V層也只專注于視圖的創(chuàng)建
M層只專注于模型的構(gòu)建(字典->模型)
優(yōu)點:
對Controller進行瘦身,View和Model之間不存在耦合,同時也將業(yè)務邏輯從View中抽離,復用性更好
缺點:
由于對視圖的渲染放在了Presenter中,所以視圖和Presenter的交互會過于頻繁。還有一點需要明白,如果Presenter過多地渲染了視圖,往往會使得它與特定的視圖的聯(lián)系過于緊密。一旦視圖需要變更,那么Presenter也需要變更了
MVVM:

在MVVM 中,view 和 view controller正式聯(lián)系在一起,我們把它們視為一個組件
view 和 view controller 都不能直接引用model,而是引用視圖模型(viewModel)
viewModel 是一個放置用戶輸入驗證邏輯,視圖顯示邏輯,發(fā)起網(wǎng)絡請求和其他代碼的地方
使用MVVM會輕微的增加代碼量,但總體上減少了代碼的復雜性
MVVM模式將Presenter改名為ViewModel,基本上與MVP模式完全一致。
唯一的區(qū)別是,它采用雙向綁定(data-binding)?: View<->ViewModel, ViewModel作為Model中值的映射,是數(shù)據(jù)發(fā)生改變時,通知View中發(fā)生改變,以后不需要考慮View和Model之間的交互更新,只需著手界面布局邏輯即可。
①View和Model 不直接關聯(lián),而是通過ViewModel作為樞紐,溝通View和Model之間的關系。
②View中控件的值與屬性進行綁定,通過KVO鍵值觀察(這樣當model的值發(fā)生變化時,View會自動發(fā)生改變)?
View和Model通過ViewModel實現(xiàn)動態(tài)關聯(lián)
MVVM 的注意事項
view 引用viewModel ,但反過來不行(即不要在viewModel中引入#import UIKit.h,任何視圖本身的引用都不應該放在viewModel中)(PS:基本要求,必須滿足)
viewModel 引用model,但反過來不行
MVVM 的優(yōu)勢:
低耦合:View 可以獨立于Model變化和修改,一個 viewModel 可以綁定到不同的 View 上
可重用性:可以把一些視圖邏輯放在一個 viewModel里面,讓很多 view 重用這段視圖邏輯
獨立開發(fā):開發(fā)人員可以專注于業(yè)務邏輯和數(shù)據(jù)的開發(fā) viewModel,設計人員可以專注于頁面設計
可測試:通常界面是比較難于測試的,而 MVVM 模式可以針對 viewModel來進行測試
MVVM 的弊端:
數(shù)據(jù)綁定使得Bug 很難被調(diào)試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數(shù)據(jù)綁定使得一個位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那么容易了
對于過大的項目,數(shù)據(jù)綁定和數(shù)據(jù)轉(zhuǎn)化需要花費更多的內(nèi)存(成本)
綁定是一種響應式的通信方式。當被綁定對象某個值的變化時,綁定對象會自動感知,無需被綁定對象主動通知綁定對象??梢允褂肒VO和RAC實現(xiàn)。例如在Label中顯示倒計時,是V綁定了包含定時器的VM。
組件化:(使用cocoapods進行組件化的實現(xiàn))
組件化方案的幾種實現(xiàn):
方案一:url-block
通過在啟動時注冊組件提供的服務,把調(diào)用組件使用的url和組件提供的服務block對應起來,保存到內(nèi)存中。在使用組件的服務時,通過url找到對應的block,然后獲取服務
出現(xiàn)的問題:
1、需要在內(nèi)存中維護url-block的表,組件多了可能會有內(nèi)存問題
2、url的參數(shù)傳遞受到限制,只能傳遞常規(guī)的字符串參數(shù),無法傳遞非常規(guī)參數(shù),如UIImage、NSData等類型
3、沒有區(qū)分本地調(diào)用和遠程調(diào)用的情況,尤其是遠程調(diào)用,會因為url參數(shù)受限,導致一些功能受限
4、組件本身依賴了中間件,且分散注冊使的耦合較多
方案二:protocol-class
通過protocol定義服務接口,組件通過實現(xiàn)該接口來提供接口定義的服務,具體實現(xiàn)就是把protocol和class做一個映射,同時在內(nèi)存中保存一張映射表,使用的時候,就通過protocol找到對應的class來獲取需要的服務
出現(xiàn)的問題:
依然沒有解決組件依賴中間件的問題、內(nèi)存中維護映射表的問題、組件的分散調(diào)用的問題
方案三:target-action
通過給組件包裝一層wrapper來給外界提供服務,然后調(diào)用者通過依賴中間件來使用服務;其中,中間件是通過runtime來調(diào)用組件的服務,是真正意義上的解耦,也是該方案最核心的地方。具體實施過程是給組件封裝一層target對象來對外提供服務,不會對原來組件造成入侵;然后,通過實現(xiàn)中間件的category來提供服務給調(diào)用者,這樣使用者只需要依賴中間件,而組件則不需要依賴中間件
方案四:使用cocoapods進行組件化的實現(xiàn))
具體就是建立一個項目工程的私有化倉庫,然后把各個組件的podspec上傳到私有倉庫,在需要用到組件時,直接從倉庫里面取
1.添加Podfile文件?pod init?然后會發(fā)現(xiàn)你的工程目錄下多了Podfile文件
2.生成xcworkspace工程?pod install
3.新建一個Lib(自己起名)文件夾,用來存放組件庫(其他獨立工程)然后cd到Lib下 執(zhí)行?pod lib create?
XXX(工程名)
4.打開新建的XXX(工程名)工程里的Example,可以看到pods里面,有個ReplaceMe的文件,意思就是要替換它,換成我們自己需要對外提供的類
5.新建一個類,比如TRUXXX,復制粘貼到ReplaceMe同級目錄下,并刪掉ReplaceMe.m文件
6.?之后cd到Lib/TRUXXX/Example/文件目錄下,執(zhí)行pod install?這個時候在Development Pods文件下會多出這兩個文件,這就是本地開發(fā)的pods文件
7.而Podfile的內(nèi)容其實是
pod 'TRUXXX', :path => '../'
說明他獲取的是本地路徑
然后刪除Example for TRUXXX里面的TRUXXX類,不然運行會因為類重復報錯。
至此,一個組件的本地庫就創(chuàng)建完成了。
8. 殼工程使用本地組件庫
首先cd到殼工程LZDemo目錄下,修改LZDemo的Podfile文件,增加
pod 'TRUXXX', :path => 'Lib/TRUXXX'
執(zhí)行 pod install
組件需要對外提供依賴關系。所以我們還得多做一步操作,那就是增加podspec文件
以TRUXXX為例,cd到TRUXXX目錄下,執(zhí)行
git tag 0.1.0
git push --tags
這個tag分支就是將來提供給別人依賴的版本號分支,有了它,別人使用你的組件的時候就可以根據(jù)版本號來控制了。
改好后,在上傳之前,最好先本地檢查一下podspec是否合法
執(zhí)行下面語句
pod lib lint --verbose
如果出現(xiàn)passed validation,說明通過,可以提交到cocoapods上了
成功后,就可以pod search到我們提交的庫了
ps:如果搜不到,不是沒傳成功,是我們的本地搜索庫沒更新,可以先刪除~/Library/Caches/CocoaPods目錄下的search_index.json文件或者pod repo update一下
rm~/Library/Caches/CocoaPods/search_index.json
組件間通訊
1. Protocol注冊方案
通過JJProtocolManager 作為中間轉(zhuǎn)化
+ (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;
+ (id)moduleProviderForProtocol:(Protocol *)protocol;
有組件對外提供的procotol和組件提供的服務由中間件統(tǒng)一管理,每個組件提供的procotol和服務是一一對應的。
例如:
在JJLoginProvider中:load方法會應用啟動的時候調(diào)用,就會在JJProtocolManager進行注冊。JJLoginProvider遵守了JJLoginProvider協(xié)議,這樣就可以對外根據(jù)業(yè)務需求提供一些方法。
+ (void)load
{
? ? [JJProtocolManager registerModuleProvider:[self new] forProtocol:@protocol(JJLoginProtocol)];
}
- (UIViewController *)viewControllerWithInfo:(id)userInfo needNew:(BOOL)needNew callback:(JJModuleCallbackBlock)callback{
? ? CLoginViewController *vc = [[CLoginViewController alloc] init];
? ? vc.jj_moduleCallbackBlock = callback;
? ? vc.jj_moduleUserInfo = userInfo;
? ? return vc;
}
這樣就可以在需要登錄業(yè)務模塊的地方,通過JJProtocolManager取出JJLoginProtocol對應的服務提供者JJLoginProvider,直接獲取。如下:
id<jjwebviewvcmoduleprotocol>?provider?=?[JJProtocolManager?moduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)];?
???UIViewController?*vc?=[provider?viewControllerWithInfo:obj?needNew:YES?callback:^(id?info)?{?
???????if?(callback)?{?
???????????callback(info);?
???????}?
???}];?
???vc.hidesBottomBarWhenPushed?=?YES;?
???[self.currentNav?pushViewController:vc?animated:YES];</jjwebviewvcmoduleprotocol>?
2. URL注冊方案?OPENURL
原理:
通過url注冊服務, 其他地方通過url, 獲取服務;框架在維護一個url-block的表格
特點:
每個業(yè)務組件, 都需要依賴這個框架
url維護成本高 硬解碼
可以在組件內(nèi)部任何地方調(diào)用/注冊服務, 沒有必要統(tǒng)一組件接口服務
[MGJRouter?registerURLPattern:@"mgj://detail?id=:id"?toHandler:^(NSDictionary?*routerParameters)?{?
NSNumber?*id?=?routerParameters[@"id"];?
//create?view?controller?with?id?
//?pushview?controller?
}];?
首頁只需調(diào)用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打開相應的詳情頁。
3. Target-Action runtime調(diào)用方案
原理:
每個組件, 提供一個統(tǒng)一披露的接口文件
額外的維護一個中間件的分類擴展(在此處進行硬解碼 通過運行時進行物理解耦)
其他地方通過target-action;的方案進行交互
特點:
統(tǒng)一了組件api服務
組件與框架之間無依賴關系
需要額外維護中間件類擴展
實現(xiàn):
我們主要是依賴CTMediator?這個中間件工具類中主要使用如下方法:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
方法內(nèi)部使用Runtime調(diào)用 需要傳三個參數(shù)
當前需要調(diào)用的類名 (字符串)
當前需要調(diào)用類的方法名 (字符串)
需要傳的參數(shù) (字典形式)
# 通過Runtime 把字符串 轉(zhuǎn)換類
Class targetClass = NSClassFromString(ClassString);
id? target = [[targetClass alloc] init];
# 把字符串轉(zhuǎn)換成事件
SEL action = NSSelectorFromString(actionString);
# 如果當前類中有這個事件 那就執(zhí)行這個事件 把需要的參數(shù)傳值
if ([target respondsToSelector:action]) {
? ? return [target performSelector:action withObject:params];
}
4.使用cocoapods進行組件化的實現(xiàn))
1.每個業(yè)務組件庫里面會有一個控制器的配置文件(路由配置文件),標記著每個控制器的key;
2.在App每次啟動時,組件通訊的工具類里面需要解析控制器配置文件(路由配置文件),將其加載進內(nèi)存;
3. 在內(nèi)存中查詢路由配置,找到具體的控制器并動態(tài)生成類,然后使用==消息發(fā)送機制==進行調(diào)用函數(shù)、傳參數(shù)、回調(diào),都能做到。
5. 依賴注入
組件化的好處:
業(yè)務分層、解耦,使代碼變得可維護;
有效的拆分、組織日益龐大的工程代碼,使工程目錄變得可維護;
便于各業(yè)務功能拆分、抽離,實現(xiàn)真正的功能復用;
業(yè)務隔離,跨團隊開發(fā)代碼控制和版本風險控制的實現(xiàn);
模塊化對代碼的封裝性、合理性都有一定的要求,提升開發(fā)同學的設計能力;
在維護好各級組件的情況下,隨意組合滿足不同客戶需求;
http://www.itdecent.cn/p/59c2d2c4b737創(chuàng)建組件化的步驟
各個組件該如何進行拆分:
1.??項目主工程:主工程就是一個空殼子工程
2.??業(yè)務組件:業(yè)務組件就是各個獨立的產(chǎn)品業(yè)務功能模塊
3.??基礎工具類組件:基礎工具類是各個互相獨立,沒有任何依賴的工具組件。它們和其它的工具組件、業(yè)務組件等沒有任何依賴關系。這類組件例如有:對數(shù)組,字典進行異常保護的Safe組件,對數(shù)組功能進行擴展Array組件,對字符串進行加密處理的加密組件等等。
4.? 中間件組件:中間調(diào)度者就是一個功能獨立的中間件組件
5.??基礎UI組件:視圖組件就比較常見了,例如我們封裝的導航欄組件,Modal彈框組件,PickerView組件等。
6.??業(yè)務工具組件:這類組件是為各個業(yè)務組件提供基礎功能的組件。這類組件可能會依賴到其他的組件。例如:網(wǎng)絡請求組件,圖片緩存組件,jspatch組件等等
詳細操作步驟:
第一步:
我們先創(chuàng)建一個空的iOS工程項目:MainProject,這個空項目作為我們的主工程項目,就是上面所說的殼子工程項目,然后初始化pod
第二步:
我們創(chuàng)建一個空工程項目:ModuleA,這個ModuleA 項目作為我們的業(yè)務A組件。然后我們初始化pod,初始化podspec文件
第三步:
我們創(chuàng)建一個空工程項目:ModuleB,這個ModuleB 項目作為我們的業(yè)務B組件。然后我們初始化pod,初始化podspec文件
第四步:
我們創(chuàng)建一個空工程項目:ComponentMiddleware,這個項目就是我們上面所說的中間調(diào)度者。然后我們初始化pod,初始化podspec文件。
第五步:
我們創(chuàng)建一個空工程項目: ModuleACategory,這個工程是對應業(yè)務組件A的一個分類工程。然后我們初始化pod,初始化podspec文件。
第六步:
我們創(chuàng)建一個空工程項目: ModuleBCategory,這個工程是對應業(yè)務組件B的一個分類工程。然后我們初始化pod,初始化podspec文件。
第七步:
我們在主工程MainProject的Podfile中引入我們的業(yè)務組件B工程ModuleB,以及引入我們的ModuleB的分類工程:ModuleBCategory。然后我們pod install。這時已將這兩個組件庫引入到我們的主工程中了。
#import <ModuleBCategory/ComponentScheduler+ModuleB.h>
- (void)moduleB {
? ? UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {
? ? ? ? NSLog(@"resultB: --- %@", result);
? ? }];
? ? [self.navigationController pushViewController:VC animated:YES];
}
第八步:
上面第七步中,我們用到了ModuleBCategory 這個分類工程。這個工程我們只對外暴露了兩個文件。這兩文件是上面的中間調(diào)度者的分類,也就是說是中間件的分類。我們先來看下這個分類文件的.h 和.m 實現(xiàn)
#import "ComponentScheduler+ModuleB.h"
@implementation ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {
? ? NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
? ? params[@"callback"] = callback;
? ? return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
第九步:
這個分類的作用你可以理解為我們提前約定好Target的名字和Action的名字,因為這兩個名字中間件組件中會用到。
因為上面第八步中引用到中間件工程,這里我們就來看下中間件工程到底做了什么工作。還記得上面第八步中,我們調(diào)用了一個中間件提供的函數(shù):performTarget:action:params:shouldCacheTarget吧,這個是中間件核心函數(shù)。

這個函數(shù)最終調(diào)用到蘋果官方提供的函數(shù):[target performSelector:action withObject:params];
看到?performSelector: withObject:?大家應該就比較熟悉了,iOS的消息傳遞機制。
[Target_ModuleB performSelector:Action_viewController withObject:params];
上面這行偽代碼意思是: Target_ModuleB這個類 調(diào)用它的 Action_viewController: 方法,然后傳遞的參數(shù)為 params。
細心的小伙伴們就會發(fā)現(xiàn),我們沒有看到過哪里有這個Target_ModuleB 類啊,更沒有看到Target_ModuleB 調(diào)用它的 Action_viewController: 方法啊。
是的,這個Target_ModuleB類和類的Action_viewController方法就在第十步中講解到。
第十步:
業(yè)務組件B除了提供組件B的業(yè)務功能外,業(yè)務組件B還需要為我們提供一個Target文件
#import "Target_ModuleB.h"
#import "ModuleBViewController.h"
@implementation Target_ModuleB
- (UIViewController *)Action_viewController:(NSDictionary *)params {
? ? ModuleBViewController *VC = [[ModuleBViewController alloc] init];
? ? return VC;
}
@end
從上面的實現(xiàn)文件中,我們可以看到,Target文件的作用也很簡單,就是為我們提供導航跳轉(zhuǎn)的目標控制器實例對象。這里的目標控制器實例就是業(yè)務組件B的ModuleBViewController?實例。