mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

跨平台APP开发实践(RN、Flutter) #37

Open Ulanxx opened 4 years ago

Ulanxx commented 4 years ago

跨平台APP开发实践(RN、Flutter)


0. 前言

跨平台的优势

  • 真正的原生应用:产生的不是网页应用,不是混合应用,而是一个原生的移动应用。
  • 快速开发应用:相比原生漫长的编译过程,Hot Reload简直不要太爽。
  • 可随时呼叫原生外援:完美兼容Java/Swift/OC的组件,一部分用原生一部分用RN来做完全可以。
  • 跨平台:一套业务逻辑代码可以稳定运行在两个平台。
  • 节省劳动力:为企业节省劳动力。。。(不知道算不算好事儿)。

可以看出RN和Flutter还是呈五五开的发展态势。 github:

但是Flutter是在18年底才发行了以第一个稳定版,而React Native是15年就已经推出。这么一看,Flutter突然🔥起来,就1年的时间就挤掉了RN的大半市场,今天我们一起看一下,这两个跨平台的框架究竟有什么神奇的地方。


1. React Native的入门与实践

React Native是带着React的光环出生的一个跨平台框架,具备React的一切新特性,让从Ionic与HBuilder的时代走过的Hybrid的开发欲罢不能。因为他能通过React的代码与通用的业务逻辑,编写一套完全原生的App应用,而且APP的使用感受与OC/JAVA编写的Native APP完全一致。

1. 搭建环境

2. 创建应用

react-native init demo

3. 写法与代码结构

React Native和React的基本业务逻辑与项目结构是相通的,除了组件是从react-native的包里引用,样式是css的子集,其他的都是页面的生命周期,渲染逻辑,diff都与React无异。 image

import React from 'react';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  StatusBar,
} from 'react-native';

import {
  Header,
  LearnMoreLinks,
  Colors,
  DebugInstructions,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={styles.scrollView}>
          <Header />
          {global.HermesInternal == null ? null : (
            <View style={styles.engine}>
              <Text style={styles.footer}>Engine: Hermes</Text>
            </View>
          )}
          <View style={styles.body}>
            <View style={styles.sectionContainer}>
              <Text style={styles.sectionTitle}>Step One</Text>
              <Text style={styles.sectionDescription}>
                Edit <Text style={styles.highlight}>App.js</Text> to change this
                screen and then come back to see your edits.
              </Text>
            </View>
            <View style={styles.sectionContainer}>
              <Text style={styles.sectionTitle}>See Your Changes</Text>
              <Text style={styles.sectionDescription}>
                <ReloadInstructions />
              </Text>
            </View>
            <View style={styles.sectionContainer}>
              <Text style={styles.sectionTitle}>Debug</Text>
              <Text style={styles.sectionDescription}>
                <DebugInstructions />
              </Text>
            </View>
            <View style={styles.sectionContainer}>
              <Text style={styles.sectionTitle}>Learn More</Text>
              <Text style={styles.sectionDescription}>
                Read the docs to discover what to do next:
              </Text>
            </View>
            <LearnMoreLinks />
          </View>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: Colors.lighter,
  },
  engine: {
    position: 'absolute',
    right: 0,
  },
  body: {
    backgroundColor: Colors.white,
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: Colors.black,
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
    color: Colors.dark,
  },
  highlight: {
    fontWeight: '700',
  },
  footer: {
    color: Colors.dark,
    fontSize: 12,
    fontWeight: '600',
    padding: 4,
    paddingRight: 12,
    textAlign: 'right',
  },
});

export default App;

Android/IOS目录分别承载这各自应用架构与bundle入口,src部分会打包成jsbundle,然后通过Native的入口注入。

4.RN的运行机制

看到这里会有这样的一个疑问为什么js代码可以运行在APP中?

是因为RN有两个核心

  • JSC引擎:1 → 因为RN的包里一个有JS执行引擎(WebKit的内核JavaScriptCore),所以它可以运行js代码。(前期是JSC的环境,在0.60.x之后添加了Hermes作为js引擎)。

干货 | 加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

React Native JSC源码

  • JSI通信:其实就是JSBridge,作为JS与Native的桥梁,运行在JSC环境下,通过C++实现的Native类的代理对象,这样就可以实现JS与Native通信。

所以:JSC/Hermes会将作为JS的运行环境(解释器),JS层通过JSI获取到对应的C++层的module对象的代理,最终通过JNI回调Java层的module,在通过JNI映射到Native的函数。

RN Native Android Module源码

RN Native IOS Module源码

所以,RN中所有的标签其实都不是真是的控件,js代码中所有的控件,都是一个“Map对中的key”,JS通过这个key组合的DOM,放到VDOM的js数据结构中,然后通过JSBridge代理到Native,Native端会解析这个DOM,从而获得对应的Native的控件。

5.怎么实现一个Native Bridge的功能。(先不讲)

例子:实现判断应用是否开启通知,如果未打开通知则进入设置页面开启通知。

5.1 IOS端

#import <Foundation/Foundation.h>
#import <React/RCTEventEmitter.h>

@interface RNDataTransferManager : RCTEventEmitter <RCTBridgeModule>

@end
#import "RNDataTransferManager.h"

@implementation RNDataTransferManager

RCT_EXPORT_MODULE();
// 判断notification是否开启
RCT_EXPORT_METHOD(isNotificationEnabled:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
  BOOL isEnable = NO;
  UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];
  isEnable = (UIUserNotificationTypeNone == setting.types) ? NO : YES;
  return resolve(@(isEnable));
}

// 进入设置开启Notification
RCT_EXPORT_METHOD(gotoOpenNotification) {
  [self goToAppSystemSetting];
}

注意两个宏:

RCT_EXPORT_METHOD:用来设置给JS导出的Native Module名字。

RCT_EXPORT_MODULE:给JS提供的方法通过RCT_EXPORT_METHOD()宏实现,必须明确的声明要给 JavaScript 导出的方法,否则 React Native 不会导出任何方法。

5.2 Android端

首先新建一个JavaModule类继承ReactContextBaseJavaModule。

public class RNDataTransferManager extends ReactContextBaseJavaModule {

    private static ReactApplicationContext reactContext;

    public static RNDataTransferManager rnDataTransferManager;

    public static String currentBindAlias = "";

    public RNDataTransferManager(@Nonnull ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    public static RNDataTransferManager getInstance() {
        if (null == rnDataTransferManager) {
            rnDataTransferManager = new RNDataTransferManager(reactContext);
        }
        return rnDataTransferManager;
    }

    @Nonnull
    @Override
    public String getName() {
        return "RNDataTransferManager";
    }

        @ReactMethod
    public void isNotificationEnabled(Promise promise) {
        if (promise != null) {
            if (MainApplication.getContext() != null) {
                if (NotificationManagerCompat.from(MainApplication.getContext())
                        .areNotificationsEnabled()) {
                    Log.e("push", "推送开启 isNotificationEnabled -> true");
                    promise.resolve(true);
                } else {
                    Log.e("push", "推送未开启 isNotificationEnabled -> false");
                    promise.resolve(false);
                }
            } else {
                promise.resolve(false);
            }
        }
    }

    @ReactMethod
    public boolean gotoOpenNotification() {
        if (MainApplication.getContext() == null) {
            return false;
        }
        Intent intent = getSetIntent(MainApplication.getContext());
        PackageManager packageManager = MainApplication.getContext().getPackageManager();
        List<ResolveInfo> list = packageManager.queryIntentActivities(intent, 0);
        if (list != null && list.size() > 0) {
            try {
                MainApplication.getContext().startActivity(intent);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }

}

写好了Native Module之后需要注册模块。

1)首先通过ReactPackage的createNativeModules来注册模块。

package com.mengtuiapp.mms.bridge;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.annotation.Nonnull;

public class DataTransferPackage implements ReactPackage {

    private RNDataTransferManager transferModule;

    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> nativeModules = new ArrayList<>();
        transferModule = new RNDataTransferManager(reactContext);
        RNDataTransferManager.rnDataTransferManager = transferModule;
        nativeModules.add(transferModule);
        return nativeModules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

2)然后让你的应用拿到注册到的package,需要在Application的getPackages方法中提供。

   @Override
   protected List<ReactPackage> getPackages() {
                    List<ReactPackage> packages = new PackageList(this).getPackages();
                    packages.add(new DataTransferPackage());
                    packages.add(new RNInstallApkPackage());
                    packages.add(new RNUserAgentPackage());
                    packages.add(new RNKeyboardAdjustPackage());
                    packages.add(new CodePush(mContext.getString(R.string.InnotechCodepushKey), mContext, this.moduleId, BuildConfig.DEBUG, mContext.getString(R.string.InnotechCodepushServerUrl)));
                    return packages;
   }

5.3 JS端调用

NativeModules.RNDataTransferManager.gotoOpenNotification()
就可以前往应用设置页面打开通知。

6. 这样做的优势和问题

优势:

  • 相比Hybrid性能更高、因为都是原生组件的渲染。
  • 从render到virtual dom的过程都是React驱动,具备React的一切优秀特性,可以使用React的社区优秀工具。
  • 项目搭建起来了,用JS写APP又具有原生的渲染效率简直爽,一份代码Android、IOS、web都可以适配(毕竟vDom层是一样的,jsbridge就随你魔改了)。
  • 相比原生的编译速度,开发JS使用HotReload简直太爽了。

问题:

  • 跨平台,但是Android、IOS毕竟是不同的系统与生态,组件与功能都有一些跨平台的差异,RN的原生组件的平台差异性很大。
  • 性能问题:动画性能不好、列表数据量大性能不好,主要集中在低端机,大数据列表快速滑动会有白屏,动画层级多在Android低于30fps的情况频繁。
  • 白屏问题,加载bundle的时间会有一个白屏出现,需要手动改Native代码。
  • 开发业务功能不需要原生能力,但是开发一个完整的跨平台项目,是需要具备一定的双端原生能力(有很多要写Native的,许多功能和组件也需要自己封装)
  • 这也是RN做的不好的地方,版本迭代太慢,不痛不痒的迭代了5年了,很多问题还是没有解决。这也是Flutter为什么这么火的原因。

7. 那么Flutter怎么做的

Flutter使用Dart作为开发语言,作为一个AOT框架,Flutter是在运行时直接将Dart转化成客户端的可执行文件,然后通过Skia渲染引擎直接渲染到硬件平台。如果说RN是为开发者做了平台兼容,那Flutter更像是为开发者屏蔽了平台的概念。RN需要被转译为本地对应的组件,而Flutter是直接编译成可执行文件并渲染到设备。Flutter可以直接控制屏幕上的每一个像素,这样就可以避免由于使用JSBridge导致的性能问题。

三要素:

  • Dart语言开发。
  • 任何Dart代码都是AOT运行前编译成本地可执行文件,使用Skia(渲染引擎)直接渲染到本机。
  • 不使用原生的组件,具有自己的widget库,开发时构建自己的widget树来画页面。
如果是页面级的应用来说,Flutter是不需要任何原生代码来写组件,所有组件和页面都可以通过Flutter直接写好。

7.1 Flutter代码结构

image

7.2 Flutter运行与调试

直接演示。

8. 总结

今天我们主要看了一下两个框架开发时的代码结构,与代码书写形式,还有简单了解了一下它是怎么运行。那之后如果小伙伴想继续去学习,或做一个自己的应用,还有以下几个方面需要注意:

1. APP初始化与生命周期状态。
2. 数据持久化 - 数据管理、SP、本地数据库。
3. 碎片化处理。
4. 打包三要素:Android(混淆、签名、加固),IOS(生成证书、导入证书、使用证书)。
5. 拆包、热更新、原生集成。

Flutter因为自带了渲染引擎,理论上是要比RN渲染效率要高,但是其实实际使用上,在性能过剩的移动端设备中,并没有出现特别大的差异,而Facebook的团队在Flutter的持续施压之下也决定重构底层,并在最近几个版本有了一些进步,所以大家有兴趣的都可以研究一下。