最近在看一些 App 架構相關的文章,也看了 Facebook 分享的兩個不同時期的架構(2013 和 2014),於是就想一窺 Facebook App 的頭文件,看看會不會有更多的收獲,確實有,還不少。由於在選擇 ipa 上的失誤,下了個 7.0 版的 Facebook(最新的是 18.1),會稍有過時,不過後來又下了個 18.1 的看了下,發現變動其實不大。以下是我從頭文件中獲取到的一些信息(20多萬行,浏覽起來還是挺累的)
讓視圖組件可以方便地配置
這個在 Facebook 的演講中也提到過,自定義的 UI 組件在初始化時可以傳一些數值來表示想要呈現的效果,就像 HTML 和 CSS 一樣,Dom 結構表示這是什麼,CSS 對該結構進行個性化定制。 Facebook 是通過 Struct 來做這件事的,比如:
struct FBActionSheetButtonMetrics { CDUnknownFunctionPointerType *_vptr$FBMetrics; _Bool _initialized; float leftMargin; float textLeftMargin; float bottomSeperatorSideMargin; float bottomSeperatorHeight; int detailMaxNumLines; UIColor *titleColor; //... };
好處是減少了代碼量,而且直觀,方便復用。
盡量使用組合,適度使用繼承
如果過度使用繼承,尤其是繼承層次過深,往往會帶來更大的維護成本。有新需求或需求變更時,會花很多時間在「是否需要在基類/子類增加一個方法」,「是否需要新建一個子類」等設計相關的問題上。而組合則沒有這個問題,大不了換一個組件。
不過 Objective-C 對於組合並沒有特別的支持,所以實現起來會略麻煩
@interface People {} @property id veachle; - (void)move; @end @implementation People - (id)initWithVeachle: (id )veachle { if (self = [super init]) { self.veachle = veachle; } return self; } - (void)move { [self.veachle move]; } @end
如果有很多類似 move 這樣需要交給外部的 object 來做的方法,就會顯得冗余,盡管如此,比起繼承來還是更方便維護的。
使用組合的話,一般會使用「依賴注入」,比如這裡的 Veachle,並不需要特別指出是 Bike 還是 Car,只要有 move 方法就可以,這樣就可以很方便地替換,對於 People 來說不需要做任何改動。在 Objective-C 裡是通過 protocol 來實現的。
所以 Facebook 定義了一大堆的接口,包括 Delegate, DataSource 和 Protocol,ViewController 有 Protocol,也有 Delegate(如 FBMediaGalleryViewControllerDelegate),View / Cell 也有 Delegate(如 FBMediaGalleryViewDelegate),還有各種零零碎碎的 Protocol,如 FBDiscoveryCardProtocol, FBEventProtocol等。
定義接口的過程也是梳理架構的過程,如果對架構理解不夠深刻,是很難將接口恰當地抽象出來的。很多人放棄使用組合,有一部分原因也是架構上的不合理。
組件的粒度也是個問題,過細會導致組件過多,組合的過程就會花去很多時間;過粗又導致組件臃腫,難以復用。
當組件的接口定義完之後,使用起來大概會是這樣:
@interface FBResponseHandler : NSObject @interface FBPhotoViewController : UIViewController
這樣一眼就大概能看出來這個 Class 大概會有哪些功能,如果某個組件要作調整,只需修改一處,就可以全局通用。
適度使用繼承,可以在易維護和便利上達到平衡,比如 FBTableViewController, FBDialog 等,自定義的組件可以在它們的基礎上進行開發。繼承的層次一般不超過2層,比如 UITableViewController <- FBTableViewController <- FBFriendsNearbyTableViewController
依賴注入
前面講過,組合往往和依賴注入搭配使用,Facebook 主要是通過 FBProvider, FBProviderMapData, FBProviderMap 來實現依賴注入的。
Provider 會產生一個 Object,比如 CameraControllerProvider 調用 get 方法後,會生成一個 MNCameraController 的實例。同時 Provider 還有兩個子類 SingletonProvider 和 BlockProvider,前者用來生成一個單例,後者用在需要初始化參數的情景。
ProviderMap 跟 ProviderMapData 有些重復,它們之間的關系我也沒有捋清,感覺 ProviderMap 像是一個 Manager,注冊了一堆 Provider,然後可以通過 Provider 的 ID 來找到之前注冊的 Provider。
模塊化
不光是在 Cocoa 開發領域,其他的編程領域也一樣,模塊化是一個理想的狀態,高內聚,低耦合。像 shell 命令一樣,接受參數或標准輸入,生成格式化的標准輸出,通過管道傳遞給其他支持標准輸入的命令行工具。
但現實場景要復雜的多,模塊化的實現也更加困難。Facebook 有一個 FBAppModule 協議
@protocol FBAppModule + (id )instanceForSession:(FBSession *)arg1 providerMap:(FBProviderMap *)arg2; @property(readonly, nonatomic) NSArray *supportedURLSchemes; @property(readonly, nonatomic) NSArray *supportedKeys; @property(retain, nonatomic) id activeMenuItem; @property(readonly, nonatomic) NSString *defaultIcon; @property(readonly, nonatomic) NSString *ID; - (UIViewController *)viewControllerForMenuItem:(id )arg1;
初始化時傳入一個 FBSession (後面會講到) 和 ProviderMap,然後設置支持的 url schemes,keys(具體作用未知),對應的 menuItem,icon(用於在 menuItem 顯示) 和 ID。
有了 Module ,自然還有 ModuleManager,它的作用是注冊 Module,當一個 url 過來時,可以遍歷 Module,看看是不是有模塊可以處理這個 url,有的話,就調用該 Module 的 openURL: 方法。當然也可以根據 ModuleID 來獲取 Module。
FBAppModule 是一個 Protocol,FBNativeAppModule 是對該協議的實現,所以具體的模塊都繼承該類。
導航管理
一般來說系統的 UINavigationController 已經夠使用了,如果需要更大的自由度和更高的可定制性,可以自定義一個導航管理器,Facebook 使用了 FBUINavigationController (Protocol) 來實現自定義導航的管理,屬性和方法跟系統的差不多。 它有多個實現:FBTariffedNavigationController, FBSwipeNavigationController, FBCustomNavigationController, FBNavigationController。前面講過繼承一般不超過2層,這裡是一般之外的情況,有3層。
MVVM
MVVM 是解決 Massive View Controller 的一個有效方法,獨立出一個 ViewModel 作為 View 的數據源,以及處理 View 的一些交互操作,而 VC 只需要將 ViewModel 和 View 關聯起來即可。一般會搭配某種綁定的實現,KVO 或 ReactiveCocoa 都可以,這樣 ViewModel 的數據有變化就可以自動映射到 View 上。
Facebook 也采用了這種方式,有一個 FBViewModel 基類
@interface FBViewModel : NSObject // 省略了一些相關性不大的屬性和方法 @property __weak FBViewModelManager *viewModelManager; // @synthesize viewModelManager=_viewModelManager; @property(nonatomic) unsigned int viewModelSource; // @synthesize viewModelSource=_viewModelSource; @property(retain, nonatomic) FBViewModelConfiguration *viewModelConfiguration; // @synthesize viewModelConfiguration=_viewModelConfiguration; @property(readonly, nonatomic) unsigned int viewModelVersion; // @synthesize viewModelVersion=_viewModelVersion; @property(readonly, nonatomic) NSString *viewModelUUID; // @synthesize viewModelUUID=_viewModelUUID; @property(retain) FBMemModelObject *memModel; // @synthesize memModel=_memModel; - (void)setNilValueForKey:(id)arg1; - (id)initWithViewModelUUID:(id)arg1 viewModelVersion:(unsigned int)arg2; - (void)setViewModelVersion:(unsigned int)arg1; - (id)humanDescription; - (void)loadPermanentDataModelObjectIDFromDataModelObjectID:(id)arg1 block:(CDUnknownBlockType)arg2; - (void)didUpdateWithChangedProperties:(id)arg1; @property __weak FBViewModelController *modelController; @property(nonatomic) int loadState; @end
Facebook 自己實現了一套 ViewModel 的更新通知機制,因為 ViewModel 都是 Immutable 的,所以無法改變,那麼就需要有一個地方去集中管理這些 ViewModel,有更新時可以及時通知到, FBViewModelController 應該就是干這事的,裡面有一個方法 - (void)_notifyViewModel:(id)arg1 didUpdateWithChanges:(id)arg2; 。但 FBViewModelManager 看起來更合適,二者的功能沒有太理清楚。
FBViewModelController 還有一個 Delegate,主要有3個方法 didUpdate[Delegate][Insert]ViewModel:,可以做一些事後的操作。
Builder Pattern
在定義一個 ViewController 時,往往需要接收很多個參數,以 initWith: 這種形式出現不太合適,除非你能容忍一個10行的方法聲明。通常的做法是把這些參數聲明為 property,然後在初始化 VC 後,對這些 property 賦值,然後在 ViewDidLoad 裡使用這些 property。這樣做有幾個問題:1) 不知道哪些是需要在 ViewDidLoad 前設置的,會出現忘了設置的現象。2) 這些屬性可以在外部被改動。 3) 代碼不夠優雅。
Builder Pattern 就是用來解決這個問題的,它跟工廠模式有點像。Facebook 也用到了這個模式,比如有一個 FBMUserFetchStatus 類,該類初始化時需要一些參數,於是就有了 FBMUserFetchStatusBuilder 類
@interface FBMUserFetchStatusBuilder : NSObject + (id)aMUserFetchStatusFromExistingMUserFetchStatus:(id)arg1; + (id)aMUserFetchStatus; - (id)withIdentifiers:(BOOL)arg1; - (id)withImageUrls:(BOOL)arg1; - (id)withHasVerifiedPhone:(BOOL)arg1; - (id)withCanInstallMessenger:(BOOL)arg1; - (id)withHasMessenger:(BOOL)arg1; - (id)withIsFriend:(BOOL)arg1; - (id)withNickname:(BOOL)arg1; - (id)withPhoneticName:(BOOL)arg1; - (id)withName:(BOOL)arg1; - (id)withUserId:(BOOL)arg1; - (id)build; @end
最後的 build 方法會生成一個 FBMUserFetchStatus 實例,有了這個 Builder 就知道有哪些參數是可以在初始化時進行設置的。
Data Manager
這是重頭戲,所以看起來略累,東西很多,很可能推斷錯誤。
先來看看實體類,首先是 FBEntityRequest
@protocol FBEntityRequestParse @optional + (BOOL)canParse:(id)arg1 error:(id *)arg2; @property(retain, nonatomic) NSError *syncError; @property(nonatomic, getter=isSyncing) BOOL syncing; - (unsigned int)parse:(id)arg1 request:(id )arg2 error:(id *)arg3; - (id )request; @end
所以實體都是可以被解析和同步的,還自帶了一個 Request。
再來看看 FBEntity
@protocol FBEntity + (NSURL *)entityURLForFBID:(NSString *)arg1; @property(readonly, nonatomic) NSURL *entityURL; @property(readonly, nonatomic, getter=isDataStale) BOOL dataStale; @property(retain, nonatomic) NSDate *lastSyncTime; @property(retain, nonatomic) NSString *fbid; @optional + (unsigned int)collection:(FBEntityCollection *)arg1 parse:(id)arg2 request:(id )arg3 error:(id *)arg4; + (id )collectionRequest:(FBEntityCollection *)arg1; @property(readonly, nonatomic) FBEntityDownloader *entityDownloader; - (NSSet *)parentEdges; - (NSSet *)parentCollections; - (void)entityInitializeWithFBID:(NSString *)arg1; @end
每個 Entity 都有一個 entityURL,或許可以用來同步? dataStale 應該是用來表示數據是否 dirty,如果是的話,可能需要同步。 還可以請求 Collection。
FBEntityCollection 跟 FBEntity 類似,不過多了 syncAll / memberClass / allObjects 這些屬性/方法。
再來看看數據請求,首先是 FBRequest,不太明白這個 Class 的具體功能,因為沒有 URL,一個沒有 URL 的 Request 能做什麼? 然後看到了 FBRequester,這個看起來是一個數據請求類,有 URL, responseHandler, connection狀態, delegate等。但這只是單個的請求,如何對多個請求進行管理呢,這時看到了 FBNetworker,它有 +sharedNetworker, requestQueue, cancelRequests:, addRequest: 所以就是它了。等等,為什麼下面還有一個 FBNetworkerRequest ?看起來像是 FBNetworker 的 Delegate,但不確定。
為了避免 URI 散落在各處,Facebook 還專門為 NSURL 寫了個 Category 來統一管理 URI。
@interface NSURL (FBFoundation) + (id)friendsNearbyURL; + (id)codeGeneratorURL; + (id)tagApprovalURLWithTagId:(id)arg1; + (id)tagApprovalURL; + (id)pokesURL; + (id)personExpandedAboutURLWithFBID:(id)arg1; // ...
還有一個 URL 生成類,FBURLRequestGenerator,該類保存了 appSecret 和 appVersion,生成的 URL 會自動帶上這些屬性。
其實還有很多,實在看不下來了···
Smarter Views
我們都知道 ViewController 自帶了一個 view,可以直接在這個 view 上 addSubview,正是由於這個便利性,很多創建 View 的代碼也擠在了 VC 裡,實在是不雅觀。
更好的方法是替換 VC 的 view 為自定義的 View,然後把這個自定義 View 獨立出去。比如在 -loadView 時覆蓋 view
@implementation MyProfileViewController - (void)loadView { self.view = [MyProfileView new]; }
可以同時重定義 view 的類型,如 @property (nonatomic) MyProfileView *view,讓編譯器明白 view 的類型已經變了。
因為看到了不少 VC 中都有 -loadView 方法,所以推斷可能使用了這項技術。
FBSession
在 Web 開發領域,Session 是用來保存用戶相關的信息的,FBSession 自然也不例外,不過它保存的內容還真是多呢。
@interface FBSession : NSObject + (void)setCurrentSession:(id)arg1; + (id)_globalSessionForDebugging; + (id)DO_NOT_USE_OR_YOU_WILL_BE_FIREDcurrentSession; @property(readonly) FBAPISessionStore *apiSessionStore; // @synthesize apiSessionStore=_apiSessionStore; @property(readonly) FBSessionDiskStore *sessionDiskStore; // @synthesize sessionDiskStore=_sessionDiskStore; @property(readonly) FBStore *store; // @synthesize store=_store; @property(readonly) NSString *appSecret; // @synthesize appSecret=_appSecret; @property(readonly, nonatomic, getter=isValid) BOOL valid; @property(readonly) BOOL hasUser; @property(readonly) NSString *userFBID; @property(retain) FBViewerContext *viewerContext; @property(retain) FBUserPreferences *userPreferences; @property(retain) FBPreferences *sessionPreferences; - (void)updateAccessToken:(id)arg1; - (id)updateActingViewer:(id)arg1; - (void)clearPreferences; - (void)invalidate; - (id)DO_NOT_USE_OR_YOU_WILL_BE_FIREDvalueForKeyRequiresUser:(id)arg1 withInitializer:(CDUnknownBlockType)arg2; - (id)valueForKey:(id)arg1 withInitializer:(CDUnknownBlockType)arg2; - (id)valueForKey:(id)arg1; - (id)initWithAppSecret:(id)arg1 store:(id)arg2 apiSessionStore:(id)arg3; @property(readonly, nonatomic) FBReactionController *reactionController; @property(readonly, nonatomic) FBLocationPingback *locationPingback; @property(readonly, nonatomic) FBAppSectionManager *appSectionManager; @property(readonly, nonatomic) FBBookmarkManager *bookmarkManager; // and many more...
Session 是可以保存到本地的,有一個狀態變量用來標識是否有效(valid),是否已登錄(hasUser),用戶的一些設置(這些設置會保存到本地),可以更新 AccessToken,還帶了各種 Controller 和 Manager,所以東西還是挺多的。
這裡有兩個特殊方法,使用後會被Fire···
Services
Service 顧名思義,提供某種服務,往往跟界面無關。從目錄層級上看,Service並不在Module裡面,也就是說這二者是獨立的,比如 FBTimelineModule 並不包含 FBTimelineService。
Service 之間可以有依賴,這裡是通過 startAppServiceWithDependencies: 來實現的,不過不清楚 Service 自身如何聲明依賴哪些其他的 Services。
Style
App 的 Style 是一個容易被忽視的地方,開發往往看著設計圖就開始寫了,這樣很容易造成樣式不統一,且將來調整起來也不方便。
Facebook 是通過 Category 來自定義樣式的,舉個簡單的例子:
@interface UIButton (FBMediaKit) + (id)fb_buttonTypeSystemWithTitle:(id)arg1; + (id)fb_buttonWithNormalImage:(id)arg1 highlightedImage:(id)arg2 selectedImage:(id)arg3; + (id)fb_buttonWithTemplateImage:(id)arg1; + (id)fb_buttonWithStyle:(int)arg1 title:(id)arg2; @end @interface UIButton (FBUIKit) + (id)fb_moreOptionsNavBarButton; + (id)fb_backArrowButtonWithText; + (id)fb_backArrowButtonWithRightPadding:(float)arg1; + (id)fb_backArrowButton; @end @interface UIButton (MNLoginFormAppearanceHelpers) + (id)phoneFormHeaderButton; + (id)singleSignOnButton; + (id)skipButton; + (id)formFieldButtonInvertedColors; @end
這樣也不用關心fontColor,margin,backgroundColor等,直接拿來用即可。
其他
從目錄結構上來看,Facebook 有 FBUIKit, FBFoundation, FBAppKit, Module。其中 FBUIKit 和 FBFoundation 是業務無關的,可以用在其他 App 上,FBAppKit 和 Module 是業務相關的。
Module 自帶資源,可以看成是一個 mini app。
使用了 EGODatabase, SDWebImage, SSZipArchive, CocoaLumberjack 這幾個開源類庫(可能還有更多)。
時間和能力有限,只能挖掘出這些信息,希望能帶來些幫助。