腾讯招聘

# 小白学习:已有 Android 项目如何实现动态加载 RN bundle

作为小白的我学习 RNWeb 的过程中,在 Now 上看到调试 RN 时只需要输入 Bundle 名称和 module name 就可以打开 RN 页面进行调试,让我滋生了学习动态加载 bundle 展示 rn 页面的想法。在了解如何做动态加载之前,先简单了解一下 RN 加载流程。

# 一、RN 加载流程简析

在使用 RN cli 生成一个项目之后,程序的入口对应用进行注册

import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

AppRegistry.registerComponent(appName, () => App);

https://github.com/facebook/react-native/blob/master/Libraries/ReactNative/AppRegistry.js (opens new window)中查看registerComponent源码得知,如果要注册多个 RN 应用需要确保appName的唯一性。

    registerComponent(
        appKey: string,
        componentProvider: ComponentProvider,
        section?: boolean,
      ): string {
        let scopedPerformanceLogger = createPerformanceLogger();
    		//注册过的应用
        runnables[appKey] = {
          componentProvider,
          run: appParameters => {
            renderApplication(
              componentProviderInstrumentationHook(
                componentProvider,
                scopedPerformanceLogger,
              ),
              appParameters.initialProps,
              appParameters.rootTag,
              wrapperComponentProvider && wrapperComponentProvider(appParameters),
              appParameters.fabric,
              showArchitectureIndicator,
              scopedPerformanceLogger,
              appKey === 'LogBox',
            );
          },
        };
        if (section) {
          sections[appKey] = runnables[appKey];
        }
        return appKey;
      },

上面已经简单知道了注册 RN 应用,需要传入一个appName和具体渲染的根组件,那么就Android而言,应用是如何启动的呢?打开 Android java 目录可以看到存在两个文件,一个是MainActivityMainApplication ,熟悉 Android 开发的同学应该都知道MainApplication是一个应用启动类,而MainActivity是应用启动入口(在 AndroidManifast.xml 中注册过);

MainActivity中需要返回应用的模块名称也就是在上面 RN 中注册的模块名称

    public class MainActivity extends ReactActivity {

      /**
       * Returns the name of the main component registered from JavaScript. This is used to schedule
       * rendering of the component.
       */
      @Override
      protected String getMainComponentName() {
        return "AwesomeProject";
      }
    }

MainApplication继承了ReactApplication,并实现了getReactNativeHost方法。

    public class MainApplication extends Application implements ReactApplication {

      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
       ....
      };

      @Override
      public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
      }

      @Override
      public void onCreate() {
        super.onCreate();
    		//官方描述为:自动检查和加载多个有依赖关系的so库文件;SoLoader的主要作用就是作为Java和Javascript两种程序语言之间的通信桥梁
        SoLoader.init(this, /* native exopackage */ false);
      }
    }

ReactNativeHost是干嘛的?观看源码得知其本身就是一个抽象类,并且具有一系列的抽象方法getJSMainModuleNamegetPackagesgetJSBundleFilegetBundleAssetName;这似乎和我们想要实现动态加载 js bundle 有点契合,如果我们手动实现一个ReactNativeHost的类,复写getJSMainModuleNamegetBundleAssetNamegetJSBundleFile将具体的值改造成远程加载的 js bundle 名称,也许就可实现远程加载

    public abstract class ReactNativeHost {
    	...
      protected @Nullable
      JSIModulePackage getJSIModulePackage() {
        return null;
      }


      protected String getJSMainModuleName() {
        return "index.android";
      }

      protected @Nullable String getJSBundleFile() {
        return null;
      }


      protected @Nullable String getBundleAssetName() {
        return "index.android.bundle";
      }

    	...
    }

由于我们是在已有 Android 基础上使用 RN 页面,当点击某个按钮/某个功能的情况下才会加载对应的 js 文件,这似乎在MainApplication处写ReactNativeHost不大合适,那么有没有可能在在一个Activity里面返回自定义的ReactNativeHost的呢?

答案是有的。

让我们再来看一下啊MainActivity,他继承了 ReactActivity,打开ReactActivity一看,卧槽,就是一个抽象类。

    /**
     * Base Activity for React Native applications.
     */
    public abstract class ReactActivity extends Activity
        implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

      private final ReactActivityDelegate mDelegate;

      protected ReactActivity() {
        mDelegate = createReactActivityDelegate();
      }

      protected @Nullable String getMainComponentName() {
        return null;
      }

      protected ReactActivityDelegate createReactActivityDelegate() {
        return new ReactActivityDelegate(this, getMainComponentName());
      }

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDelegate.onCreate(savedInstanceState);
      }
    	... //此处省略一些生命周期函数
      protected final void loadApp(String appKey) {
        mDelegate.loadApp(appKey);
      }
    }

其主要作用:

  1. 提供  getMainComponentName  方法的声明
  2. 创建  ReactActivityDelegate  实例,便于把具体的功能全委托给  ReactActivityDelegate  类来处理

既然他把所以的工作都委托给了ReactActivityDelegate那么我们可以在ReactActivityDelegate中找到ReactNativeHost的实例吗?

    //ReactActivityDelegate源码

    public class ReactActivityDelegate {
    	... //省略一些初始化
      protected ReactRootView createRootView() {
        return new ReactRootView(getContext());
      }

      //正是我们要找的ReactNativeHost
      protected ReactNativeHost getReactNativeHost() {
        return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
      }

      public ReactInstanceManager getReactInstanceManager() {
        return getReactNativeHost().getReactInstanceManager();
      }

      protected void onCreate(Bundle savedInstanceState) {
        if (mMainComponentName != null) {
          loadApp(mMainComponentName);
        }
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
      }

      protected void loadApp(String appKey) {
        if (mReactRootView != null) {
          throw new IllegalStateException("Cannot loadApp while app is already running.");
        }
        mReactRootView = createRootView();
        mReactRootView.startReactApplication(
          getReactNativeHost().getReactInstanceManager(),
          appKey,
          getLaunchOptions());
        getPlainActivity().setContentView(mReactRootView);
      }
    	... //省略一些生命周期函数执行
    }

在查看ReactActivityDelegate的源码中我们发现了getReactNativeHost方法,那么是不是意味着我们可以在 Activity 中重写ReactNativeHost

ReactActivityDelegate中我们可以看到 RN 应用加载的核心逻辑其实就两句话:创建视图并加载对应 Bundle 文件。

     mReactRootView = createRootView();//ReactRootView是一个自定义View,且继承自FrameLayout
     mReactRootView.startReactApplication(
          getReactNativeHost().getReactInstanceManager(),
          appKey,
          getLaunchOptions());

问题来了ReactNativeHost到时是干什么的?ReactInstanceManager又是干什么的?查看源码得知,核心在于ReactInstanceManager,这个类管理 CatalystInstance 的实例。他通过 ReactPackage 对外暴露一种设置 catalyst 实例的方式,并且跟踪实例的生命周期。

看了源码发现动态加载的核心在如何创建一个**CatalystInstance**实例,其中主要包括设置 js 模块的路径,应用上下文信息等

    public abstract class ReactNativeHost {

      private final Application mApplication;
      private @Nullable ReactInstanceManager mReactInstanceManager;

    	 ... //此处省略

      protected ReactInstanceManager createReactInstanceManager() {
        ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
        ReactInstanceManagerBuilder builder =
            ReactInstanceManager.builder()
                .setApplication(mApplication)//设置应用上下文
                .setJSMainModulePath(getJSMainModuleName())//设置需要加载的js bundle路径
                .setUseDeveloperSupport(getUseDeveloperSupport()) //是否开启dev模式
                .setRedBoxHandler(getRedBoxHandler()) //设置红盒回调,dev模式才会有红屏展示
                .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())// 设置 js 的执行器工厂实例,为后续加载和解析 js bundle 作准备
                .setUIImplementationProvider(getUIImplementationProvider()) // 设置UI实现机制的Provider
                .setJSIModulesPackage(getJSIModulePackage())//设置js模块到jsbridge中
                .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);//初始化生命周期

        for (ReactPackage reactPackage : getPackages()) {
          builder.addPackage(reactPackage);
        }

        String jsBundleFile = getJSBundleFile();
        if (jsBundleFile != null) {
          builder.setJSBundleFile(jsBundleFile);
        } else {
          builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
        }
        ReactInstanceManager reactInstanceManager = builder.build();
        ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
        return reactInstanceManager;
      }

      protected @Nullable JSIModulePackage getJSIModulePackage() {
        return null;
      }

      protected String getJSMainModuleName() {
        return "index.android";
      }

      protected @Nullable String getJSBundleFile() {
        return null;
      }


      protected @Nullable String getBundleAssetName() {
        return "index.android.bundle";
      }

    }

好啦,基础部分暂时就到这里啦,再往下写就得偏题啦。

# 二、动态加载工程实现

有了上面的解决办法,那我们开始整理一下这个工程实现的思路。

  1. 工程搭建
    • 搭建 2 个 RN 工程,并打离线包作为 app 动态加载 RN 的资源文件;
    • 搭建一个Android工程,用于编写加载逻辑(为了方便,就直接使用react-native创建一个 rn 工程,我们只使用其中的android部分)
    • 使用 express 或者 koa 搭建一个后端服务返回模块信息和对应的离线包;
  2. 编写 app 端主逻辑
    • 使用 RecyclerView 呈现出一个列表(其中包括 adapter 编写,使用 okhttp 请求服务端数据)
    • 点击列表中每一项向后端服务请求对应的 RN 离线包
    • 下载完成离线包解压缩,并跳转到 RN 对应的 Activity
  3. 编写 RN 加载的 Activity
    • 复写 createReactActivityDelegate 返回自定义 ReacActivity 的委托
    • 自定义 ReactNativeHost 修改 js bundle 对应加载逻辑;

其主要流程如下:

# 1、工程搭建

首先让我们建立两个 rn 工程:

    react-native init module_1 #第一个RN工程
    react-native init module_2 #第二个RN工程

    react-native init app # 主工程(为了省去自建Android项目的过程及rn相关依赖)

    express mock_server # 创建一个后端服务

打离线包

    # model_1工程,打离线包并且压缩成zip (zip -q -r -o module_1.zip ./module_1 非Mac的同学可以删掉这句话)
    react-native bundle --entry-file index.js --bundle-output ./module_1/module_1.bundle --platform android --assets-dest ./module_1 --dev false  zip -q -r -o module_1.zip ./module_1
    # model_2工程
    react-native bundle --entry-file index.js --bundle-output ./module_2/module_2.bundle --platform android --assets-dest ./module_2 --dev false  zip -q -r -o module_2.zip ./module_2

如何打离线包?离线包的命令都是些啥?输入react-native bundle —help就知道咯

    react-native bundle [参数]
      构建 js 离线包

      Options:

        -h, --help                   输出如何使用的信息
        --entry-file <path>          RN入口文件的路径, 绝对路径或相对路径
        --platform [string]          ios 或 andorid
        --transformer [string]       Specify a custom transformer to be used
        --dev [boolean]              如果为false, 警告会不显示并且打出的包的大小会变小
        --prepack                    当通过时, 打包输出将使用Prepack格式化
        --bridge-config [string]     使用Prepack的一个json格式的文件__fbBatchedBridgeConfig 例如: ./bridgeconfig.json
        --bundle-output <string>     打包后的文件输出目录, 例: /tmp/groups.bundle,tmp目录没有需要手动创建哦
        --bundle-encoding [string]   打离线包的格式 可参考链接https://nodejs.org/api/buffer.html#buffer_buffer.
        --sourcemap-output [string]  生成Source Map,但0.14之后不再自动生成source map,需要手动指定这个参数。例: /tmp/groups.map
        --assets-dest [string]       打包时图片资源的存储路径
        --verbose                    显示打包过程
        --reset-cache                移除缓存文件
        --config [string]            命令行的配置文件路径

运行完成之后可以在对应工程下面查看 module_n 目录下有对应的 bundle 文件,

可能会出现的错误:解决办法(在 model_1/model_2 工程下新建一个 module_n 目录)

    Loading dependency graph...info Writing bundle output to:, ./bundle/module_1.bundle
    error ENOENT: no such file or directory, open './module_1/module_1.bundle'. Run CLI with --verbose flag for more details.

# 2、后端服务编写

我们使用express创建一个 Mock 服务,主要包括模块信息的返回和对应模块 zip 文件的下载。那么我们需要将打包之后的module_1/module_2目录压缩成 zip 各式。

const express = require("express");
const router = express.Router();

/**
 * 返回对应module信息
 */
router.get("/modules", function(req, res, next) {
  res.json({
    data: [
      {
        name: "module_1"
      },
      {
        name: "module_2"
      }
    ]
  });
});
/**
 * 根据名称返回对应module的zip文件
 */
router.get("/download/:name", function(req, res, next) {
  const name = req.params.name;
  res.download(`static/${name}.zip`);
});

module.exports = router;

# 3、Android 工程改造

前面提到我们直接使用 rn 创建的一个 Android 工程,那么现在我们就对这个 Android 工程进行改造吧。

Step1:改造MainApplication.java其他部分的代码干掉,值保留SoLoader加载器,前面提到 SoLoader 的作用是加载 so 文件,作为 js 和 Java 通信的桥梁。

    public class MainApplication extends Application implements ReactApplication {


      @Override
      public void onCreate() {
        super.onCreate();
        SoLoader.init(this, /* native exopackage */ false);
      }

    }

Step2: 新增okhttp等依赖。由于需要向后端服务请求对应 zip 包,所以需要 http 请求。

    //在android的app目录下的build.gradle文件新增如下依赖
    implementation 'com.liulishuo.filedownloader:library:1.7.4' // 文件下载
    implementation "com.squareup.okhttp3:okhttp:3.12.0" // 网络请求
    implementation 'com.google.code.gson:gson:2.8.5' // json转换
    implementation 'com.android.support:recyclerview-v7:28.0.0' //列表展示

Step3: 编写主逻辑

  • 我们设想在 App 首页请求后端服务,查看到当前所有 RN 的 module
  • 点击列表中的每一个,跳转到对应的 RN 页面。

Step4: 使用 okhttp 获取模块列表,点击下载对应 Bundle 文件

由于需要调用服务,需要在 AndroidManifest.xml 文件中声明网络获取权限,详情代码请见文末【源码】

    public class MainActivity extends AppCompatActivity {
        RecyclerView recyclerView;
        ListModuleAdapter adapter;

        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            initView();
            featchData();
        }

        /**
         * 初始化布局视图,默认数据为空
         */
        public void initView() {
            recyclerView = this.findViewById(R.id.list);
            recyclerView.setLayoutManager(new LinearLayoutManager(this));
            adapter = new ListModuleAdapter(this, new ArrayList<ModuleItem.Bundle>());
            recyclerView.setAdapter(adapter);
            adapter.setOnItemClickListener(bundle -> {
                // 检查是否下载过,如果已经下载过则直接打开,暂不考虑各种版本问题
                String f = MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundle.name + "/" + bundle.name + ".bundle";
                File file = new File((f));
                if (file.exists()) {
                    goToRNActivity(bundle.name);
                } else {
                    download(bundle.name);
                }
            });
        }

        /**
         * 跳转到RN的展示页面
         * @param bundleName
         */
        public void goToRNActivity(String bundleName) {
            Intent starter = new Intent(MainActivity.this, RNDynamicActivity.class);
            RNDynamicActivity.bundleName = bundleName;
            MainActivity.this.startActivity(starter);
        }

        /**
         * 调用服务获取数据
         */
        public void featchData() {
            OkHttpClient okHttpClient = new OkHttpClient();
            Request request = new Request.Builder().url(API.MODULES).method("GET", null).build();
            Call call = okHttpClient.newCall(request);
            call.enqueue(new Callback() {

                @Override
                public void onFailure(Call call, IOException e) {
                    System.out.println("数据获取失败");
                    System.out.println(e);
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String data = response.body().string();

                    ModuleItem moduleItem = new Gson().fromJson(data, ModuleItem.class);
                    runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
    												//刷新列表
                            adapter.clearModules();
                            adapter.addModules(moduleItem.data);
                        }
                    });


                }
            });
        }

        /**
         * 下载对应的bundle
         *
         * @param bundleName
         */
        private void download(final String bundleName) {
            System.out.println(API.DOWNLOAD + bundleName);
            FileDownloader.setup(this);
            FileDownloader.getImpl().create(API.DOWNLOAD + bundleName).setPath(this.getFilesDir().getAbsolutePath(), true)

                    .setListener(new FileDownloadListener() {
                        @Override
                        protected void started(BaseDownloadTask task) {
                            super.started(task);
                        }

                        @Override
                        protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {

                        }

                        @Override
                        protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {

                        }

                        @Override
                        protected void completed(BaseDownloadTask task) {

                            try {
                                //下载之后解压,然后打开
                                ZipUtils.unzip(MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundleName + ".zip", MainActivity.this.getFilesDir().getAbsolutePath());

                               goToRNActivity(bundleName);

                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                        }

                        @Override
                        protected void paused(BaseDownloadTask task, int soFarBytes, int totalBytes) {

                        }

                        @Override
                        protected void error(BaseDownloadTask task, Throwable e) {
                        }

                        @Override
                        protected void warn(BaseDownloadTask task) {

                        }
                    }).start();
        }
    }

**Step5:**自定义 RN 加载页面(记得在AndroidManifest.xml 文件中声明)并创建ReacActivity委托对象DispatchDelegate

    //RNDynamicActivity
    public class RNDynamicActivity extends ReactActivity {
    		public static String bundleName;

        @Override
        protected ReactActivityDelegate createReactActivityDelegate() {

            DispatchDelegate delegate = new DispatchDelegate(this, bundleName);

            return delegate;
        }
    }

    //DispatchDelegate
    public class DispatchDelegate extends ReactActivityDelegate {

        private Activity activity;
        private String bundleName;


        public DispatchDelegate(Activity activity, @Nullable String bundleName) {
            super(activity, bundleName);
            this.activity = activity;
            this.bundleName = bundleName;
        }

        @Override
        protected ReactNativeHost getReactNativeHost() {

            ReactNativeHost mReactNativeHost = new ReactNativeHost(activity.getApplication()) {
                @Override
                public boolean getUseDeveloperSupport() {
                    return BuildConfig.DEBUG;
                }
    						//注册原生模块,这样
                @Override
                protected List<ReactPackage> getPackages() {
                    return Arrays.<ReactPackage>asList(
                            new MainReactPackage()
                    );
                }

                @Nullable
                @Override
                protected String getJSBundleFile() {
    								// 读取已经解压的bundle文件
                    String file = activity.getFilesDir().getAbsolutePath() + "/" + bundleName + "/" + bundleName + ".bundle";
                    return file;
                }

                @Nullable
                @Override
                protected String getBundleAssetName() {
                    return bundleName + ".bundle";
                }

                @Override
                protected String getJSMainModuleName() {
                    return "index";
                }
            };
            return mReactNativeHost;
        }
    }

其余详细代码请见:

MrGaoGang/luckly_learn (opens new window)

最后效果图如下:

其实真实的加载远不止如此简单,还有很多方面需要我我们考虑。比如:

  • 离线包版本管理
  • 离线包加载时机,比如用户在什么时间下载离线包,加载离线包,已经打开了当前离线包,下次打开是否更新?等
  • 离线包缓存机制
  • 权限管理
  • 降级策略
  • 等待

# 三、可能会遇到的问题及疑问

1、为什么页面跳转到 RNDynamicActivity 没有使用 Intent 传递数据

这就要看 Activity 中createReactActivityDelegateonCreate执行顺序了,查看 ReactActivity 源码得知createReactActivityDelegate是在 RectActivity 初始化的时候执行,所以优先于onCreate执行

    public abstract class ReactActivity extends AppCompatActivity
        implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

      private final ReactActivityDelegate mDelegate;

      protected ReactActivity() {
        mDelegate = createReactActivityDelegate();
      }


      protected @Nullable String getMainComponentName() {
        return null;
      }

      /** Called at construction time, override if you have a custom delegate implementation. */
      protected ReactActivityDelegate createReactActivityDelegate() {
        return new ReactActivityDelegate(this, getMainComponentName());
      }

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

2、出现模块没有注册的报错

请检查工程的名称是否和打包的名称相同

3、报错:错误: 需要 class, interface 或 enum

# 四、源码

MrGaoGang/luckly_learn (opens new window)

【未经作者允许禁止转载】 Last Updated: 7/28/2021, 9:07:20 AM