最近在看一些 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> 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 来实现的。

所以 Facebook 定义了一大堆的接口,包括 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

依赖注入

前面讲过,组合往往和依赖注入搭配使用,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 <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 已经够使用了,如果需要更大的自由度和更高的可定制性,可以自定义一个导航管理器,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 也采用了这种方式,www.bsck.org有一个 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) 不知道哪些是需要在新视觉影院6080 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:www.90168.org(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 散落在各处,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 <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 是一个容易被忽视的地方,开发往往看着设计图就开始写了,这样很容易造成样式不统一,且将来调整起来也不方便。

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 这几个开源类库(可能还有更多)。

时间和能力有限,只能挖掘出这些信息,希望能带来些帮助。

Facebook App 的头文件会有更多的收获的更多相关文章

  1. 解决javah生成.h头文件找不到找不到android.support.v7.app.AppCompatActivity的问题

    问题描写叙述: 在使用Android Studio进行JNI开发时,须要使用javah生成C或C++的头文件,可是可能会遇到: 错误: 无法訪问android.support.v7.app.AppCo ...

  2. App开发流程之通用宏定义及头文件

    工欲善其事,必先利其器. 在正式实现各种炫酷的功能和UI前,做好准备工作是提高后续开发效率的必经之路. 所以,这个系列,我不是在各种堆技术,更关注的是“兵马动”之前的“粮草行”,有些繁琐,但当清晰理出 ...

  3. App开发流程之增加预编译头文件

    在继续增加预编译头文件前,先稍等. Xcode为我们创建了一个模板项目,很棒!但有一点不太令人满意,问题就在下图中: 这是一个新项目的初始文件目录,几乎所有文件都在同一级目录下,随着项目文件数量急速增 ...

  4. jni.h头文件详解二

    作者:左少华 博客:http://blog.csdn.net/shaohuazuo/article/details/42932813 转载请注明出处:http://blog.csdn.net/shao ...

  5. 【转】 jni.h头文件详解(二)

    原文网址:http://blog.csdn.net/shaohuazuo/article/details/42932813 作者:左少华 博客:http://blog.csdn.net/shaohua ...

  6. Objective-C头文件导出工具class-dump

    首先,这个工具是开源的.作者网站:http://stevenygard.com/projects/class-dump/ 用途: 分析库文件或可执行文件,得到Objective-C类和部分C结构体的信 ...

  7. C/C++头文件使用 #ifndef #define #endif 的原因

    背景 在编译的时候,出现"redefine"的错误,最后检查才发现对应的头文件没有写正确的预编译信息: #ifndef _HeadFileName_H #define _HeadF ...

  8. [转]Linux学习笔记——例说makefile 头文件查找路径

    0.前言     从学习C语言开始就慢慢开始接触makefile,查阅了很多的makefile的资料但总感觉没有真正掌握makefile,如果自己动手写一个makefile总觉得非常吃力.所以特意借助 ...

  9. 单片机中用c编程时头文件reg51.h及reg52.h解析

    单片机中用c编程时头文件reg51.h及reg52.h解析 我们在用c语言编程是往往第一行就是reg51.h或者其他的自定义头文件,我们怎么样来理解呢? 1)“文件包含”处理. 程序的第一行是一个“文 ...

随机推荐

  1. 【虚拟机-网关】如何在使用应用程序网关和 Nginx 的环境下实现强制 HTTPS 跳转

    背景介绍 大家在使用 Nginx 部署网站时,实现 HTTP 到 HTTPS 的强制跳转是非常容易的事情,一般可以使用rewrite 命令或者使用返回自定义 301 页面的方法对 HTTP 请求进行 ...

  2. 单链表常见面试题(C语言实现)

    总结常见的单链表操作函数,复习使用,仅供参考,代码调试通过. #include<stdio.h> typedef struct node{ int data; struct node *n ...

  3. Mysql的介绍和安装注意

    1.Mysql所属公司:Oracle 2.数据库类型:关系型数据库 3.开发语言:C++ 4.版本:企业收费版和社区免费版 5.搭建Mysql环境 点击下一步直到遇到选择数据库编码的时候选择utf-8 ...

  4. 用代码判断当前系统是否支持某个版本的feature

    JDK9已经出来有一段时间了,因此很多流行的Java应用纷纷增添了对JDK9乃至JDK10的支持,比如Tomcat. 我们通过这个链接下载最新的Tomcat源文件包,总共7MB: https://to ...

  5. VC-基础:vs2010快捷键

    F12: 转到所调用过程或变量的定义 CTRL + SHIFT + B生成解决方案 CTRL + F7 生成编译 CTRL + O 打开文件 CTRL + SHIFT + O打开项目 CTRL + S ...

  6. data命令详解

    Linux date命令的用法 在linux shell编程中,经常用到日期的加减运算 以前都是自己通过expr函数计算,很麻烦 其实date命令本身提供了日期的加减运算 非常方便.例如:得到昨天的时 ...

  7. 洛谷 P2568 GCD

    https://www.luogu.org/problemnew/show/P2568#sub 最喜欢题面简洁的题目了. 本题为求两个数的gcd是素数,那么我们将x和y拆一下, 假设p为$gcd(x, ...

  8. Vue表单输入绑定

    <h3>基础用法</h3> <p>你可以用<strong>v-model</strong>指令在表单input,textarea以及sele ...

  9. python3 输入某年某月某日,判断这一天是这一年的第几天?

    题目 输入某年某月某日,判断这一天是这一年的第几天? 程序分析 特殊情况,闰年时需考虑二月多加一天. 代码: import calendar year = int(input("Year:& ...

  10. OpenCV中的绘图函数

    OpenCV可以用来绘制不同的集合图形,包括直线,矩形,圆,椭圆,多边形以及在图片上添加文字.用到的绘图函数包括 cv2.line(),cv2.circle(),cv2.rectangle() ,cv ...