讀 非死book App 頭文件的一些收獲

jopen 11年前發布 | 27K 次閱讀 Facebook C/C++開發

最近在看一些 App 架構相關的文章,也看了 非死book 分享的兩個不同時期的架構(2013 和 2014),于是就想一窺 非死book App 的頭文件,看看會不會有更多的收獲,確實有,還不少。由于在選擇 ipa 上的失誤,下了個 7.0 版的 非死book(最新的是 18.1),會稍有過時,不過后來又下了個 18.1 的看了下,發現變動其實不大。以下是我從頭文件中獲取到的一些信息(20多萬行,瀏覽起來還是挺累的)

讓視圖組件可以方便地配置

這個在 非死book 的演講中也提到過,自定義的 UI 組件在初始化時可以傳一些數值來表示想要呈現的效果,就像 HTML 和 CSS 一樣,Dom 結構表示這是什么,CSS 對該結構進行個性化定制。 非死book 是通過 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> veachle;
- (void)move;
@end

@implementation People
- (id)initWithVeachle: (id <Veachle>)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 來實現的。

所以 非死book 定義了一大堆的接口,包括 Delegate, DataSource 和 Protocol,ViewController 有 Protocol,也有 Delegate(如 FBMediaGalleryViewControllerDelegate),View / Cell 也有 Delegate(如 FBMediaGalleryViewDelegate),還有各種零零碎碎的 Protocol,如 FBDiscoveryCardProtocol, FBEventProtocol等。

定義接口的過程也是梳理架構的過程,如果對架構理解不夠深刻,是很難將接口恰當地抽象出來的。很多人放棄使用組合,有一部分原因也是架構上的不合理。

組件的粒度也是個問題,過細會導致組件過多,組合的過程就會花去很多時間;過粗又導致組件臃腫,難以復用。

當組件的接口定義完之后,使用起來大概會是這樣:

@interface FBResponseHandler : NSObject <FBTestable, FBReceivedDataBufferDelegate, FBResponseHandlerProtocol>

@interface FBPhotoViewController : UIViewController <FBPagingViewDelegate, FBPagingViewDataSource, FBPresentableViewController>

這樣一眼就大概能看出來這個 Class 大概會有哪些功能,如果某個組件要作調整,只需修改一處,就可以全局通用。

適度使用繼承,可以在易維護和便利上達到平衡,比如 FBTableViewController, FBDialog 等,自定義的組件可以在它們的基礎上進行開發。繼承的層次一般不超過2層,比如 UITableViewController <- FBTableViewController <- FBFriendsNearbyTableViewController

依賴注入

前面講過,組合往往和依賴注入搭配使用,非死book 主要是通過 FBProvider, FBProviderMapData, FBProviderMap 來實現依賴注入的。

Provider 會產生一個 Object,比如 CameraControllerProvider 調用 get 方法后,會生成一個 MNCameraController 的實例。同時 Provider 還有兩個子類 SingletonProvider 和 BlockProvider,前者用來生成一個單例,后者用在需要初始化參數的情景。

ProviderMap 跟 ProviderMapData 有些重復,它們之間的關系我也沒有捋清,感覺 ProviderMap 像是一個 Manager,注冊了一堆 Provider,然后可以通過 Provider 的 ID 來找到之前注冊的 Provider。

模塊化

不光是在 Cocoa 開發領域,其他的編程領域也一樣,模塊化是一個理想的狀態,高內聚,低耦合。像 shell 命令一樣,接受參數或標準輸入,生成格式化的標準輸出,通過管道傳遞給其他支持標準輸入的命令行工具。

但現實場景要復雜的多,模塊化的實現也更加困難。非死book 有一個 FBAppModule 協議

@protocol FBAppModule <NSObject>
+ (id <FBAppModule>)instanceForSession:(FBSession *)arg1 providerMap:(FBProviderMap *)arg2;
@property(readonly, nonatomic) NSArray *supportedURLSchemes;
@property(readonly, nonatomic) NSArray *supportedKeys;
@property(retain, nonatomic) id <FBMenuItem> activeMenuItem;
@property(readonly, nonatomic) NSString *defaultIcon;
@property(readonly, nonatomic) NSString *ID;                                                                                                                                                         
- (UIViewController *)viewControllerForMenuItem:(id <FBMenuItem>)arg1;

初始化時傳入一個 FBSession (后面會講到) 和 ProviderMap,然后設置支持的 url schemes,keys(具體作用未知),對應的 menuItem,icon(用于在 menuItem 顯示) 和 ID

有了 Module ,自然還有 ModuleManager,它的作用是注冊 Module,當一個 url 過來時,可以遍歷 Module,看看是不是有模塊可以處理這個 url,有的話,就調用該 Module 的 openURL: 方法。當然也可以根據 ModuleID 來獲取 Module。

FBAppModule 是一個 Protocol,FBNativeAppModule 是對該協議的實現,所以具體的模塊都繼承該類。

導航管理

一般來說系統的 UINavigationController 已經夠使用了,如果需要更大的自由度和更高的可定制性,可以自定義一個導航管理器,非死book 使用了 FBUINavigationController (Protocol) 來實現自定義導航的管理,屬性和方法跟系統的差不多。 它有多個實現:FBTariffedNavigationController, FBSwipeNavigationController, FBCustomNavigationController, FBNavigationController。前面講過繼承一般不超過2層,這里是一般之外的情況,有3層。

MVVM

MVVM 是解決 Massive View Controller 的一個有效方法,獨立出一個 ViewModel 作為 View 的數據源,以及處理 View 的一些交互操作,而 VC 只需要將 ViewModel 和 View 關聯起來即可。一般會搭配某種綁定的實現,KVO 或 ReactiveCocoa 都可以,這樣 ViewModel 的數據有變化就可以自動映射到 View 上。

非死book 也采用了這種方式,有一個 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

非死book 自己實現了一套 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 就是用來解決這個問題的,它跟工廠模式有點像。非死book 也用到了這個模式,比如有一個 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 <FBRequest>)arg2 error:(id *)arg3;
- (id <FBRequest>)request;
@end

所以實體都是可以被解析和同步的,還自帶了一個 Request。

再來看看 FBEntity

@protocol FBEntity <FBEntityRequestParse, NSObject>                                                                                                                                                  
+ (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 <FBRequest>)arg3 error:(id *)arg4;
+ (id <FBRequest>)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 散落在各處,非死book 還專門為 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 <FBInvalidating>

+ (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 是一個容易被忽視的地方,開發往往看著設計圖就開始寫了,這樣很容易造成樣式不統一,且將來調整起來也不方便。

非死book 是通過 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等,直接拿來用即可。

其他

從目錄結構上來看,非死book 有 FBUIKit, FBFoundation, FBAppKit, Module。其中 FBUIKit 和 FBFoundation 是業務無關的,可以用在其他 App 上,FBAppKit 和 Module 是業務相關的。

Module 自帶資源,可以看成是一個 mini app。

使用了 EGODatabase, SDWebImage, SSZipArchive, CocoaLumberjack 這幾個開源類庫(可能還有更多)。

時間和能力有限,只能挖掘出這些信息,希望能帶來些幫助。

原文鏈接: http://blog.leezhong.com/ios/2014/11/28/非死book-app-headers.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!