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

898 查看

近期开始研究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啊。
<!-- more -->

iOS f8app效果展示

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


f8appiOS

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文件分析

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代码分析

从项目的入口类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滚动UI组件代码分析

然后看下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。