近期开始研究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模拟器上动效还是很好的。
首先还是先看下ios工程的结构:
.
├── 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是CocosPod的配置文件,是ruby语言写的,定义了用到的第三方模块,和一些处理过程。
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看起,
#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,这个是RN的滚动UI组件,在f8app/js/common/ListContainer.js中用到了这个组件的js代码。我们看看代码里做了哪些事情:
#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,下面是它的定义
/**
* 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)