React-Native系列Android——Javascript文件加载过程分析_jsbundleloader-程序员宅基地

技术标签: React-Native  android  加载  react  javascript  

React-Native应用程序的内容是由Javascript语言开发的,而Android或者IOS手机系统只是一个容器和各类服务提供者。

众所周知,Javascript是一门解释型脚本语言,对于浏览器而言,浏览器负责解释和执行Javascript脚本。而对于手机系统而言,同样是负责解释和执行Javascript脚本,当然其核心都是使用的webkit内核。

浏览器获取Javascript脚本,主要通过网络下载 + 本地缓存的机制,达到效率的最大化。当然,移动应用也不例外,但不同的是移动应用可以将Javascript脚本直接打包在应用程序内,免去网络下载这个极其不稳定的过程,这样可以达到加载效率和性能流畅的最大化,也就是风靡一时Hybrid技术,而这一点浏览器是做不到的。

无论使用网络下载还是本地文件,最终都是要加载JS文件,而React-Native项目中包含大量的JS文件构成的框架和组件,那么Android框架又是如何去加载它们的呢?这个过程就是本篇博客的研究的主题了!


1、JS文件的整合

有这样一个常识:拷贝1001M的文件,比拷贝1100M的文件要慢的多。

一个React-Native项目中,包含有成百上千个JS文件,可以想象,如果一次性加载(读)这么多个文件,其效率将会极其低下。但是如果将这些JS文件预先合并成一个文件,然后去加载,其效率肯定能提高很多。

当所有相关的JS文件合并成一个文件后,还需要进行优化。包括去除空格和换行符、代码混淆等,这样处理之后会有两个好处:
1、大幅减小文件大小,无论是对加载效率还是应用体积,好处都是莫大的。
2、提高应用程序的安全性,防止反编译等。

那么,React-Native框架是如何整合JS文件的呢?

首先,需要知道一点,这个整合过程肯定是极其缓慢的,毕竟涉及上千个文件,所以不能是放在应用程序内进行,最合适的做法是预处理,即时机放在打包或者编译时。

另外,Javascript前端开发的模式流程和移动应用开发的模式流程是完全不一样的。Javascript开发者,不需要反复的打包安装应用,对他们而言,一个解释执行器(比如浏览器)就够了,所有的代码都直接放在本地服务器。

React-Native很好地遵循了这一模式,一次安装的应用程序作为解释执行器,nodejs服务器作为本地服务器,所有的JS文件全部部署在这个服务器上。前端开发者修改完代码,直接在应用程序上reload一下就能看到结果。这种模式,对前端开发者来说几乎不要学习什么,完全是轻车熟路的。

所以,JS整合的工作,自然就是交给nodejs服务器来做了!整合过程的细节不是本博客的重点,就不去分析了。

如果是正式发布包,在应用运行时,是不存在本地nodejs服务器这个概念的,所以JS整合文件都是预先打包到assets资源文件里的。下面,来看下这个打包过程。

JS整合文件的打包逻辑,位于项目\android\app\react.gradle

...
def devEnabled = !targetName.toLowerCase().contains("release")
commandLine "cmd", "/c", "react-native", "bundle", "--platform", "android", "--dev", "${devEnabled}", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir
...

gradle打包流程里面插入一个自定义Task任务,即在命令行中运行react-native bundle命令,整合和优化JS文件,存放到assets资源文件目录中。

来看一下react-native bundle命令的参数。

这里写图片描述

–entry-file: 应用入口文件,默认为项目根目录下的index.android.jsindex.ios.js

–platform:系统平台,android或者ios选其一

–transformerbabel转换器,默认使用\node_modules\react-native\packager\transformer.js

–dev:是否开发模式,默认开启,此时不会进行JS混淆和压缩优化,方便开发者调试。

–bundle-output: 最终整合的输出文件名,一般是index.android.bundleindex.ios.bundle

–bundle-encoding:整合文件的编码格式,默认utf-8

–assets-dest:整合文件存储目录,android打包时会定义为项目的assets资源编译临时目录。

所以,Android项目打正式包的时候,运行的命令如下:

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output index.android.bundle --assets-dest xxx

其中xxx表示编译资源包时assets所在临时目录,一般为app/build/intermediates/res/merged/release/的绝对路径。

最终apk安装包的assets文件夹下将有一个名为index.android.bundleJS文件(无扩展名)。当应用程序启动的时候,只要去加载这个文件,整个React-Native就被完全启动了!

有趣的是,React-Native还额外提供了一个unbundle命令,使用方式和bundle命令完全相同。unbundle命令是在bundle命令的基础上增加了一项功能,除了生成整合JS文件index.android.bundle外,还会生成各个单独的未整合JS文件(但会被优化),全部放在js-modules目录下,同时会生成一个名为UNBUNDLE的标识文件,一并放在其中。UNBUNDLE标识文件的前4个字节固定为0xFB0BD1E5,用于加载前的校验。需要注意的是,js-modules目录会一并打包到apkassets文件夹中,如果使用unbundle命令的话。

另外,unbundle命令是后来增加扩展的功能,到目前为止并没有使用到,这里提到是因为后面分析JS文件加载时会有特殊处理。


2、JS文件的加载

不管JS文件是从服务器下载,还是直接使用本地文件,最终都是需要一次性加载到webkit内核的解释器中的。当然,这部分功能都是有Native框架完成的,我们来研究一下。

首先,来看需要加载的对象。

生产模式下,需要加载的JS文件为assets/index.android.bundle

开发模式下,需要先从服务器下载到本地,缓存文件为data/data/package-name/files/ReactNativeDevBundle.js

代码位于com.facebook.react.devsupport.DevSupportManagerImpl.java

public class DevSupportManagerImpl implements DevSupportManager {
    
   ...
   private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js";
   ...
   // We store JS bundle loaded from dev server in a single destination in app's data dir.
    // In case when someone schedule 2 subsequent reloads it may happen that JS thread will
    // start reading first reload output while the second reload starts writing to the same
    // file. As this should only be the case in dev mode we leave it as it is.
    // TODO(6418010): Fix readers-writers problem in debug reload from HTTP server
    mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);

}

加载是由JSBundleLoader来处理的,提供了三种处理方式:

1、加载本地JS文件,包括assets文件和普通文件。
2、加载网络JS文件,同时提供缓存目录,方便reload时直接切换到1方式。
3、加载网络JS文件,直接远程调用,用于debug调试。

仔细阅读代码,发现后两种方式,和第1种调用的API一样,所以我们只要看第1种处理方式就行了。

public abstract class JSBundleLoader {
    

  public static JSBundleLoader createFileLoader(
      final Context context,
      final String fileName) {
    return new JSBundleLoader() {
      @Override
      public void loadScript(ReactBridge bridge) {
        if (fileName.startsWith("assets://")) {
          bridge.loadScriptFromAssets(context.getAssets(), fileName.replaceFirst("assets://", ""));
        } else {
          bridge.loadScriptFromFile(fileName, "file://" + fileName);
        }
      }

      @Override
      public String getSourceUrl() {
        return (fileName.startsWith("assets://") ? "" : "file://") + fileName;
      }
    };
  }
}

和普通的磁盘文件不同,assets文件是存在于apk安装包内的,只能通过AssetManager来操作,不能直接读取。所以对于这两种情况,分别使用
loadScriptFromAssetsloadScriptFromFile来处理。

两种方式都是通过ReactBridge来调用到JNI层,来看这两个native方法注册的部分,位于\jni\react\jni\OnLoad.cpp

registerNatives("com/facebook/react/bridge/ReactBridge", {
     ...
     makeNativeMethod("loadScriptFromAssets", "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", bridge::loadScriptFromAssets),
     makeNativeMethod("loadScriptFromFile", bridge::loadScriptFromFile),
     ...
});

2.1 加载Assets文件

先来看bridge::loadScriptFromAssets的逻辑,同样在OnLoad.cpp文件里

static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, jstring assetName) {
  ...
  auto manager = AAssetManager_fromJava(env, assetManager);
  auto bridge = extractRefPtr<CountableBridge>(env, obj);
  auto assetNameStr = fromJString(env, assetName);
  ...
  auto script = react::loadScriptFromAssets(manager, assetNameStr);
  ...
  if (JniJSModulesUnbundle::isUnbundle(manager, assetNameStr)) {
    loadApplicationUnbundle(bridge, manager, script, "file://" + assetNameStr);
  } else {
    loadApplicationScript(bridge, script, "file://" + assetNameStr);
  }
  if (env->ExceptionCheck()) {
    return;
  }
  ...
}

assetManager: 指的是 Android系统的资源管理器AssetManager(java), 所有资源文件都是通过它来管理的,这里是通过系统动态链接库的android/asset_manager_jni.hAAssetManager_fromJava方法获取到AAssetManager(c++)对象的操作指针。

assetName: 这里是文件名,为index.android.bundle

接下来,通过JSLoader对象的loadScriptFromAssets方法读文件,得到字符串script,也就是JS的内容。

下面一步,判断isUnbundle

前面提过,如果打包时使用unbundle命令,会在assets中生成js-modules文件夹,里面存放着标志文件UNBUNDLE和各个单独未整合到一起的JS文件。在C++层中有着专门的类JniJSModulesUnbundle来处理这些文件,代码位于react\jni\JniJSModulesUnbundle.cpp

先来看JniJSModulesUnbundle::isUnbundle

const magic_number_t MAGIC_FILE_HEADER = 0xFB0BD1E5;
const std::string MAGIC_FILE_NAME = "UNBUNDLE";
...
bool JniJSModulesUnbundle::isUnbundle(AAssetManager *assetManager, const std::string& assetName) {
  if (!assetManager) {
    return false;
  }

  auto magicFileName = jsModulesDir(assetName) + MAGIC_FILE_NAME;
  auto asset = openAsset(assetManager, magicFileName.c_str());
  if (asset == nullptr) {
    return false;
  }

  magic_number_t fileHeader = 0;
  AAsset_read(asset.get(), &fileHeader, sizeof(fileHeader));
  return fileHeader == htole32(MAGIC_FILE_HEADER);
}

判断标志文件UNBUNDLE是否存在,并校验文件头部4个字节是否为
0xFB0BD1E5

如果isUnbundletrue的话,调用loadApplicationUnbundle,猜测应该是加载js-modules目录下面的单个JS文件了。

static void loadApplicationUnbundle(const RefPtr<CountableBridge>& bridge, AAssetManager *assetManager, const std::string& startupCode, const std::string& startupFileName) {
  try {
    bridge->loadApplicationUnbundle(std::unique_ptr<JSModulesUnbundle>(new JniJSModulesUnbundle(assetManager, startupFileName)), startupCode, startupFileName);
  } catch (...) {
    translatePendingCppExceptionToJavaException();
  }
}
JniJSModulesUnbundle::JniJSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile) :
  m_assetManager(assetManager),
  m_moduleDirectory(jsModulesDir(entryFile)) {
    }
static std::string jsModulesDir(const std::string& entryFile) {
  std::string dir = dirname(entryFile.c_str());

  // android's asset manager does not work with paths that start with a dot
  return dir == "." ? "js-modules/" : dir + "/js-modules/";
}

首先,创建一个JniJSModulesUnbundle对象,里面保存着AAssetManager对象指针m_assetManager和文件根目录m_moduleDirectory,有了这两者,只要知道文件名,就能获取到指定的JS文件了。

接下来,调用bridge->loadApplicationUnbundle,实现代码在react\Bridge.cpp,由于Bridge.cpp只是对JSExecutor的转发,所以直接来看react\JSCExecutor.cpp

void JSCExecutor::loadApplicationUnbundle(std::unique_ptr<JSModulesUnbundle> unbundle, const std::string& startupCode, const std::string& sourceURL) {
  if (!m_unbundle) {
    installGlobalFunction(m_context, "nativeRequire", nativeRequire);
  }
  m_unbundle = std::move(unbundle);
  loadApplicationScript(startupCode, sourceURL);
}

参数中的智能指针unbundle会被赋值给JSCExecutor对象的m_unbundle,由于m_unbundle初始为空,所以第一次会执行installGlobalFunction

installGlobalFunction方法的作用是为JavascriptGlobal全局对象动态创建属性函数,这里是创建了一个名为nativeRequire的属性,指向的函数是JSCExecutor::nativeRequire。如果在Javascript代码中使用nativeRequire,就会对应执行JSCExecutor::nativeRequire

比如,在Javascript中使用:

global.nativeRequire('TextInput')

就会加载assets/js-modules/TextInput.js这个文件,来看nativeRequire的实现细节。

JSValueRef JSCExecutor::nativeRequire(...){
   ...
   JSCExecutor *executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx));
   ...
   double moduleId = JSValueToNumber(ctx, arguments[0], exception);
   ...
   executor->loadModule(moduleId);
}
void JSCExecutor::loadModule(uint32_t moduleId) {
  auto module = m_unbundle->getModule(moduleId);
  auto sourceUrl = String::createExpectingAscii(module.name);
  auto source = String::createExpectingAscii(module.code);
  evaluateScript(m_context, source, sourceUrl);
}
JSModulesUnbundle::Module JniJSModulesUnbundle::getModule(uint32_t moduleId) const {
  ...

  std::ostringstream sourceUrlBuilder;
  sourceUrlBuilder << moduleId << ".js";
  auto sourceUrl = sourceUrlBuilder.str();

  auto fileName = m_moduleDirectory + sourceUrl;
  auto asset = openAsset(m_assetManager, fileName, AASSET_MODE_BUFFER);

  const char *buffer = nullptr;
  if (asset != nullptr) {
    buffer = static_cast<const char *>(AAsset_getBuffer(asset.get()));
  }
  ...
  return {sourceUrl, std::string(buffer, AAsset_getLength(asset.get()))};
}

nativeRequire函数的功能是加载js-modules目录中对应的JS文件,moduleId虽然是int型,但实质上是文件名(区别于通信机制中的moduleId),这里的m_unbundle就是前面保存的JSModulesUnbundle对象的智能指针了。

总结一下,loadApplicationUnbundle的主要功能是,为JavascriptGlobal全局对象创建nativeRequire函数,Javascript中调用时,能够加载对应的JS文件。

处理完unbundle的逻辑,该继续完成assets/index.android.bundle文件的加载了,前面分析到此文件的内容已经读成字符串script,无论是否isUnbundle,都会调用loadApplicationScript

void JSCExecutor::loadApplicationScript(const std::string& script, const std::string& sourceURL) {
  ...
  String jsScript = String::createExpectingAscii(script);
  ...

  String jsSourceURL(sourceURL.c_str());
  ...
  if (!jsSourceURL) {
    evaluateScript(m_context, jsScript, jsSourceURL);
  } else {
    // If we're evaluating a script, get the device's cache dir
    //  in which a cache file for that script will be stored.
    evaluateScript(m_context, jsScript, jsSourceURL, m_deviceCacheDir.c_str());
  }
  flush();
}

由于sourceURL的值不为空,所以执行的evaluateScript方法是带有缓存目录参数的,m_deviceCacheDir缓存目录为系统的/data/data/cache目录,用来存储script

evaluateScript方法的作用就是使用webkit去真正解释执行Javascript了!


2.2 加载普通File文件

相比于从assets中加载文件,直接加载磁盘文件就简单得多了,这种只用在开发模式中,加载从本地服务器上down到手机内存中的JS文件。

static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstring sourceURL) {
  ...
  auto bridge = jni::extractRefPtr<CountableBridge>(env, obj);
  auto fileNameStr = fileName == NULL ? "" : fromJString(env, fileName);
  ...
  auto script = fileName == NULL ? "" : react::loadScriptFromFile(fileNameStr);
  ...
  loadApplicationScript(bridge, script, jni::fromJString(env, sourceURL));
  ...
}

先将fileName对应的文件内容读读取成字符串script,然后调用loadApplicationScript使用webkit内核解释执行,需要特别注意的是如果fileName为空或者文件不存在,webkit内核在加载时,会使用sourceURL自动下载并缓存。

读文件loadScriptFromFile的实现在react\jni\JSLoader.cpp

std::string loadScriptFromFile(const std::string& fileName) {
  ...
  std::ifstream jsfile(fileName);
  if (jsfile) {
    std::string output;
    jsfile.seekg(0, std::ios::end);
    output.reserve(jsfile.tellg());
    jsfile.seekg(0, std::ios::beg);
    output.assign((std::istreambuf_iterator<char>(jsfile)), std::istreambuf_iterator<char>());
    return output;
  }
  ...
  return "";
}

简单的文件读取操作,不细说了。


3、最后一步

你以为到这里就结束了?当然不了!

还漏了一步,loadApplicationScript中使用evaluateScript解释执行Javascript代码,是没有处理执行结果的,也就是意味着Javascript的加载执行最终并没有能够和Native完全建立通信连接,所以Javascript的执行结果并没有反馈到Native端。

原因是我们漏了最后一步:flush

void JSCExecutor::flush() {
  // TODO: Make this a first class function instead of evaling. #9317773
  std::string calls = executeJSCallWithJSC(m_context, "flushedQueue", std::vector<folly::dynamic>());
  m_bridge->callNativeModules(*this, calls, true);
}

手动调用MessageQueue.jsflushedQueue方法,将Javascript执行过程中需要调用Native组件的通信请求通知到Native。这个过程在React-Native系列Android——Native与Javascript通信原理(二)中详细分析过。

  flushedQueue() {
    this.__callImmediates();

    let queue = this._queue;
    this._queue = [[], [], [], this._callID];
    return queue[0].length ? queue : null;
  }

这样,在JS加载后,Native组件也就被调用起来了,比如视图结构等等。

到此,JS文件的加载过程才算真正结束了。


本博客不定期持续更新,欢迎关注和交流:

http://blog.csdn.net/megatronkings

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/MegatronKings/article/details/51534250

智能推荐

Linux的用户管理-程序员宅基地

文章浏览阅读66次。用户管理:一.用户的配置文件:用户配置文件的路径:/etc/passwd/etc/passwd的一部分内容:每一行都是一个用户的信息(内容如下):用户名 :密码 :uid :gid :用户的备注 :用户的家目录 :和根交互使用的shell路径 二.用户超级用户root ----- uid为 0系统用户 ----- 使用的shell的路径为 /sbin/nologin ,uid为 201 —— 999,添加系统用户不会默认创建家目录和邮箱普通用户 ----- uid为 1000 ——

心田花开:二年级语文阅读《黄山奇石》附答案解析_黄山奇石阅读题及答案二年级-程序员宅基地

文章浏览阅读6.9k次。心田花开为大家分享了二年级语文上册黄山奇石练习题,本部分练习题包括了看拼音,写词语;选择合适的字;填空题;造句子及句子赏析,最后附加课文原文,希望能对大家学习有帮助。【原文】黄山奇石闻名中外的黄山风景区在我国安徽省南部。那里景色秀丽神奇,尤其是那些怪石,有趣极了。就说“仙桃石”吧,它好像从天上飞下来的一个大桃子,落在山顶的石盘上。在一座陡峭的山峰上,有一只“猴子”。它两只胳膊抱着腿,一动不动..._黄山奇石阅读题及答案二年级

css实现tab导航下划线动画效果(从中间过渡到两边)_css过度动画从中间向两边移动-程序员宅基地

文章浏览阅读3.2k次,点赞45次,收藏7次。tab导航的中间开始然后向两边过渡的动画效果,增加页面的美观性..._css过度动画从中间向两边移动

S7-1515-2pn 带pn总线设备有 库卡机器人 西门子S120伺服驱动器 sew伺服驱动器_pn总线伺服-程序员宅基地

文章浏览阅读175次。S7-1515-2pn 带pn总线设备有 库卡机器人 西门子S120伺服驱动器 sew伺服驱动器 pn绝对值编码器 SSI编码器应用 7个触摸屏包含程序 一个上位机组态画面包含程序 包含graph语言编写的程序 STL SCL语言编写的程序 模拟量采集 是学习西门子工艺对象组态运动控制 机器人等不可多得呢学习资料 全部程序均调试通过可以立即应用。通过对S7-1515-2pn带pn总线设备的应用,我们可以实现各种工业自动化控制,比如各种机器人的控制、数控机床的控制等等。_pn总线伺服

oracle查看表锁并解锁_oracle锁表查询和解锁方法-程序员宅基地

文章浏览阅读2.1k次。当在一个应用程序能改动数据库,而其他应用程序都不能改动时,基本就说明表被锁了。_oracle锁表查询和解锁方法

DBSCAN聚类算法及其参数配置-python实现_dbscan调参-程序员宅基地

文章浏览阅读1.5k次,点赞21次,收藏38次。DBSCAN聚类算法是一种基于空间密度有传递性质的聚类算法,将簇定义为密度相连的点的最大的集合,可以将高密度点区域划分为簇,并有效地过滤低密度点区域,可以在含有噪声的数据集中识别任意形状和数量的簇。_dbscan调参

随便推点

通信技术基础知识回顾_isdn传输距离-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏21次。通信技术基础知识汇总智能网(Intelligentized Network)的思想起源于美国。20世纪80年代初,AT&T公司就采用集中数据库方式提供800号(被叫付费)业务和电话记帐卡业务,这是智能网的雏形。后来国际电联ITU-T (International Telecommunications Union)在1992年正式命名了智能网一词。智能网是在现有交换与传输的基础网络结构上,为快速、方便、经济地提供电信新业务(或称增值业务)而设置的一种附加网络结构。智能网提供新业务的突出优点是可以做到快_isdn传输距离

【MATLAB】解决MATLAB安装后出现 “License Manager Error -8”(亲测有效)_matlab激活后报错error8-程序员宅基地

文章浏览阅读2w次,点赞2次,收藏10次。把应用程序拉取到桌面上。_matlab激活后报错error8

【C++】局部变量、全局变量、静态变量与动态对象的性质_动态局部对象-程序员宅基地

文章浏览阅读4.4k次,点赞17次,收藏46次。 【fishing-pan:https://blog.csdn.net/u013921430转载请注明出处】概述 局部变量 在一个函数内部定义的变量(包括函数形参)是局部变量。 全局变量 在函数外定义的变量是局部变量。 静态变量 静态全局变量 在全局变量..._动态局部对象

计算机怎么配置最好,怎么样才能把电脑的配置调到最高或者最好?-程序员宅基地

文章浏览阅读928次。1、加速网上邻居在Windows XP中访问网上邻居是相当恼人的,系统会搜索自己的共享目录和可作为网络共享的打印机以及计划任务中和网络相关的计划任务,然后才显示出来,显然这样速度就会比Windows 9x中慢很多。其实这些功能我们并没有使用上,与其不用还不如删除它们,这样速度就会明显加快。打开注册表编辑器,找到HKEY_LOCAL_ MACHINE/sofeware/Microsoft/Windo..._prefetchparameters

学习axios必知必会(1)~axios基本介绍、axios配置、json-server接口模拟工具_axios httpagent-程序员宅基地

文章浏览阅读5.4k次,点赞16次,收藏31次。学习axios必知必会(1)~axios基本介绍、axios配置、json-server接口模拟工具_axios httpagent

好故事-程序员宅基地

文章浏览阅读192次。我是一个老程序员,最近因为公司,因为家事,心情一直比较郁闷,偶尔翻翻,在网上看到了一个故事,内容写的就是一个毕业的学生如何打拼职场的,一开始感觉一般,但随着故事的推进,能体会到主人公的辛酸,更重要的是,发现了自己当年的影子,在这里推荐给大家,我每日发一篇,有可能会侵占版权,是吗,呵呵,但愿没有,随便吧,我想作者还应该感谢我呢。 公司杀手  一个刚毕业的大学生,对社会、对工作、对生活、对爱情充满了美好的向往,可没想到他却在跌跌撞撞中前行。他的职场之

推荐文章

热门文章

相关标签