Facebook F8App-ReactNative项目源码分析

作者: offbye西涛 | 来源:发表于2016-06-08 14:56 被阅读1195次

    近期开始研究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工程的结构:

    .
    ├── 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。

    相关文章

      网友评论

      本文标题:Facebook F8App-ReactNative项目源码分析

      本文链接:https://www.haomeiwen.com/subject/xfejdttx.html