Skip to content

BM-laoli/Android-IOS-ReactNative

Repository files navigation

介绍

为了实现和探究ReactNative的分包功能,以及构建一个 相对从性能上 和 技术上都比较ok 的项目架构 而存在的一个库。你可以把它理解为一个 App的技术架构 方案。

重要细节(Android)

按照官方的教程踩坑的地方

1.1 注意集成的时候 和 发build 的时候 权限问题

你需要注意的点 权限问题Error调试弹出层ActiveyHttp在deb模式i下是否安全的问题

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapprnn">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    


    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyappRNN"
        android:usesCleartextTraffic="true"
        tools:targetApi="31">
        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
        <activity
            android:name=".MainActivity"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

1.2 onCreate 怎么样才是完整的?

实际上 在 官方的文档中 这里的代码是不完整的,比较全的代码在这里

  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
            }
        }

        SoLoader.init(this, false);

        mReactRootView = new ReactRootView(this);
        List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
        // 有一些第三方可能不能自动链接,对于这些包我们可以用下面的方式手动添加进来:
        // packages.add(new MyReactNativePackage());
        // 同时需要手动把他们添加到`settings.gradle`和 `app/build.gradle`配置文件中。

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackages(packages)
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 注意这里的MyReactNativeApp 必须对应"index.js"中的
        // "AppRegistry.registerComponent()"的第一个参数
        mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null);

        setContentView(mReactRootView);

    }
  1. build 的时候到底如何做呢?
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/com/your-company-name/app-package-name/src/main/assets/index.android.bundle --assets-dest android/com/your-company-name/app-package-name/src/main/res/

这个是官方给的shell 但实际上,对于我们的这个项目而言,它是这样构建的(等一下你问我怎么知道这样改是正确的?看RN的源码啦 弟弟,好吧有时间我会出一个文章来详解源码的细节)

react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./android/app/src/main/assets/index.android.bundle --assets-dest ./android/app/src/main/res/
  1. assets 资源在android 中到底如何进行的呢?

通过分析原来的项目,发现都是build 的阶段 会生成一份固定的资源路径文件,这里是一份详细的流程说明 ,https://juejin.cn/post/7113713363900694565,但是实际上打出来的包还是会在res资源下 进行载入,文件的名称变化而已,对于常用的 build apk 包查看是否符合预期,可以尝试使用 反编译工具进行查看 🔧https://cloud.tencent.com/developer/article/1904018

基于此建议业务包都采取http 加载资源

  1. 关于native 模块的集成

首先第一点要说明的就是 react-native 的cli 更新到9.x版本它不在支持 link ,什么意思呢?也就是说 “yarn react-native link xxx”会报错哈,

我们这里选用 react-native-device-info 做native 模块来验证,是否可用, 有下面几点需要注意

  • 9.x 下的cli 不需要link 只需要 yarn add 就完了 ,很方便

  • 注意把 settings.gradle 中的配置改了 (7.x 的 gradle 的管理方式不一样!不改的话,会报错的哈),注意要重载一下gradle ,到此为止 你的Android Native 模块已经可以正常使用了

    dependencyResolutionManagement {
      // 注意这里 要去掉 请见一个 github 的issuss https://github.com/realm/realm-java/issues/7374 ,以及 7.0 下 gradle 的管理文档
      // repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
      repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
  • 然后把 权限 加上 因为要读区mac 地址,所以 设备的wifi 权限要授予

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

    然后就用 TestNativeInfo.js 下的代码跑就好了。

重点 拆包方案

要想完善拆包方案,就必须对包 和RN的运行原理有所了解

1.1 说到拆包我们先了解 “包” 是什么, 由 什么组成

一个 包 bundle 说白了 就说 一些js 代码,只不过后缀叫 bundle ,它实际上是一些js 代码,只不过这些代码的运行 环境在RN 提供的环境 不是在浏览器,通过这些代码RN 引擎可以使用 Native 组件 渲染 出你想要的UI ,好 这就是 包 bundle。

一个rn 的bundle 主要由三部分构成

  1. 环境变量 和 require define 方法的预定义 (polyfills)

  2. 模块代码定义 (module define)

  3. 执行 (require 调用)

1.2 从一个简单的 RNDemo 分析 一个 简单的bundle 的构建

在根目录 下有一个RNDemo =>

import { StyleSheet, Text, View, AppRegistry } from "react-native";

class BU1 extends React.Component {
render() {
  return (
    <View style={styles.container}>
      <Text style={styles.hello}>BU1 </Text>
    </View>
  );
}
}

const styles = StyleSheet.create({
// -----省略
});

AppRegistry.registerComponent("Bu1Activity", () => BU1);

执行build 之后 ,我们来分析bundle 嘛

yarn react-native bundle --platform android --dev false --entry-file ./RNDemo.js --bundle-output ./android/app/src/main/assets/rn.android.bundle --assets-dest ./android/app/src/main/res --minify false --reset-cache

# 上面有几个参数 --minify false 不要混淆,--reset-cache 清理缓存 具体的可以看 @react-native-community/cli 源代码

首先我们前面说过 个rn 的bundle 主要由三部分构成 (polyfills、defined、require )

先看第一部分 polyfills 它从第 1行 一直到 第 799 行

// 第一句话
var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),
   __DEV__=false,
   process=this.process||{},
   __METRO_GLOBAL_PREFIX__='';
   process.env=process.env||{};
   process.env.NODE_ENV=process.env.NODE_ENV||"production";
//可以看到 它定义了 运行时的基本环境变量 __BUNDLE_START_TIME__、__DEV__、__METRO_GLOBAL_PREFIX__..... 其作用是给RN 的Native 容器识别的 ,我们这里不深入,你只需要 知道没有这个 RN 的Native 容器识别会异常! 报错闪退


// 解析来 是三个闭包立即执行 函数 ,重点是第一个 它定义了 __r ,__d, 这两个函数 就说后面 模块定义 和 模块执行的关键函数
global.__r = metroRequire;
global[__METRO_GLOBAL_PREFIX__ + "__d"] = define;
metroRequire.packModuleId = packModuleId;
var modules = clear();
function clear() {
 modules = Object.create(null);
 return modules;
}
var moduleDefinersBySegmentID = [];
var definingSegmentByModuleID = new Map();

// 下面的说 __r  的主要定义  
function metroRequire(moduleId) {
   var moduleIdReallyIsNumber = moduleId;
   var module = modules[moduleIdReallyIsNumber];
   return module && module.isInitialized ? module.publicModule.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
 }
 // 可以看到上述函数 的作用是 从 module(在下称它为 模块组册表 )看看 是否已经初始化 了 ,如果是 就导出 (exports) 如果没有就 加载一次 (guardedLoadModule)

 function guardedLoadModule(moduleId, module) {
   if (!inGuard && global.ErrorUtils) {
     inGuard = true;
     var returnValue;

     try {
       returnValue = loadModuleImplementation(moduleId, module);
     } catch (e) {
       global.ErrorUtils.reportFatalError(e);
     }

     inGuard = false;
     return returnValue;
   } else {
     return loadModuleImplementation(moduleId, module);
   }
 }
 // 上述函数 最重要的事情 就是 执行 loadModuleImplementation 函数,传递 moduleId 和 module 


 function loadModuleImplementation(moduleId, module) {
   if (!module && moduleDefinersBySegmentID.length > 0) {
     var _definingSegmentByMod;

     var segmentId = (_definingSegmentByMod = definingSegmentByModuleID.get(moduleId)) !== null && _definingSegmentByMod !== undefined ? _definingSegmentByMod : 0;
     var definer = moduleDefinersBySegmentID[segmentId];

     if (definer != null) {
       definer(moduleId);
       module = modules[moduleId];
       definingSegmentByModuleID.delete(moduleId);
     }
   }

   var nativeRequire = global.nativeRequire;

   if (!module && nativeRequire) {
     var _unpackModuleId = unpackModuleId(moduleId),
         _segmentId = _unpackModuleId.segmentId,
         localId = _unpackModuleId.localId;

     nativeRequire(localId, _segmentId);
     module = modules[moduleId];
   }

   if (!module) {
     throw unknownModuleError(moduleId);
   }

   if (module.hasError) {
     throw moduleThrewError(moduleId, module.error);
   }

   module.isInitialized = true;
   var _module = module,
       factory = _module.factory,
       dependencyMap = _module.dependencyMap;

   try {
     var moduleObject = module.publicModule;
     moduleObject.id = moduleId;
     factory(global, metroRequire, metroImportDefault, metroImportAll, moduleObject, moduleObject.exports, dependencyMap);
     {
       module.factory = undefined;
       module.dependencyMap = undefined;
     }
     return moduleObject.exports;
   } catch (e) {
     module.hasError = true;
     module.error = e;
     module.isInitialized = false;
     module.publicModule.exports = undefined;
     throw e;
   } finally {}
 }
// 上述 重要的函数就是  factory(global, metroRequire, metroImportDefault, metroImportAll, moduleObject, moduleObject.exports, dependencyMap); 。它复杂执行模块的代码 ,好了 到这里为止我们就够了,现在不用分析太深入,要特别注意的是 factory  不是 定义好的函数,而是传入 的函数 ! factory = _module.factory, 具体点来说,它的执行是依据每个模块 的传入参数来执行的


// 然后我们来看看 __d define  ,这个东西就比较的简单了
function define(factory, moduleId, dependencyMap) {
   if (modules[moduleId] != null) {
     return;
   }

   var mod = {
     dependencyMap: dependencyMap,
     factory: factory,
     hasError: false,
     importedAll: EMPTY,
     importedDefault: EMPTY,
     isInitialized: false,
     publicModule: {
       exports: {}
     }
   };
   modules[moduleId] = mod;
 }
 // 可以看到这个非常的简单,就是在 组册表(modules)中 添加 对应的 模块 

我们再来看看 重要的 一个 module 的定义是如何实现的

// 为了方便起见 我们直接找到  BU1 组件的声明  通过全局搜索🔍 我们找到了这个 定义,他在 802 -> 876 行

// 我们先看他 __d 参数部分 ,它 的执行器 factory = fn,模块id = 0 , 依赖模块的Map(别的依赖模块的 moduleId) = [1,2,3,4,6,9,10,12,179]
__d(fn,0,[1,2,3,4,6,9,10,12,179]) 

__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  var _interopRequireDefault = _$$_REQUIRE(_dependencyMap[0]);

  var _classCallCheck2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[1]));

  var _createClass2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[2]));

  var _inherits2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[3]));

  var _possibleConstructorReturn2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[4]));

  var _getPrototypeOf2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[5]));

// 下面三个模块 是 react -> react-native -> jsxRuntime 的重要模块 !分包负责 核心加载 RN 以来,JSXruntime 解析 
  var _react = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[6]));

  var _reactNative = _$$_REQUIRE(_dependencyMap[7]);

  var _jsxRuntime = _$$_REQUIRE(_dependencyMap[8]);

  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }

  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }

// BU1 组件编译后的渲染就是 这一坨
  var BU1 = function (_React$Component) {
    (0, _inherits2.default)(BU1, _React$Component);

    var _super = _createSuper(BU1);

    function BU1() {
      (0, _classCallCheck2.default)(this, BU1);
      return _super.apply(this, arguments);
    }

    (0, _createClass2.default)(BU1, [{
      key: "render",
      value: function render() {
        return (0, _jsxRuntime.jsx)(_reactNative.View, {
          style: styles.container,
          children: (0, _jsxRuntime.jsx)(_reactNative.Text, {
            style: styles.hello,
            children: "BU1 "
          })
        });
      }
    }]);
    return BU1;
  }(_react.default.Component);

// 我们自己写的styles 函数
  var styles = _reactNative.StyleSheet.create({
    container: {
      flex: 1,
      justifyContent: "center",
      height: 100
    },
    hello: {
      fontSize: 20,
      textAlign: "center",
      margin: 10
    },
    imgView: {
      width: "100%"
    },
    img: {
      width: "100%",
      height: 600
    },
    flatContainer: {
      flex: 1
    }
  });

// RNDemo 的 registerComponent 函数 
  _reactNative.AppRegistry.registerComponent("Bu1Activity", function () {
    return BU1;
  });
},0,[1,2,3,4,6,9,10,12,179]);

最后 就是RNDemo 的__r 执行了

__r(27);
// 27 这个模块id 我们可以去看看它在做什么
__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  'use strict';

  var start = Date.now();

  _$$_REQUIRE(_dependencyMap[0]);

  _$$_REQUIRE(_dependencyMap[1]);

  _$$_REQUIRE(_dependencyMap[2]);

  _$$_REQUIRE(_dependencyMap[3]);

  _$$_REQUIRE(_dependencyMap[4]);

  _$$_REQUIRE(_dependencyMap[5]);

  _$$_REQUIRE(_dependencyMap[6]);

  _$$_REQUIRE(_dependencyMap[7]);

  _$$_REQUIRE(_dependencyMap[8]);

  _$$_REQUIRE(_dependencyMap[9]);

  _$$_REQUIRE(_dependencyMap[10]);

  _$$_REQUIRE(_dependencyMap[11]); 

  var GlobalPerformanceLogger = _$$_REQUIRE(_dependencyMap[12]);

  GlobalPerformanceLogger.markPoint('initializeCore_start', GlobalPerformanceLogger.currentTimestamp() - (Date.now() - start));
  GlobalPerformanceLogger.markPoint('initializeCore_end');
},27,[28,29,30,32,56,62,65,70,101,105,106,116,78]);

// 这个 模块,可以这样理解,它实际上是 在执行 initializeCore_start,初始化的工作 initializeCore,预载入一些系统 模块 


// 直接就是执行 RNDemo 1 的模块代码了 具体的细节这里就不说了,核心就是 执行 模块中 的factory 代码 既__d 的第一个参数fn 
__r(0);

1.3 从 刚才的demo 我们来看 metro 的打包工作流

我们了解完 bundle 的生成之后,不妨陷入了一个思考 🤔 这些模块id 如何生成的呢?

首先我们看命了行

yarn react-native bundle 
     --platform android 
     --dev false 
     --entry-file ./RNDemo.js 
     --bundle-output ./android/app/src/main/assets/rn.android.bundle 
     --assets-dest ./android/app/src/main/res 
     --minify false 
     --reset-cache
// 我们不妨找一下 react-native cli 的源码 它位于/node_modules/bin 下的目录(为什么是bin 目录?你对node 不熟悉,请去补充一下node 相关的知识) 

'use strict';
var cli = require('@react-native-community/cli');
if (require.main === module) {
  cli.run();
}
module.exports = cli;

// 可以看到 实际上就是执行 @react-native-community/cli 里的 cli 
// 然后我们去看看 官方,仓库源代码 仓库里有一份清晰的文档说明,详细的描述里 每个参数的作用 ,这里不详细的解了 

// 我们找到源代码仓库 .cli/ 里面有一个bin bin 里有一个run ,run 函数定义在 index 中

async function run() {
  try {
    await setupAndRun();
  } catch (e) {
    handleError(e);
  }
}

async function setupAndRun() {
  ....
  // 重点函数  从 detachedCommands 添加更多的 command
  for (const command of detachedCommands) {
    attachCommand(command);
  }
  ....
}

// command 在 commands index 中 于是我们发现了 
import {Command, DetachedCommand} from '@react-native-community/cli-types';
import {commands as cleanCommands} from '@react-native-community/cli-clean';
import {commands as doctorCommands} from '@react-native-community/cli-doctor';
import {commands as configCommands} from '@react-native-community/cli-config';
import {commands as metroCommands} from '@react-native-community/cli-plugin-metro';

import profileHermes from '@react-native-community/cli-hermes';
import upgrade from './upgrade/upgrade';
import init from './init';

export const projectCommands = [
  ...metroCommands,
  ...configCommands,
  cleanCommands.clean,
  doctorCommands.info,
  upgrade,
  profileHermes,
] as Command[];

export const detachedCommands = [
  init,
  doctorCommands.doctor,
] as DetachedCommand[];


// 我们找到 cli-plugin-metro 是我们需要的因为 在其文件夹下 我们发现了start 和bundle 两个command 
// 解析来 我们找到了它的调用链
import Server from 'metro/src/Server';

 const server = new Server(config);

  try {
    const bundle = await output.build(server, requestOpts);

    await output.save(bundle, args, logger.info);
  }

// 然后我们来看 Server  metro 仓库的 Server 中 
class Server {
 constructor(config, options) {
    this._config = config;
    this._serverOptions = options;

    if (this._config.resetCache) {
      this._config.cacheStores.forEach((store) => store.clear());

      this._config.reporter.update({
        type: "transform_cache_reset",
      });
    }

    this._reporter = config.reporter;
    this._logger = Logger;
    this._platforms = new Set(this._config.resolver.platforms);
    this._isEnded = false; // TODO(T34760917): These two properties should eventually be instantiated
    // elsewhere and passed as parameters, since they are also needed by
    // the HmrServer.
    // The whole bundling/serializing logic should follow as well.

    
    this._createModuleId = config.serializer.createModuleIdFactory();
    this._bundler = new IncrementalBundler(config, {
      hasReducedPerformance: options && options.hasReducedPerformance,
      watch: options ? options.watch : undefined,
    });
    this._nextBundleBuildID = 1;
  }
  //....

    // 诶 重点代码 _createModuleId ,创建 ModuleId 但 它从那儿来呢?我们回到执行的地方 @react-native-community/的 cli-plugin-metro中 找到 buildBundle, 它就是命令 执行的地方
// 这个函数下 loadMetroConfig 返回一个config 我们看看 loadMetroConfig 在干什么
async function buildBundle(
  args: CommandLineArgs,
  ctx: Config,
  output: typeof outputBundle = outputBundle,
) {
  const config = await loadMetroConfig(ctx, {
    maxWorkers: args.maxWorkers,
    resetCache: args.resetCache,
    config: args.config,
  });

  return buildBundleWithConfig(args, config, output);
}


export default function loadMetroConfig(
  ctx: ConfigLoadingContext,
  options?: ConfigOptionsT,
): Promise<MetroConfig> {
  const defaultConfig = getDefaultConfig(ctx);
  if (options && options.reporter) {
    defaultConfig.reporter = options.reporter;
  }
  // 发现这里有一个 loadConfig
  return loadConfig({cwd: ctx.root, ...options}, defaultConfig);
}

// loadConfig 从 metro 里 来 通过调用链我们锁定了 这行代码
const getDefaultConfig = require('./defaults');

// 它里面正好有一个

const defaultCreateModuleIdFactory = require('metro/src/lib/createModuleIdFactory');

// 然后我们先不阅读 具体内容,鉴于 爱metro 和 cli 中反复 跳 我们先理解metro 

首先我们在metro 官网找到了 相关的 build 构建流程 (https://facebook.github.io/metro/docs/concepts)。主要分下面几个阶段

  • Resolution (依据入口文件 解析,他于Transformation 是并行的 )
  • Transformation (转换比如一些es6 的语法)
  • Serialization (序列化,实际上moduleId 就是这个理生成的)组合成单个 JavaScript 文件的模块包。
// metro 官方文档(https://facebook.github.io/metro/docs/configuration#serializer-options)中提到了 Serialization 时期使用到的几个函数,其中我们要关注的点是“moduleId 如何生成的 ”

// 具体的源代码在  ./node_modules/metro/src/lib/createModuleIdFactory.js 这里是metro 默认 的 moduleId 生成方式

function createModuleIdFactory() {
  const fileToIdMap = new Map();
  let nextId = 0;
  return (path) => {
    let id = fileToIdMap.get(path);

    if (typeof id !== "number") {
      id = nextId++;
      fileToIdMap.set(path, id);
    }

    return id;
  };
}

// 不难看出 非常的简单 就是0 开始的 自增,后面我们分包的时候 需要手动的定制一些 moduleId 要不然 运行的时候 会导致 模块的依赖出现问题 和冲突 导致闪退!

顺便说一下 Serialization时期 还有一个重要的函数 processModuleFilter,他可以完成模块 build 阶段的过滤,当他 返回 false 就是不打入,这个特性对我们后续的拆包会很有用。

到此为止,我们对bundle 和 metro 的浅析接结束了,以上都是前置内容是了解后续拆包方案的 js部分的基础

1.4 js基础部分我们掰开 说完整了,我们看看 RN 在Android 上的loading 原理

我们先梳理流程

// 创建一个ReactRootView
 mReactRootView = new ReactRootView(this);

// 增加依赖
List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
packages.add(new RNToolPackage());

// 创建 ReactInstanceManager 实例
mReactInstanceManager = ReactInstanceManager.builder()
          .setApplication(getApplication())
          .setCurrentActivity(this)
          .setBundleAssetName("index.android.bundle")
          .setJSMainModulePath("index") // 仅dev 下有效
          .addPackages(packages)
          .setUseDeveloperSupport(BuildConfig.DEBUG)
          .setInitialLifecycleState(LifecycleState.RESUMED)
          .build();

// 组册 js 组件 并挂到ReactRootView 实例上
mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null);

// 把 mReactRootView 设置到当前的 View 上
setContentView(mReactRootView);

解析来我们浅析 每个调用链

// 我们看看这个 ReactRootView 类 还有这个类的 startReactApplication 方法 
class ReactRootView extends FrameLayout implements RootView, ReactRoot {
  //..... 省去部分代码

  // 可以看到它继承 FrameLayout ,并且实现了 两个借口,
    @ThreadConfined("UI")
    public void startReactApplication(ReactInstanceManager reactInstanceManager, String moduleName, @Nullable Bundle initialProperties, @Nullable String initialUITemplate) {
        Systrace.beginSection(0L, "startReactApplication");

        try {
            UiThreadUtil.assertOnUiThread();
            Assertions.assertCondition(this.mReactInstanceManager == null, "This root view has already been attached to a catalyst instance manager");
            // 看看 mReactInstanceManager 实例是否正常 加载

            // 赋值 
            this.mReactInstanceManager = reactInstanceManager;
            this.mJSModuleName = moduleName; // 用上面的例子来说 这个地方的值 就是 MyReactNativeApp
            this.mAppProperties = initialProperties;
            this.mInitialUITemplate = initialUITemplate;

            // 创建 jscore 基础容器 上下午
            this.mReactInstanceManager.createReactContextInBackground();

            if (ReactFeatureFlags.enableEagerRootViewAttachment) {
                if (!this.mWasMeasured) {
                    // 适配屏幕
                    this.setSurfaceConstraintsToScreenSize();
                }
                // 简单的理解就是 让这个RootView 和 reactInstanceManager 关联起来 这一步上是rn 容器的基础
                // 一些js 通信view 渲染的都在这个里面 由 reactInstanceManager 管理
                this.attachToReactInstanceManager();
            }

        } finally {
            Systrace.endSection(0L);
        }

    }


    private void attachToReactInstanceManager() {
      Systrace.beginSection(0L, "attachToReactInstanceManager");
      ReactMarker.logMarker(ReactMarkerConstantsROOT_VIEW_ATTACH_TO_REACT_INSTANCE_MANAGER_START);
      if (this.getId() != -1) {
            ReactSoftExceptionLogger.logSoftException("ReactRootView", new IllegalViewOperationException("Trying to attach a ReactRootView with an explicit id already set to [" + this.getId() + "]. React Native uses the id field to track react tags and will overwrite this field. If that is fine, explicitly overwrite the id field to View.NO_ID."));
        }

        try {
            if (!this.mIsAttachedToInstance) {
                this.mIsAttachedToInstance = true;
                // 重点 ReactInstanceManager attachRootView 当前view 
                ((ReactInstanceManager)Assertions.assertNotNull(this.mReactInstanceManager)).attachRootView(this);
                
                // 执行 我们自己定义的监听器(详细见RCTDeviceEventEmitter 
                this.getViewTreeObserver().addOnGlobalLayoutListener(this.
                getCustomGlobalLayoutListener());
                return;
            }
        } finally {
            ReactMarker.logMarker(ReactMarkerConstants.ROOT_VIEW_ATTACH_TO_REACT_INSTANCE_MANAGER_END);
            Systrace.endSection(0L);
        }

    }

 }
 

// 这个内容比较简单 读取当前 application ,然后返回 package List 
List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
// 如果还需要其它package 可以接着add 
packages.add(new RNToolPackage());

// PackageList class 
public class PackageList {
  private Application application;
  private ReactNativeHost reactNativeHost;
  private MainPackageConfig mConfig;

  ...
  public PackageList(Application application) {
    this(application, null);
  }

  public ArrayList<ReactPackage> getPackages() {
    return new ArrayList<>(Arrays.<ReactPackage>asList(
      new MainReactPackage(mConfig),
      new AsyncStoragePackage(),
      new RNDeviceInfo()
    ));
  }
}



//  我们来看这个 
mReactInstanceManager = ReactInstanceManager.builder()
          .setApplication(getApplication())
          .setCurrentActivity(this)
          .setBundleAssetName("index.android.bundle")
          .setJSMainModulePath("index") // 仅dev 下有效
          .addPackages(packages)
          .setUseDeveloperSupport(BuildConfig.DEBUG)
          .setInitialLifecycleState(LifecycleState.RESUMED)
          .build();


class ReactInstanceManager  {
    public static ReactInstanceManagerBuilder builder() {
        return new ReactInstanceManagerBuilder();
    }

    // 构造函数
    ReactInstanceManager(..../* 太多了省去不写 后面有说明 */){
        // 这两个function 不是我们讨论的重点 省去
        initializeSoLoaderIfNecessary(applicationContext);// 
        DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext);

        this.mApplicationContext = applicationContext;
        this.mCurrentActivity = currentActivity;
        this.mDefaultBackButtonImpl = defaultHardwareBackBtnHandler;
        this.mJavaScriptExecutorFactory = javaScriptExecutorFactory;
        this.mBundleLoader = bundleLoader;
        this.mJSMainModulePath = jsMainModulePath; // 只有在dev 的时候有用
        this.mPackages = new ArrayList();
        this.mUseDeveloperSupport = useDeveloperSupport;
        this.mRequireActivity = requireActivity;
        Systrace.beginSection(0L, "ReactInstanceManager.initDevSupportManager");
        
        // dev 模式下 才使用 mJSMainModulePath
        this.mDevSupportManager = devSupportManagerFactory.create(applicationContext, this.createDevHelperInterface(), this.mJSMainModulePath, useDeveloperSupport, redBoxHandler, devBundleDownloadListener, minNumShakes, customPackagerCommandHandlers, surfaceDelegateFactory);

        Systrace.endSection(0L);
        this.mBridgeIdleDebugListener = bridgeIdleDebugListener;
        this.mLifecycleState = initialLifecycleState;
        this.mMemoryPressureRouter = new MemoryPressureRouter(applicationContext);
        this.mJSExceptionHandler = jSExceptionHandler;
        this.mTMMDelegateBuilder = tmmDelegateBuilder;
        
        // 开启线程执行载入 package 
        synchronized(this.mPackages) {
            PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: Use Split Packages");
            this.mPackages.add(new CoreModulesPackage(this, new DefaultHardwareBackBtnHandler() {
                public void invokeDefaultOnBackPressed() {
                    ReactInstanceManager.this.invokeDefaultOnBackPressed();
                }
            }, mUIImplementationProvider, lazyViewManagersEnabled, minTimeLeftInFrameForNonBatchedOperationMs));
            if (this.mUseDeveloperSupport) {
                this.mPackages.add(new DebugCorePackage());
            }

            this.mPackages.addAll(packages);
        }

        this.mJSIModulePackage = jsiModulePackage;
        ReactChoreographer.initialize();
        if (this.mUseDeveloperSupport) {
            this.mDevSupportManager.startInspector();
        }

        this.registerCxxErrorHandlerFunc();
    }

    // 是否创建 了 InitContext
    public boolean hasStartedCreatingInitialContext() {
          return this.mHasStartedCreatingInitialContext;
    }

    // 加一个监听器 看看 context 容器 实例 是否载入
    public void addReactInstanceEventListener(com.facebook.react.ReactInstanceEventListener listener) {
        this.mReactInstanceEventListeners.add(listener);
    }



}

class ReactInstanceManagerBuilder {
    ....
    ReactInstanceManagerBuilder() { // 指定一个JS 解释器
        this.jsInterpreter = JSInterpreter.OLD_LOGIC; 
        // JSInterpreter JS解释器,里面有三种模式 OLD_LOGIC,JSC,HERMES
    }

    // 为 ReactInstanceManagerBuilder 实例 设置当前 application
    public ReactInstanceManagerBuilder setApplication(Application application) {
        this.mApplication = application;
        return this;
    }

    // 为 ReactInstanceManagerBuilder 实例 设置当前 activity
    public ReactInstanceManagerBuilder setCurrentActivity(Activity activity) {
        this.mCurrentActivity = activity;
        return this;
    }

    // 设置当前 mJSBundleAssetUrl,此时 mJSBundleLoader = null 
    public ReactInstanceManagerBuilder setBundleAssetName(String bundleAssetName) {
        this.mJSBundleAssetUrl = bundleAssetName == null ? null : "assets://" + bundleAssetName;
        this.mJSBundleLoader = null;
        return this;
    }

   // 设置 mJSMainModulePath 这个只有在 dev 模式下有效,至于为什么 请看后面的一个源代码 --TODO
    public ReactInstanceManagerBuilder setJSMainModulePath(String jsMainModulePath) {
        this.mJSMainModulePath = jsMainModulePath;
        return this;
    }

   // 把 PackageList 全部添加到自己身上
    public ReactInstanceManagerBuilder addPackages(List<ReactPackage> reactPackages) {
        this.mPackages.addAll(reactPackages);
        return this;
    }

   // 设置是否dev 模式
    public ReactInstanceManagerBuilder setUseDeveloperSupport(boolean useDeveloperSupport) {
        this.mUseDeveloperSupport = useDeveloperSupport;
        return this;
    }

    // 设置是否生命周期 他说这些枚举 位于facebook 的包下
    // BEFORE_CREATE, 创建之前
    // BEFORE_RESUME, resume 之前
    // RESUMED;  已经 resume
    public ReactInstanceManagerBuilder setInitialLifecycleState(LifecycleState initialLifecycleState) {
        this.mInitialLifecycleState = initialLifecycleState;
        return this;
    }

    public ReactInstanceManager build() {
        Assertions.assertNotNull(this.mApplication, "Application property has not been set with this builder");
        if (this.mInitialLifecycleState == LifecycleState.RESUMED) {
            Assertions.assertNotNull(this.mCurrentActivity, "Activity needs to be set if initial lifecycle state is resumed");
        }

        Assertions.assertCondition(this.mUseDeveloperSupport || this.mJSBundleAssetUrl != null || this.mJSBundleLoader != null, "JS Bundle File or Asset URL has to be provided when dev support is disabled");
        Assertions.assertCondition(this.mJSMainModulePath != null || this.mJSBundleAssetUrl != null || this.mJSBundleLoader != null, "Either MainModulePath or JS Bundle File needs to be provided");
        
        // RN 的UI 提供者 
        if (this.mUIImplementationProvider == null) {
            this.mUIImplementationProvider = new UIImplementationProvider();
        }

        // 获取当前包名
        String appName = this.mApplication.getPackageName();
        String deviceName = AndroidInfoHelpers.getFriendlyDeviceName(); // 获取设备名称

        // 创建一个 ReactInstanceManager 
        return new ReactInstanceManager(
         this.mApplication,
         this.mCurrentActivity,
         this.mDefaultHardwareBackBtnHandler, // android 物理返回键处理程序 
         this.mJavaScriptExecutorFactory == null ? this.getDefaultJSExecutorFactory(appName, deviceName, this.mApplication.getApplicationContext()) : this.mJavaScriptExecutorFactory, 
         this.mJSBundleLoader == null && this.mJSBundleAssetUrl != null ? JSBundleLoader.createAssetLoader(this.mApplication, this.mJSBundleAssetUrl, false) : this.mJSBundleLoader, 
         //  mJSBundleLoader js bundle 捆绑器 详细见下面的类 
         this.mJSMainModulePath, 
         this.mPackages, 
         this.mUseDeveloperSupport,
          (DevSupportManagerFactory)(this.mDevSupportManagerFactory == null ? new DefaultDevSupportManagerFactory() : this.mDevSupportManagerFactory), 
         this.mRequireActivity, 
         this.mBridgeIdleDebugListener, (LifecycleState)Assertions.assertNotNull(this.mInitialLifecycleState, "Initial lifecycle state was not set"), 
         this.mUIImplementationProvider, 
         this.mJSExceptionHandler, 
         this.mRedBoxHandler, 
         this.mLazyViewManagersEnabled, // boolean 是否开启 lazy 加载
         this.mDevBundleDownloadListener,  // dev bundle 下载监听器
         this.mMinNumShakes, 
         this.mMinTimeLeftInFrameForNonBatchedOperationMs, 
         this.mJSIModulesPackage,  // ReactInstanceManager 里的 jsiModulePackage  这个 package 还和 rn 的bridge 有关 这里不深入
         this.mCustomPackagerCommandHandlers, 
         this.mTMMDelegateBuilder, 
         this.mSurfaceDelegateFactory);
    }

    // 工厂函数 js 执行器 看看到底给你的是 JSCExecutorFactory 还是 HermesExecutorFactory 
    private JavaScriptExecutorFactory getDefaultJSExecutorFactory(String appName, String deviceName, Context applicationContext) {
        if (this.jsInterpreter == JSInterpreter.OLD_LOGIC) {
            try {
                ReactInstanceManager.initializeSoLoaderIfNecessary(applicationContext);
                JSCExecutor.loadLibrary();
                return new JSCExecutorFactory(appName, deviceName);
            } catch (UnsatisfiedLinkError var5) {
                if (var5.getMessage().contains("__cxa_bad_typeid")) {
                    throw var5;
                } else {
                    HermesExecutor.loadLibrary();
                    return new HermesExecutorFactory();
                }
            }
        } else if (this.jsInterpreter == JSInterpreter.HERMES) {
            HermesExecutor.loadLibrary();
            return new HermesExecutorFactory();
        } else {
            JSCExecutor.loadLibrary();
            return new JSCExecutorFactory(appName, deviceName);
        }
    }

}

public abstract class JSBundleLoader {
    public JSBundleLoader() {
    }
    ....
    public static JSBundleLoader createAssetLoader(final Context context, final String assetUrl, final boolean loadSynchronously) {
        return new JSBundleLoader() {
            public String loadScript(JSBundleLoaderDelegate delegate) { // 重点参数 loadScriptFromAssets
                // 重点 这个就是 loadScriptFromAssets 的方法 。具体实现在 JSCExecutor.cpp 这里不详细扩开了,
                // 如果我们知道 rn 中谁在调用这个方法 就知道是如何载入js 的了
                delegate.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
                return assetUrl;
            }
        };
    }

    .....
    public abstract String loadScript(JSBundleLoaderDelegate var1);
    ....
}

// 当我们的步骤执行完之后 mReactInstanceManager 是一个这样的东西
mReactInstanceManager = {
  this.mApplication = "当前Application"
  this.mCurrentActivity   = "当前的Activity"
  this.mDefaultBackButtonImpl = "当前硬件返回处理程序"
  this.mJavaScriptExecutorFactory = "JSCExecutorFactory 执行器 appName =myrnApp deviceName 小米2s"
  this.mBundleLoader= "JSBundleLoader.createAssetLoader(this.mApplication, assets://index.android.bundle, false)  "
  this.mJSMainModulePath="index"
  this.mPackages="Packages 里面包含了dev 的一些包 因为UseDeveloperSupport = true"
  this.mUseDeveloperSupport="true"
  this.mRequireActivity="false"
  this.mBridgeIdleDebugListener="null"
  this.mJSExceptionHandler="null"
  this.mRedBoxHandler="null"
  this.mLazyViewManagersEnabled="false"
  this.mDevBundleDownloadListener="null"
  this.mMinNumShakes="1"
  this.mMinTimeLeftInFrameForNonBatchedOperationMs="-1"
  this.mJSIModulesPackage="null"
  this.mCustomPackagerCommandHandlers="{}"
  this.mTMMDelegateBuilder="null"
  this.mSurfaceDelegateFactory="null"
}

// 在 RootView 类中有一个 startApplication 方法 里面有一个  createReactContextInBackground 它属于 ReactInstanceManager 里面分治了两类 dev 和 release 的
class ReactInstanceManager {
  ....

    // 通过调用链 我们找到了最总的调用方法 recreateReactContextInBackgroundInner 和 runCreateReactContextOnNewThread 以及 createReactContext
    @ThreadConfined("UI")
    private void recreateReactContextInBackgroundInner() {
        FLog.d(TAG, "ReactInstanceManager.recreateReactContextInBackgroundInner()");
        PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: recreateReactContextInBackground");
        UiThreadUtil.assertOnUiThread();
        
        if (this.mUseDeveloperSupport && this.mJSMainModulePath != null) { //进入dev
            final DeveloperSettings devSettings = this.mDevSupportManager.getDevSettings();
            if (!Systrace.isTracing(0L)) {
                if (this.mBundleLoader == null) {
                    this.mDevSupportManager.handleReloadJS(); // reload js 
                } else {
                    this.mDevSupportManager.isPackagerRunning(new PackagerStatusCallback() {
                        public void onPackagerStatusFetched(final boolean packagerIsRunning) {
                            UiThreadUtil.runOnUiThread(new Runnable() {
                                public void run() {
                                    if (packagerIsRunning) {
                                        ReactInstanceManager.this.mDevSupportManager.handleReloadJS();
                                    } else if (ReactInstanceManager.this.mDevSupportManager.hasUpToDateJSBundleInCache() && !devSettings.isRemoteJSDebugEnabled() && !ReactInstanceManager.this.mUseFallbackBundle) {
                                        ReactInstanceManager.this.onJSBundleLoadedFromServer();
                                    } else {
                                        devSettings.setRemoteJSDebugEnabled(false);
                                        ReactInstanceManager.this.recreateReactContextInBackgroundFromBundleLoader();
                                    }

                                }
                            });
                        }
                    });
                }

                return;
            }
        }

      // 正常 release 如何 loader 呢?依据调用链 查找到 runCreateReactContextOnNewThread 函数
        this.recreateReactContextInBackgroundFromBundleLoader();
    }

    // 开启线程 执行 CreateReactContext 这里有很多的线程代码 我们不深入 
    @ThreadConfined("UI")
    private void runCreateReactContextOnNewThread(final ReactInstanceManager.ReactContextInitParams initParams) {
      ....
      reactApplicationContext = ReactInstanceManager.this.createReactContext(initParams.getJsExecutorFactory().create(), initParams.getJsBundleLoader());
      .....
      ReactInstanceManager.this.setupReactContext(reactApplicationContext); // 更新上去 
    }

    // 找到 createReactContext 函数 我们先 理解一下 他的参数 jsExecutor,jsBundleLoader
    // jsExecutor 这个是之前我们找到的 执行器 ,jsBundleLoader就是上述说明的Loader 这个需要重点看看,因为从上述的类来看 最总的加载在它 
    private ReactApplicationContext createReactContext(JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) {
      ....
      // 关键代码 
      com.facebook.react.bridge.CatalystInstanceImpl.Builder catalystInstanceBuilder = (
        new com.facebook.react.bridge.CatalystInstanceImpl.Builder()).setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()).setJSExecutor(jsExecutor).setRegistry(nativeModuleRegistry).setJSBundleLoader(jsBundleLoader).setJSExceptionHandler((JSExceptionHandler)exceptionHandler);
      // catalystInstanceBuilder 主要做的事情 是 设置 队列(因为涉及到线程),->设置JS执行器 -> 设置 nativeModuleRegistry -> 设置 jsBundleLoader-> 设置异常捕获器

      // catalystInstanceBuilder 这个类身上就有我们的jsbundle 了
      CatalystInstanceImpl catalystInstance = catalystInstanceBuilder.build();
      // build 就是依据传如的参数 返回一个 CatalystInstanceImpl 实例
      // 最后一行就跑去了
      catalystInstance.runJSBundle() 
      // 我们分析一下  catalystInstanceBuilder 类的build 返回了什么。以及它 返回的类上的 runJSBundle 在干什么
      ....
    }
  
}

    public static class Builder {
        @Nullable
        private ReactQueueConfigurationSpec mReactQueueConfigurationSpec;
        @Nullable
        private JSBundleLoader mJSBundleLoader;
        @Nullable
        private NativeModuleRegistry mRegistry;
        @Nullable
        private JavaScriptExecutor mJSExecutor;
        @Nullable
        private JSExceptionHandler mJSExceptionHandler;

        public Builder() {
        }

        public CatalystInstanceImpl.Builder setReactQueueConfigurationSpec(ReactQueueConfigurationSpec ReactQueueConfigurationSpec) {
            this.mReactQueueConfigurationSpec = ReactQueueConfigurationSpec;
            return this;
        }

        public CatalystInstanceImpl.Builder setRegistry(NativeModuleRegistry registry) {
            this.mRegistry = registry;
            return this;
        }

        public CatalystInstanceImpl.Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) {
            this.mJSBundleLoader = jsBundleLoader;
            return this;
        }

        public CatalystInstanceImpl.Builder setJSExecutor(JavaScriptExecutor jsExecutor) {
            this.mJSExecutor = jsExecutor;
            return this;
        }

        public CatalystInstanceImpl.Builder setJSExceptionHandler(JSExceptionHandler handler) {
            this.mJSExceptionHandler = handler;
            return this;
        }

        public CatalystInstanceImpl build() {
            return new CatalystInstanceImpl((ReactQueueConfigurationSpec)Assertions.assertNotNull(this.mReactQueueConfigurationSpec), (JavaScriptExecutor)Assertions.assertNotNull(this.mJSExecutor), (NativeModuleRegistry)Assertions.assertNotNull(this.mRegistry), (JSBundleLoader)Assertions.assertNotNull(this.mJSBundleLoader), (JSExceptionHandler)Assertions.assertNotNull(this.mJSExceptionHandler));
        }
    }

public class CatalystInstanceImpl implements CatalystInstance { 
    public void runJSBundle() {
            FLog.d("ReactNative", "CatalystInstanceImpl.runJSBundle()");
            Assertions.assertCondition(!this.mJSBundleHasLoaded, "JS bundle was already loaded!");

            this.mJSBundleLoader.loadScript(this); // 运行load loadScript这个不深入了,他和一部分的C++代码有关系
            // loadScript -> 实际上就是  loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously); 返回 assetUrl string
            
            synchronized(this.mJSCallsPendingInitLock) {
                this.mAcceptCalls = true;
                Iterator var2 = this.mJSCallsPendingInit.iterator();

                while(true) {
                    if (!var2.hasNext()) {
                        this.mJSCallsPendingInit.clear();
                        this.mJSBundleHasLoaded = true;
                        break;
                    }

                    CatalystInstanceImpl.PendingJSCall function = (CatalystInstanceImpl.PendingJSCall)var2.next();
                    function.call(this);
                }
            }

            Systrace.registerListener(this.mTraceListener);
        }
}

// 从上述我们可以看到,执行runjs 的 实际上是 CatalystInstance ,这点请你记住

到此为止,我们的前置知识都搞定了!

  1. 首先我们来看看第一版方案( 直接丢到不同的 acitvy 中运行)

    主要的思路:“让多个Native 容器去承载 不同的RN 容器,每一个RN容器都是一个独立的BU业务,在通过RN 的native 桥接 就能够实现 这类的拆包”, 需要注意的 开发阶段 和 build 阶段,

  • 在开发阶段:

    由于metro build 的末日目录在根目录 ,我们的需要在root 根目录下进行 (我是指每个module 的入口要在根目录 )要不然会有路径问题,metro 实际上是一个 static 文件托管service 它默认监听的是项目根目录, 譬如你请求的是 index.bundle.好,默认就是根目录下的index ,如果你请求的是 a.bundle,那么加载和编译的就是 根目录下的 a.js 文件,这些就是所谓的“入口文件”,这些文件里 有一个 registerComponent 方法,这个就是runtime 的时候 rn 触发的 view 试图绑定的关键代码,在RN 引擎中 ,它的加载顺序是 :js端先运行js代码注册组件---->原生端找到这个组件并关联

  • 需要注意我们的这个参数

          mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")  // 对应的release 包名称,如果多个业务就是 bu1.android.bundle, bu2.android.bundle ......
                .setJSMainModulePath("index") // 根目录下 index.js . 如果同的文件 就是 Bu1.js  Bu2.js xxxxx 依次类推 不一定都叫这个名字哈 只在dev 模式下生效 setJSMainModulePath
                .addPackages(packages)
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
          mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null); // js 端 的registerComponent name MyReactNativeApp
          setContentView(mReactRootView);
  1. 第二版方案 (基础包 common + bu 业务包 = 运行时的 全量包 )

我们的发现上述的分包方案有明显的不足:“每个独立的包 都包含RN 的公共部分”,它会让我们的包体积变大,加载的时候白屏实际也会变长,基于此和市面上主流的方案,我们可以这样玩 :把公共的包提取出来,bu包只包含业务,在实际运行的时候,把它们合成一个 runtime 的bundle 去执行,于是我们就有了这样的东西: common + bu 业务包 = 运行时的 全量包

  • 首先我们就要处理 “拆开” 这一个问题,在上述的 cli 源码分析中,我可以所需要用到的东西,只有两个函数 metro 提供的配置 createModuleIdFactory 和 processModuleFilter,前者处理模块命名,后者处理过滤(哪些需要打入bundle 哪些不需要),主要的内容前问已经描述过了,这里不在赘述

    我们先看看moduleId 的处理,首先啊,我们还是使用 number 做为 id (而不是使用string string 太大了),为了区分基础包和 bu 包,我们规定 10000000 为业务包的开始自增的 moduleId 初值(每个BU的值不一样,main->10000000 -> bu1 20000000-> bu2 -> 30000000) ,基础包的id 还是从0 -> 开始递增。还需要注意的是,由于我们的moduleId 之间是有相互依赖的 ,所以为了确保,依赖关系的正确性,我们需要为基础包做一个映射(做法是 把基础包的 路径 存到一个json 中,业务包遇到这个路径的时候 去找这个映射中的moduleId 就好了)如果你不这样做,那么你的模块依赖 会乱掉. 而在 bu 打包的时候 只需要过滤掉 基础包映射中的js module就好了

    build.js

    const fs = require("fs");
    
    const clean = function (file) {
      fs.writeFileSync(file, JSON.stringify({}));
    };
    
    const hasBuildInfo = function (file, path) {
      const cacheFile = require(file);
      return Boolean(cacheFile[path]);
    };
    
    const writeBuildInfo = function (file, path, id) {
      const cacheFile = require(file);
      cacheFile[path] = id;
      fs.writeFileSync(file, JSON.stringify(cacheFile));
    };
    
    const getCacheFile = function (file, path) {
      const cacheFile = require(file);
      return cacheFile[path] || 0;
    };
    
    const isPwdFile = (path) => {
      const cwd = __dirname.split("/").splice(-1, 1).toString();
    
      const pathArray = path.split("/");
      const map = new Map();
      const reverseMap = new Map();
    
      pathArray.forEach((it, indx) => {
        map.set(it, indx);
        reverseMap.set(indx, it);
      });
    
      if (pathArray.length - 2 == map.get(cwd)) {
        return reverseMap.get(pathArray.length - 1).replace(/\.js/, "");
      }
    
      return "";
    };
    
    module.exports = {
      hasBuildInfo,
      writeBuildInfo,
      getCacheFile,
      clean,
      isPwdFile,
    };

    common.metro.js

    const { hasBuildInfo, writeBuildInfo, clean } = require("./build");
    
    function createModuleIdFactory() {
      const fileToIdMap = new Map();
      let nextId = 0;
      clean("./config/bundleCommonInfo.json");
    
      // 如果是业务 模块请以 10000000 来自增命名
      return (path) => {
        let id = fileToIdMap.get(path);
    
        if (typeof id !== "number") {
          id = nextId++;
          fileToIdMap.set(path, id);
    
          !hasBuildInfo("./config/bundleCommonInfo.json", path) &&
            writeBuildInfo(
              "./config/bundleCommonInfo.json",
              path,
              fileToIdMap.get(path)
            );
        }
    
        return id;
      };
    }
    
    module.exports = {
      serializer: {
        createModuleIdFactory: createModuleIdFactory, // 给 bundle 一个id 避免冲突 cli 源码中这个id 是从1 开始 自增的
      },
    };

    mian.metro.js

    const { hasBuildInfo, getCacheFile, isPwdFile } = require("./build");
    const bundleBuInfo = require("./config/bundleBuInfo.json");
    function postProcessModulesFilter(module) {
      if (
        module["path"].indexOf("__prelude__") >= 0 ||
        module["path"].indexOf("polyfills") >= 0
      ) {
        return false;
      }
    
      if (hasBuildInfo("./config/bundleCommonInfo.json", module.path)) {
        return false;
      }
    
      return true;
    }
    
    // 不要使用 string 会导致 bundle 体积陡增
    function createModuleIdFactory() {
      // 如果是业务 模块请以 10000000 来自增命名
      const fileToIdMap = new Map();
      let nextId = 10000000;
      let isFirst = false;
    
      return (path) => {
        if (Boolean(getCacheFile("./config/bundleCommonInfo.json", path))) {
          return getCacheFile("./config/bundleCommonInfo.json", path);
        }
    
        if (!isFirst && isPwdFile(path)) {
          nextId = bundleBuInfo[isPwdFile(path)];
          isFirst = true;
        }
    
        let id = fileToIdMap.get(path);
        if (typeof id !== "number") {
          id = nextId++;
          fileToIdMap.set(path, id);
        }
        return id;
      };
    }
    
    module.exports = {
      serializer: {
        createModuleIdFactory: createModuleIdFactory, // 给 bundle 一个id 避免冲突 cli 源码中这个id 是从1 开始 自增的
        processModuleFilter: postProcessModulesFilter, // 返回false 就不会build 进去
      },
    };

    config/bundleBuInfo.json

      {
        "index": 10000000,
        "Bu1": 20000000,
        "Bu2": 30000000
      }
  • 执行build 命令就好了, 当然你可以把它们都编如一个shell 中去 打包简化的目的, 我这里没有怎么做,因为我们后续还需针对热更新做优化

    # common
    yarn react-native bundle --platform android --dev false --entry-file ./common.js --bundle-output ./android/app/src/main/assets/common.android.bundle --assets-dest ./android/app/src/main/res --config ./metro.common.config.js --reset-cache
    
    # BU
    yarn react-native bundle --platform android --dev false --entry-file ./Bu2.js --bundle-output ./android/app/src/main/assets/bu2.android.bundle --assets-dest ./android/app/src/main/res --config ./metro.main.config.js --reset-cache

    打包之前(假设我们没有 进行压缩🗜️ 参数 --minify false )我们发现,如果不拆包 每个bundle 也得有 将近2.3M的大小,

    打包之后(假设我们不对代码 进行压缩🗜️ 参数 --minify false )我们发现common 1.9MB (比较大 因为包含了公共依赖),其余的包 基本不到 50kb

    可以看到 效果显著啊,如果进行压缩 处理 common 将不足1kb 每个 bu将不会超过 20kb

    特别说明,上述的大小对比仅仅是我的这个项目来说,实际情况还是要以项目实际情况为主

    • 好了现在我们把js 的拆分已经完成了,然后重点来了“如何在Android native”,合并这两个包形成一个runtime 的 bundle呢?
    // 前文中我们就提到过 android code 执行的流程,现在我们来change 一下啊 ,主要的核心代码是:(具体的完整代码请看 源码)
    // 我这里把它们抽象 一个公共的类,每个 Activity 加载的时候 重写 getJSBundleAssetName,getJsModulePathPath,getResName 就 可以很方便的加载指定 的 Activity 了 ,
    
        @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
              if (!Settings.canDrawOverlays(this)) {
                  Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                          Uri.parse("package:" + getPackageName()));
                  startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
              }
          }
          SoLoader.init(this, false);
    
          // root 容器
          mReactRootView = new ReactRootView(this);
    
          if( BuildConfig.DEBUG ){
              mReactInstanceManager = ReactInstanceManager.builder()
                      .setApplication(getApplication())
                      .setCurrentActivity(this)
                      .setBundleAssetName(getJSBundleAssetName())
                      .setJSMainModulePath(getJsModulePathPath())
                      .addPackages(MainApplication.getInstance().packages)
                      .setUseDeveloperSupport(true)
                      .setInitialLifecycleState(LifecycleState.RESUMED)
                      .build();
    
              mReactRootView.startReactApplication(mReactInstanceManager, getResName(), null);
              setContentView(mReactRootView);
              return;
          }
    
    
          mReactInstanceManager = MainApplication.getInstance().builder.setCurrentActivity(this).build();
    
          if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
              mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
                  @Override
                  public void onReactContextInitialized(ReactContext context) {
                      //加载业务包
                      ReactContext mContext = mReactInstanceManager.getCurrentReactContext();
                      CatalystInstance instance = mContext.getCatalystInstance();
                      ((CatalystInstanceImpl)instance).loadScriptFromAssets(context.getAssets(), "assets://" + getJSBundleAssetName(),false);
    
                      mReactRootView.startReactApplication(mReactInstanceManager, getResName(), null);
                      setContentView(mReactRootView);
    
                      mReactInstanceManager.removeReactInstanceEventListener(this);
                  }
              });
              mReactInstanceManager.createReactContextInBackground();
          }
    
          return;
      }
    • 但光这样就结束了?远远没有,如果像上述这样做的话,会导致 每个 Activity 都会全量载入 一次 bundle ,如果有一种方法,能够把基础的common 缓存起来,每次 Activity 只加载 bu 包就好了。

    市面上对于这一块有不同的做法,网上能搜到的就是 腾讯某团队的 一篇文章了 (https://cloud.tencent.com/developer/article/1005382),但是这....是有局限的 直接缓存 RootView 要仔细处理 Native 的生命周期 和 RN 的生命周期,要不然会导致 缓存的RootView 无法执行 componnetDid 等,因为他执行过一次就不在执行js 了你没有reload js 只是缓存绘制好的View 而且 ,在 native 的 onDestroy 中也要处理,要不然缓存的view 无法相应JS。

    基于此我换了一种思路去实现呢它,我把common 缓存起来,动态加载不同的bundle ,目前我现在的做法基本上 是妙进的!

    // MainApplication
    public class MainApplication extends Application   {
        public ReactInstanceManagerBuilder builder;
        public  List<ReactPackage> packages;
        private  ReactInstanceManager cacheReactInstanceManager;
        private Boolean isload = false;
    
        private static MainApplication mApp;
    
        @Override
        public void onCreate() {
            super.onCreate();
            SoLoader.init(this, /* native exopackage */ false);
            mApp = this;
    
            packages = new PackageList(this).getPackages();
            packages.add(new RNToolPackage());
    
            cacheReactInstanceManager = ReactInstanceManager.builder()
                    .setApplication(this)
                    .addPackages(packages)
                    .setJSBundleFile("assets://common.android.bundle")
                    .setInitialLifecycleState(LifecycleState.BEFORE_CREATE).build();
    
        }
    
        public static MainApplication getInstance(){
            return mApp;
        }
    
        // 获取 已经缓存过的 rcInstanceManager
        public ReactInstanceManager getRcInstanceManager () {
            return this.cacheReactInstanceManager;
        }
    
    
        public void setIsLoad(Boolean isload) {
            this.isload = isload;
        }
    
        public boolean getIsLoad(){
            return this.isload;
        }
    
    // PreBaseInit (只列出 核心的代码 ) 完整代码请到仓库 自行查看
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
              if (!Settings.canDrawOverlays(this)) {
                  Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                          Uri.parse("package:" + getPackageName()));
                  startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
              }
          }
          SoLoader.init(this, false);
    
          if( BuildConfig.DEBUG ){
              mReactRootView = new ReactRootView(this);
              mReactInstanceManager = ReactInstanceManager.builder()
                      .setApplication(getApplication())
                      .setCurrentActivity(this)
                      .setBundleAssetName(getJSBundleAssetName())
                      .setJSMainModulePath(getJsModulePathPath())
                      .addPackages(MainApplication.getInstance().packages)
                      .setUseDeveloperSupport(true)
                      .setInitialLifecycleState(LifecycleState.RESUMED)
                      .build();
    
              mReactRootView.startReactApplication(mReactInstanceManager, getResName(), null);
              setContentView(mReactRootView);
              return;
          }
    
          // 重新设置 Activity 和 files
          mReactInstanceManager = MainApplication.getInstance().getRcInstanceManager();
          mReactInstanceManager.onHostResume(this, this);
          mReactRootView = new ReactRootView(this);
    
          mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
              @Override
              public void onReactContextInitialized(ReactContext context) {
                  MainApplication.getInstance().setIsLoad(true);
    
                  //加载业务包
                  ReactContext mContext = mReactInstanceManager.getCurrentReactContext();
                  CatalystInstance instance = mContext.getCatalystInstance();
                  ((CatalystInstanceImpl)instance).loadScriptFromAssets(context.getAssets(), "assets://" + getJSBundleAssetName(),false);
    
                  mReactRootView.startReactApplication(mReactInstanceManager, getResName(), null);
                  setContentView(mReactRootView);
    
                  mReactInstanceManager.removeReactInstanceEventListener(this);
              }
          });
    
          if(MainApplication.getInstance().getIsLoad()){
              ReactContext mContext = mReactInstanceManager.getCurrentReactContext();
              CatalystInstance instance = mContext.getCatalystInstance();
              ((CatalystInstanceImpl)instance).loadScriptFromAssets(mContext.getAssets(), "assets://" + getJSBundleAssetName(),false);
    
              mReactRootView.startReactApplication(mReactInstanceManager, getResName(), null);
              setContentView(mReactRootView);
    
          }
    
          mReactInstanceManager.createReactContextInBackground();
          return;
      }

    至此 基于RN 的拆包 JS 和 Android 端已经完美实现

    1. 关于热更新 和版本管理

    重要说明特(common 包为方便管理 我们不进行热更新)

    目前我使用 CodePush 遇到了问题,code push 适合 使用 rn 创建的新项目,如果使用 Android 项目开始的 那么,code push 集成 将会是一个棘手的问题。于是我自己作了一个 简单的热更新.

    技术预研

    1. 预先调研 (删除问文件夹操作)  是否可以 创建文件夹  + CV文件  + 删除文件 - ✅
    
    2. 预先调研  是否可以载入 fileSystem 的包 - ✅
    
    3. common开头独立执行嘛 - ✅4
    
    3. RN 下载 zip 并解包 - ✅
    

    相关详细的设计

重要的细节 (IOS)

按照官方的教程踩坑的地方

  1. 集成阶段
  • 安装pod 依赖
# Uncomment the next line to define a global platform for your project
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

install! 'cocoapods', :deterministic_uuids => false
platform :ios, '12.4'

target 'myrnapp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  config = use_native_modules!
  flags = get_default_flags()

  pod 'RNDeviceInfo', path: '../node_modules/react-native-device-info'
  
  use_react_native!(
    :path => config[:reactNativePath],
    # Hermes is now enabled by default. Disable by setting this flag to false.
    # Upcoming versions of React Native may rely on get_default_flags(), but
    # we make it explicit here to aid in the React Native upgrade process.
    :hermes_enabled => false,
    :fabric_enabled => flags[:fabric_enabled],
    # Enables Flipper.
    #
    # Note that if you have use_frameworks! enabled, Flipper will not work and
    # you should disable the next line.
    # :flipper_configuration => FlipperConfiguration.enabled,
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

  # Pods for myrnapp
  target 'myrnappTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'myrnappUITests' do
    # Pods for testing
  end

  post_install do |installer|
    react_native_post_install(
      installer,
      # Set `mac_catalyst_enabled` to `true` in order to apply patches
      # necessary for Mac Catalyst builds
      :mac_catalyst_enabled => true
    )
    __apply_Xcode_12_5_M1_post_install_workaround(installer)
  end
end
  • 如果你的项目中含有 SceneDelegate 请去掉它

原理 -> https://blog.csdn.net/c1o2c3o4/article/details/108711477?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-108711477-blog-104754971.t0_edu_mix&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-108711477-blog-104754971.t0_edu_mix&utm_relevant_index=1

删除方法 -> https://www.jianshu.com/p/6b3f40319877

删除main storyboard https://blog.csdn.net/qq_31598345/article/details/119979791

  • 我们不用官方的例子 只是按照它提供的思路 去自己写一个
//
//  ViewController.m
//  myrnapp
//
//  Created by 李仕增 on 2022/10/8.
//

#import "ViewController.h"
#import <React/RCTRootView.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
//    加一些oc 的code 确保项目上正常的状态
    UIView *view = [[UIView alloc] init];
    view.backgroundColor = [UIColor redColor];
    view.frame = CGRectMake(100,100, 100, 100);
    [self.view addSubview:view];

    view.userInteractionEnabled = YES;
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openRNView)];
    [view addGestureRecognizer:tap];

    UIView *view2 = [[UIView alloc] init];
    view2.backgroundColor = [UIColor greenColor];
    view2.frame = CGRectMake(150,300, 100, 100);
    [self.view addSubview:view2];
    
    view2.userInteractionEnabled = YES;
    UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openRNView2)];
    [view2 addGestureRecognizer:tap2];
    
//    直接开始集成
  
}

- (void) testClick {
    NSLog(@"6666666");
}

- (void)openRNView {
    NSLog(@"High Score Button Pressed");
    NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8082/IOS.bundle?platform=ios"];

        RCTRootView *rootView =
          [[RCTRootView alloc] initWithBundleURL: jsCodeLocation
                                      moduleName: @"RNHighScores"
                               initialProperties: nil
                                   launchOptions: nil];
        UIViewController *vc = [[UIViewController alloc] init];
        vc.view = rootView;
        [self presentViewController:vc animated:YES completion:nil];
 }

- (void)openRNView2 {
    NSLog(@"High Score Button Pressed");
    NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8082/IOS2.bundle?platform=ios"];

        RCTRootView *rootView =
          [[RCTRootView alloc] initWithBundleURL: jsCodeLocation
                                      moduleName: @"RNHighScores2"
                               initialProperties: nil
                                   launchOptions: nil];
        UIViewController *vc = [[UIViewController alloc] init];
        vc.view = rootView;
        [self presentViewController:vc animated:YES completion:nil];
 }

@end
  • 确保你的相关权限已经开放 比如网络

    确保你的info.plist 包含下面的字段

    <key>NSAppTransportSecurity</key>
    
NSExceptionDomains localhost NSTemporaryExceptionAllowsInsecureHTTPLoads ```
  1. build 阶段
  • 你可能会遇到的问题 你也许会越到当不限于下面的这些问题

    相关的问题都可以去react-native官方的github issue 里有,我最终采取静态连接的办法

    关键代码

    ++++
    use_frameworks! :linkage => :static
    # 使用静态库 连接 不要使用动态库 或者 默认的连接 ,会有问题
    ++++
  • 解析来 build 环节需要注意的地方

    Native 的BUILD 现在解决了,那么RN的build 怎么办呢?

    首先是native 代码需要修改 资源路径 不要从远程加载 直接从本地载入

    - (void)openRNView2 {
        NSLog(@"High Score Button Pressed");
    //    NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8082/IOS2.bundle?platform=ios"];
    
        NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"bundle/IOS2.ios" withExtension:@"bundle"];
        
    //        RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL: jsCodeLocation
    //                                      moduleName: @"RNHighScores2"
    //                                launchOptions: nil];
        RCTRootView *rootView = [[RCTRootView alloc]
                                initWithBundleURL:jsCodeLocation
                                moduleName:@"RNHighScores2"
                                initialProperties:nil
                                launchOptions:nil ];
        
            UIViewController *vc = [[UIViewController alloc] init];
            vc.view = rootView;
            [self presentViewController:vc animated:YES completion:nil];
    };

    然后关于js 和资源的build ,下面是它们的构建脚本

     yarn react-native bundle --entry-file ./IOS2.js --bundle-output ./bundle/IOS2.ios.bundle --platform ios --assets-dest ./bundle --dev false

    最后要注意的是 ==> 请你直接把整个文件夹拖拽进入Xcode!中的projext 下

    如果发现有问题 跑不通, 需要分析原因 给IOS debug 看看那个环节有问题

  1. 关于native 包的问题

实际上这个非常的简单,我在这个项目中 ,所有的native 包再 pod install 的时候都自动安装了,如果你需要手动包含,可以参考旧版本的做法. 在 PodFile 中手动+ (比如下面的例子)

++++
 pod 'RNDeviceInfo', path: '../node_modules/react-native-device-info'
+++

重点 拆包方案

  1. 参考

首先我参考了一部分的材料 主要的材料是这两片文章 掘金文章 RN的分包实践 GitHub项目

  1. 重要的原理

我们先看看 RN 在IOS 中的加载过程 就能明白 我目前采用的方案的原理了

-> 创建 RCTRootView,为 React Native 提供原生 UI 中的根视图。

-> 创建 RCTBridge,提供 iOS 需要的桥接功能。

-> 创建 RCTBatchedBridge,实际上是这个对象为 RCTBridge 提供方法,让其将这些方法暴露出去。 [RCTCxxBridge start],启动 JavaScript 解析进程。 [RCTCxxBridge loadSource],通过 RCTJavaScriptLoader 下载 bundle,并且执行。

-> 建立 JavaScript 和 iOS 之间的 Module 映射。

-> 将模块映射到对应的 RCTRootView 当中。

可以看到 最重要的是 Bridge 所有的script 的加载都可以在这找到一些线索,通过debuger 我们可以找到一个关键的方法 executeSourceCode 这就是执行 js 代码的方法。如果要实现自己的分包我必须 重写这里面的逻辑 所以有了下面的代码

如果是dev 模式的话,可以把这些code 去掉换成 http的方式,当然这些都是后话了

  1. 实践
  • 首先是重载 executeSourceCode 和定义 brige
//  ViewController.h

#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>

// 保留出这个 方法
@interface RCTBridge (PackageBundle)

- (RCTBridge *)batchedBridge;
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;

@end

@interface ViewController : UIViewController

@property (nonatomic, strong) RCTBridge *bridge;
@end
  • 其次我们要重新编写一下我们的js 的build 脚本,因为ios 和android 的打出来的包不一样!,😢 之前一只使用android 的 common 包 和 bu(注意我的bu包是 ios 和ios2.js) 包,一直报错 ,找好久才找到原因
{
  "build:common-ios": "react-native bundle --platform ios --dev false --entry-file ./common.js --bundle-output ./bundle/common.ios.bundle   --config ./metro.common.config.js  --minify false --reset-cache",
    "build:ios1": "react-native bundle --entry-file ./IOS.js --bundle-output ./bundle/IOS.ios.bundle --platform ios --assets-dest ./bundle  --config ./metro.main.config.js --minify false --dev false",
    "build:ios2": "react-native bundle --entry-file ./IOS2.js --bundle-output ./bundle/IOS2.ios.bundle --platform ios --assets-dest ./bundle  --config ./metro.main.config.js --minify false --dev false"
}

别忘记了!你在build 的时候要把bu的其实 id 搞进去!

{
  "index": 10000000,
  "Bu1": 20000000,
  "Bu2": 30000000,
  // 把下面的bu 加上!
  "IOS": 40000000,
  "IOS2": 50000000
}
  • 然后我们来测试一下 使用分包的模式先载入 common 再载入 bu包, 注意啊 我们不采取dev环境下的从 service 载入 bundle 我们从本地文件载入 ,因此有改动 需要先build 再去运行 查看效果
//  ViewController.m
-(instancetype) init {
    self = [super init];
    [self initBridge];
    return  self;
};

- (void) initBridge {
    if(!self.bridge) {
        NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"bundle/common.ios" withExtension:@"bundle"];
       // 初始化 bridge,并且加载主包
        self.bridge = [[RCTBridge alloc] initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:nil];
    }
  
};

// 在点击load 的时候 让 brige 再执行一次bu 的js 
++++
-(void) loadScript {
    NSString * bundlePath = @"bundle/IOS2.ios";
    NSString * bunldeName = @"IOS2";
    NSURL  *jsCodeLocation = [[NSBundle mainBundle] URLForResource:bundlePath withExtension:@"bundle"];
    
    if(self.bridge) {
        NSError *error = nil;
        NSData *sourceBuz = [NSData dataWithContentsOfFile:jsCodeLocation.path
                                                options:NSDataReadingMappedIfSafe
                                                  error:&error];
        
        [self.bridge.batchedBridge executeSourceCode:sourceBuz sync:NO];
        
        RCTRootView *rootView =
          [[RCTRootView alloc] initWithBridge:self.bridge moduleName:bunldeName initialProperties:nil];
        UIViewController *vc = [[UIViewController alloc] init];
        vc.view = rootView;
        [self presentViewController:vc animated:YES completion:nil];
    };
}

可以看到 ,现在我们单独的一个bu 已经可以完全集成了,为了以后简化 函数调用我们把loadScript 改造成参数的方式

//  ViewController.h

@interface ViewController : UIViewController
@property (nonatomic, strong) RCTBridge *bridge;
-(void) loadScript:(NSString *)bundlePath bunldeName: (NSString *)bunldeName;
@end


//  ViewController.m
-(void) loadScript:(NSString *)bundlePath bunldeName: (NSString *)bunldeName {
    
    NSURL  *jsCodeLocation = [[NSBundle mainBundle] URLForResource:bundlePath withExtension:@"bundle"];
    
    if(self.bridge) {
        NSError *error = nil;
        NSData *sourceBuz = [NSData dataWithContentsOfFile:jsCodeLocation.path
                                                options:NSDataReadingMappedIfSafe
                                                  error:&error];
        
        [self.bridge.batchedBridge executeSourceCode:sourceBuz sync:NO];
        
        RCTRootView *rootView =
          [[RCTRootView alloc] initWithBridge:self.bridge moduleName:bunldeName initialProperties:nil];
        UIViewController *vc = [[UIViewController alloc] init];
        vc.view = rootView;
        [self presentViewController:vc animated:YES completion:nil];
    };
}

这就完了?当然没有啦,我们需要在RN中进行bu 的载入 和切换,我们需要一些桥接 的代码桥接到IOS中, 这一点我之前专门有文章讲解 ,如果你不懂请千万 , 同时这里还会设计到一个IOS的 知识比如notifaction 和 GCD,看不懂的话也没有关系 什么不懂google 一下 自己实践code一下就明白了,我们直接放出代码

注册RN 桥接模块,为了和Android 中保持一致,我们使用一样的名字 RNToolsManager,然后我们使用notifation 的方式 去直接调用View 中的code ,当然不要忘记了!一定要把这段代码加到主线程去 ,要不然会有问题

// ViewController.m
-(instancetype) init {
    self = [super init];
    [self initBridge];
    [self addObservers];
    return  self;
};


- (void)changeView:(NSNotification *)notif{

    NSString *bundlePath = @"";
    NSString *bunldeName = @"";
    bundlePath = [notif.object valueForKey:@"bundlePath"];
    bunldeName = [notif.object valueForKey:@"bunldeName"];
    
    //  OC 的代码 我是方便调试弄的 如果你不需要可以去掉然后 把     [self presentViewController:vc animated:YES completion:nil]; 也去掉,当然还是看你们的需求吧 
    [self dismissViewControllerAnimated:YES completion:nil];

    [self loadScript:bundlePath bunldeName:bunldeName];
};

// 监听通知
- (void)addObservers {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeView:) name:@"changeBunle" object:nil];
};

// 监听通知
- (void)removeObservers {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
};

- (void)dealloc {
    [self removeObservers];
};

//  RNToolPackage.m

#import "RNToolPackage.h"

@implementation RNToolPackage

RCT_EXPORT_MODULE(RNToolsManager)

// 最简单的一个方法 变更多个bundle
RCT_REMAP_METHOD(changeActivity,
                 changeActivityWithA:( NSString *)bundlePath bunldeName:( NSString*)bunldeName
                 ){
    
    // 重新设置一个rootView 
    dispatch_async(dispatch_get_main_queue(),^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"changeBunle" object:@{
            @"bundlePath":bundlePath,
            @"bunldeName":bunldeName,
        }];
    });
    
};

@end

最后要说的,我们需要统一一下android ios rn 的module 跳转方法

// ./common/native/index.js
changeActivity: (value) => {
    // 此处可以优化 把名字全部统一,只需要确定一个规则 path 为 [moduleName].[platform].bundle
    // 比如 common.ios.bundle, IO2.ios.bundle, common.android.bundle, IO2.android.bundle, 
    // 参数只需要 传递 IO2 就好了这个IOS2 应该和模块的 registerComponent name 保持一致!
    if(Platform.OS === 'ios') {
      return NativeModules.RNToolsManager.changeActivity(`bundle/${value}.ios`, value); 
    }
    return NativeModules.RNToolsManager.changeActivity(value, null);
  },

Todo

项目 Android IOS
依照官方进行集成 ✅ 完成 ✅ 完成
dev是否正常运行 ✅ 完成 ✅ 完成
build 一下是否正常运行 ✅ 完成 ✅ 完成
Assets 资源加载逻辑 ✅ 完成 ✅ 完成
native版本的包管理 ✅ 完成 ✅ 完成
------ ------ ------
初步的拆包方案 ✅ 完成 ✅ 完成
优化拆包方案 common + bu = runtime ✅ 完成 ✅ 完成
容器的缓存复用 ✅ 完成 ✅ 完成(bridge 复用)
------ ------ ------
热更新的实现 ✅ 完成 ✅完成
WebView 的实现 / /

参考和感谢

RN 的Android 端执行过程

一种RN的分包策略

ReactNative JNI C++ 源代码

RN 在IOS 中的build 方式

RN的CI/CD到 IOS脚本分析

RN集成到IOS- 1

RN集成到IOS- 2

为什么IOS要禁用 字节编译

About

这是一个 客户端集成RN的项目

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published