Facebook F8App-ReactNative项目源码分析5-iOS篇

近期开始研究Facebook f8app项目,目标是理解Facebook官方React Native f8app的整体技术架构,给公司目前几个的React Native项目开发提供官方经验借鉴,并对原生开发和React Native开发进行框架层面的融合。
本文分析f8app iOS代码的结构和技术实现,阅读本文的前提是对React Native和iOS开发有一定的了解。
f8app ios项目使用了CocosPod管理模块,现在RN的最新版本创建的项目默认已经不再使用CocosPods了,直接通过工程引用。f8app还是用了CocosPod,因此我们首先需要在ios目录下运行pod install,安装好依赖的项目,然后用Xcode打开F8v2.xcworkspace工作空间,注意不是打开F8v2.xcodeproj工程文件,我经常犯这个错误,实在不喜欢用CocosPods啊。

iOS f8app效果展示

先看下效果吧,在iOS模拟器上动效还是很好的。

f8appiOS

ios工程结构

首先还是先看下ios工程的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
.
├── Default-568h@2x.png
├── F8Scrolling.h
├── F8Scrolling.m
├── F8v2
│   ├── AppDelegate.h
│   ├── AppDelegate.m
│   ├── Base.lproj
│   │   └── LaunchScreen.xib
│   ├── Images.xcassets
│   │   ├── AppIcon.appiconset
│   │   │   ├── AppIcon@2x.png
│   │   │   ├── AppIcon@3x.png
│   │   │   └── Contents.json
│   │   └── Contents.json
│   ├── Info.plist
│   └── main.m
├── F8v2.xcodeproj
├── F8v2.xcworkspace
├── F8v2Tests
│   └── Info.plist
├── PodFile
├── Podfile.lock
├── Pods
│   ├── Bolts
│   │   ├── Bolts
│   │   │   ├── Common
│   │   │   └── iOS
│   │   ├── LICENSE
│   │   └── README.md
│   ├── FBSDKCoreKit
│   │   ├── FBSDKCoreKit
│   │   │   └── FBSDKCoreKit
│   │   ├── LICENSE
│   │   └── README.mdown
│   ├── FBSDKLoginKit
│   │   ├── FBSDKLoginKit
│   │   │   └── FBSDKLoginKit
│   │   ├── LICENSE
│   │   └── README.mdown
│   ├── FBSDKShareKit
│   │   ├── FBSDKShareKit
│   │   │   └── FBSDKShareKit
│   │   ├── LICENSE
│   │   └── README.mdown
│   ├── Headers
│   │   ├── Private
│   │   │   ├── Bolts
│   │   │   ├── CodePush
│   │   │   ├── FBSDKCoreKit
│   │   │   ├── FBSDKLoginKit
│   │   │   ├── FBSDKShareKit
│   │   │   ├── React
│   │   │   ├── react-native-fbsdkcore
│   │   │   ├── react-native-fbsdklogin
│   │   │   └── react-native-fbsdkshare
│   │   └── Public
│   │   ├── Bolts
│   │   ├── CodePush
│   │   ├── FBSDKCoreKit
│   │   ├── FBSDKLoginKit
│   │   ├── FBSDKShareKit
│   │   ├── React
│   │   ├── react-native-fbsdkcore
│   │   ├── react-native-fbsdklogin
│   │   └── react-native-fbsdkshare
│   ├── Local\ Podspecs
│   │   ├── CodePush.podspec.json
│   │   ├── React.podspec.json
│   │   ├── react-native-fbsdkcore.podspec.json
│   │   ├── react-native-fbsdklogin.podspec.json
│   │   └── react-native-fbsdkshare.podspec.json
│   ├── Manifest.lock
│   ├── Pods.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── xcshareddata
│   │   │   └── xcschemes
│   └── Target\ Support\ Files
│   ├── Bolts
│   │   ├── Bolts-dummy.m
│   │   ├── Bolts-prefix.pch
│   │   └── Bolts.xcconfig
│   ├── CodePush
│   │   ├── CodePush-dummy.m
│   │   ├── CodePush-prefix.pch
│   │   └── CodePush.xcconfig
│   ├── FBSDKCoreKit
│   │   ├── FBSDKCoreKit-dummy.m
│   │   ├── FBSDKCoreKit-prefix.pch
│   │   └── FBSDKCoreKit.xcconfig
│   ├── FBSDKLoginKit
│   │   ├── FBSDKLoginKit-dummy.m
│   │   ├── FBSDKLoginKit-prefix.pch
│   │   └── FBSDKLoginKit.xcconfig
│   ├── FBSDKShareKit
│   │   ├── FBSDKShareKit-dummy.m
│   │   ├── FBSDKShareKit-prefix.pch
│   │   └── FBSDKShareKit.xcconfig
│   ├── Pods-F8v2
│   │   ├── Pods-F8v2-acknowledgements.markdown
│   │   ├── Pods-F8v2-acknowledgements.plist
│   │   ├── Pods-F8v2-dummy.m
│   │   ├── Pods-F8v2-frameworks.sh
│   │   ├── Pods-F8v2-resources.sh
│   │   ├── Pods-F8v2.debug.xcconfig
│   │   └── Pods-F8v2.release.xcconfig
│   ├── React
│   │   ├── React-dummy.m
│   │   ├── React-prefix.pch
│   │   └── React.xcconfig
│   ├── react-native-fbsdkcore
│   │   ├── react-native-fbsdkcore-dummy.m
│   │   ├── react-native-fbsdkcore-prefix.pch
│   │   └── react-native-fbsdkcore.xcconfig
│   ├── react-native-fbsdklogin
│   │   ├── react-native-fbsdklogin-dummy.m
│   │   ├── react-native-fbsdklogin-prefix.pch
│   │   └── react-native-fbsdklogin.xcconfig
│   └── react-native-fbsdkshare
│   ├── react-native-fbsdkshare-dummy.m
│   ├── react-native-fbsdkshare-prefix.pch
│   └── react-native-fbsdkshare.xcconfig
├── Settings.bundle
│   ├── About.plist
│   ├── Root.plist
│   └── en.lproj
│   └── Root.strings
├── Splash@2x.png
└── build

PodFile文件分析

PodFile是CocosPod的配置文件,是ruby语言写的,定义了用到的第三方模块,和一些处理过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
source 'https://github.com/CocoaPods/Specs.git'

target 'F8v2' do
pod 'React', :subspecs => [
'Core',
'RCTActionSheet',
'RCTImage',
'RCTNetwork',
'RCTText',
'RCTWebSocket',
'RCTPushNotification',
'RCTLinkingIOS',
'RCTVibration',
], :path => '../node_modules/react-native'
pod 'react-native-fbsdkcore', :path => '../node_modules/react-native-fbsdk/iOS/core'
pod 'react-native-fbsdklogin', :path => '../node_modules/react-native-fbsdk/iOS/login'
pod 'react-native-fbsdkshare', :path => '../node_modules/react-native-fbsdk/iOS/share'
pod 'CodePush', :path => '../node_modules/react-native-code-push'
end

# Start the React Native JS packager server when running the project in Xcode.

start_packager = %q(
if nc -w 5 -z localhost 8081 ; then
if ! curl -s "http://localhost:8081/status" | grep -q "packager-status:running" ; then
echo "Port 8081 already in use, packager is either not running or not running correctly"
exit 2
fi
else
open $SRCROOT/../../node_modules/react-native/packager/launchPackager.command || echo "Can't start packager automatically"
fi
)

post_install do |installer|
target = installer.pods_project.targets.select{|t| 'React' == t.name}.first
phase = target.new_shell_script_build_phase('Run Script')
phase.shell_script = start_packager
end

通过CocosPod引入了React,react-native-fbsdkcore,react-native-fbsdklogin,react-native-fbsdkshare,CodePush这些模块,
最后会尝试启动React Native packager。

入口类AppDelegate.m代码分析

从项目的入口类AppDelegate.m看起,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <CodePush/CodePush.h>

#import "AppDelegate.h"

#import "RCTRootView.h"
#import "RCTPushNotificationManager.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;

#ifdef DEBUG
NSString *ip = [[NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"] encoding:NSUTF8StringEncoding error:nil] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\n"]];

if (!ip) {
ip = @"127.0.0.1";
}

jsCodeLocation = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:8081/index.ios.bundle?platform=ios&dev=true", ip]];
#else
jsCodeLocation = [CodePush bundleURL];
#endif
NSLog(jsCodeLocation.absoluteString);

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"F8v2"
initialProperties:nil
launchOptions:launchOptions];

NSArray *objects = [[NSBundle mainBundle] loadNibNamed:@"LaunchScreen" owner:self options:nil];
UIImageView *loadingView = [[[objects objectAtIndex:0] subviews] objectAtIndex:0];
loadingView = [[UIImageView alloc] initWithImage:[loadingView image]];
loadingView.frame = [UIScreen mainScreen].bounds;

rootView.loadingView = loadingView;

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [[UIViewController alloc] init];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[[UIApplication sharedApplication] setStatusBarHidden:NO];
[self.window makeKeyAndVisible];

return YES;
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
[FBSDKAppEvents activateApp];
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return [[FBSDKApplicationDelegate sharedInstance] application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
[RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
[RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification
{
[RCTPushNotificationManager didReceiveRemoteNotification:notification];
}

@end

可以看到主要用了FBSDKCoreKit,CodePush热更新,RCTPushNotificationManager推送。
首先创建了RCTRootView rootView,RCTRootView就是RN页面的容器,我们只要在iOS ViewController中添加RCTRootView就可以展示RN的页面了。
然后从LaunchScreen.xib中取出一个子View作为rootView的加载页面loadingView,设置view的frame,创建一个rootViewController,并把它的view设置成RCTRootView
rootView,然后把UIWindow的rootViewController设成刚才创建的rootViewController,这些代码还是很简单的。
didRegisterUserNotificationSettings,didRegisterForRemoteNotificationsWithDeviceToken,didReceiveRemoteNotification几个方法是对推送通知的处理,也比较简单。

F8Scrolling.m滚动UI组件代码分析

然后看下F8Scrolling.m,这个是RN的滚动UI组件,在f8app/js/common/ListContainer.js中用到了这个组件的js代码。我们看看代码里做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#import <UIKit/UIKit.h>
#import <CoreGraphics/CoreGraphics.h>

#import "F8Scrolling.h"
#import "RCTUIManager.h"
#import "RCTScrollView.h"

@interface F8Scrolling () {
NSMapTable *_pinnedViews;
NSMapTable *_distances;
}

@end

@implementation F8Scrolling

@synthesize bridge = _bridge;

RCT_EXPORT_MODULE()

- (instancetype)init
{
if (self = [super init]) {
_pinnedViews = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableWeakMemory capacity:20];
_distances = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory capacity:20];
}
return self;
}

- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}

RCT_EXPORT_METHOD(pin:(nonnull NSNumber *)scrollViewReactTag
toView:(nonnull NSNumber *)pinnedViewReactTag
withDistance:(nonnull NSNumber *)distance)
{
UIView *pinnedView = [self.bridge.uiManager viewForReactTag:pinnedViewReactTag];
UIView *scrollView = [self.bridge.uiManager viewForReactTag:scrollViewReactTag];
if ([scrollView isKindOfClass:[RCTScrollView class]]) {
RCTScrollView *reactScrollView = (RCTScrollView *)scrollView;
[_pinnedViews setObject:pinnedView forKey:reactScrollView.scrollView];
[_distances setObject:distance forKey:reactScrollView.scrollView];
[reactScrollView setNativeScrollDelegate:self];
[self scrollViewDidScroll:reactScrollView.scrollView];
}
}

RCT_EXPORT_METHOD(unpin:(nonnull NSNumber *)scrollViewReactTag)
{
UIView *scrollView = [self.bridge.uiManager viewForReactTag:scrollViewReactTag];
if ([scrollView isKindOfClass:[RCTScrollView class]]) {
RCTScrollView *reactScrollView = (RCTScrollView *)scrollView;
[_pinnedViews removeObjectForKey:reactScrollView.scrollView];
[_distances removeObjectForKey:reactScrollView.scrollView];
[reactScrollView setNativeScrollDelegate:nil];
}
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UIView *pinnedView = [_pinnedViews objectForKey:scrollView];
if (!pinnedView) {
return;
}

CGFloat distance = [[_distances objectForKey:scrollView] doubleValue];
CGFloat y = MAX(0, distance - scrollView.contentOffset.y);
pinnedView.transform = CGAffineTransformMakeTranslation(0, y);
}

@end

首先是类定义,oc里面是用@interface定义类的,这个和Java的interface很不一样,protocol对应Java的接口interface。
@interface F8Scrolling : NSObject <RCTBridgeModule, UIScrollViewDelegate>
init构造函数初始化了_pinnedViews和_distances2个变量,都是NSMapTable类型,NSMapTable(顾名思义)更适合于一般意义的映射。这取决于它是如何构造的,NSMapTable可以处理的“key-to-object”样式映射的NSDictionary,但它也可以处理“object-to-object”的映射 - 也被称为“associative array”或简称为“map”。_pinnedViews的key和value都是weak引用,_distances的key是weak引用,value是强引用。

@synthesize bridge=_bridge;意思是说,bridge 属性为 _bridge 实例变量合成访问器方法。
也就是说,bridge属性生成存取方法是setBridge,这个setWindow方法就是_bridge变量的存取方法,它操作的就是_bridge这个变量。通过这个看似是赋值的这样一个操作,我们可以在@synthesize 中定义与变量名不相同的getter和setter的命名,籍此来保护变量不会被不恰当的访问。

methodQueue返回了main_queue,规定这个组件运行在UI线程,因为它是UI组件啊
然后是几个方法的定义,RCT_EXPORT_METHOD宏提供导出方法到js的能力,可以用RCT_REMAP_METHOD重新定义在js中的函数名,还可以让js方法异步返回Promise,下面是它的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Wrap the parameter line of your method implementation with this macro to
* expose it to JS. By default the exposed method will match the first part of
* the Objective-C method selector name (up to the first colon). Use
* RCT_REMAP_METHOD to specify the JS name of the method.
*
* For example, in ModuleName.m:
*
* - (void)doSomething:(NSString *)aString withA:(NSInteger)a andB:(NSInteger)b
* { ... }
*
* becomes
*
* RCT_EXPORT_METHOD(doSomething:(NSString *)aString
* withA:(NSInteger)a
* andB:(NSInteger)b)
* { ... }
*
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
*
* ## Promises
*
* Bridge modules can also define methods that are exported to JavaScript as
* methods that return a Promise, and are compatible with JS async functions.
*
* Declare the last two parameters of your native method to be a resolver block
* and a rejecter block. The resolver block must precede the rejecter block.
*
* For example:
*
* RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString
* resolver:(RCTPromiseResolveBlock)resolve
* rejecter:(RCTPromiseRejectBlock)reject
* { ... }
*
* Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from
* JavaScript will return a promise that is resolved or rejected when your
* native method implementation calls the respective block.
*
*/
#define RCT_EXPORT_METHOD(method) \
RCT_REMAP_METHOD(, method)

方法pin从名字就可以知道,功能是固定view的。通过self.bridge.uiManager viewForReactTag方法获取到view。
方法scrollViewDidScroll最后定义了pinnedView的transform动画效果,pinnedView.transform = CGAffineTransformMakeTranslation(0, y);

F8v2目录下的代码文件基本上就介绍完了,Info.plist文件定义了项目的一些基本属性,包括CodePush key等的自定义属性。

总结

f8app iOS的代码量还是比较少的,本文主要分析了AppDelegate.m和 F8Scrolling UI组件的代码。项目还用了BVLinearGradient渐变UI组件,通过工程引用的,代码也比较简单。另外还通过CocosPod引入了React,react-native-fbsdkcore,react-native-fbsdklogin,react-native-fbsdkshare,CodePush这些模块,可以参考ios/PodFile。

[本文独立博客地址](http://www.offbye.com)

Contents
  1. 1. iOS f8app效果展示
  2. 2. ios工程结构
  3. 3. PodFile文件分析
  4. 4. 入口类AppDelegate.m代码分析
  5. 5. F8Scrolling.m滚动UI组件代码分析
  6. 6. 总结
,