HermesHermes 引擎是 Facebook 研发,在 React-Native Android 端用于替换 JavaScript Core 的 JavaScript 引擎。Hermes 引擎的优势是适合移动端的轻量级 JavaScript 引擎,使用 aot 编译,可以减少 Android 端内存使用,减小安装包大小,提升执行效率。
相关文章:干货 | 加载速度提升15%,携程对RN新一代JS引擎Hermes的调研
首先,请确保您至少使用 React Native 的 0.60.4 版本。
集成 Hermes
官网的集成方式很简单,只需要在android/app/build.gradle
中将enableHermes
改成 true,但基本上是不会成功的。下面是正确的开启方式 ~
- 第一步 编辑
android/app/build.gradle
文件并进行如下所示的更改:
project.ext.react = [
entryFile: "index.js",
enableHermes: true, // clean and rebuild if changing
hermesCommand: "../../node_modules/hermes-engine/%OS-BIN%/hermes",
]
-
第二步
npm install --save hermes-engine@0.1.0
,安装 0.1.0 版本的 hermes -
第三步 替换 node_modules\react-native 目录下 react.gradle 文件
// Copyright (c) Facebook, Inc. and its affiliates.
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
import org.apache.tools.ant.taskdefs.condition.Os
def config = project.hasProperty("react") ? project.react : [];
def cliPath = config.cliPath ?: "node_modules/react-native/cli.js"
def composeSourceMapsPath = config.composeSourceMapsPath ?: "node_modules/react-native/scripts/compose-source-maps.js"
def bundleAssetName = config.bundleAssetName ?: "index.android.bundle"
def entryFile = config.entryFile ?: "index.android.js"
def bundleCommand = config.bundleCommand ?: "bundle"
def reactRoot = file(config.root ?: "../../")
def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"]
def bundleConfig = config.bundleConfig ? "${reactRoot}/${config.bundleConfig}" : null ;
def enableVmCleanup = config.enableVmCleanup == null ? true : config.enableVmCleanup
def hermesCommand = config.hermesCommand ?: "../../node_modules/hermes-engine/%OS-BIN%/hermes"
def reactNativeDevServerPort() {
def value = project.getProperties().get("reactNativeDevServerPort")
return value != null ? value : "8081"
}
def reactNativeInspectorProxyPort() {
def value = project.getProperties().get("reactNativeInspectorProxyPort")
return value != null ? value : reactNativeDevServerPort()
}
def getHermesOSBin() {
if (Os.isFamily(Os.FAMILY_WINDOWS)) return "win64-bin";
if (Os.isFamily(Os.FAMILY_MAC)) return "osx-bin";
if (Os.isOs(null, "linux", "amd64", null)) return "linux64-bin";
throw new Exception("OS not recognized. Please set project.ext.react.hermesCommand " +
"to the path of a working Hermes compiler.");
}
// Make sure not to inspect the Hermes config unless we need it,
// to avoid breaking any JSC-only setups.
def getHermesCommand = {
// If the project specifies a Hermes command, don't second guess it.
if (!hermesCommand.contains("%OS-BIN%")) {
return hermesCommand
}
// Execution on Windows fails with / as separator
return hermesCommand
.replaceAll("%OS-BIN%", getHermesOSBin())
.replace('/' as char, File.separatorChar);
}
// Set enableHermesForVariant to a function to configure per variant,
// or set `enableHermes` to True/False to set all of them
def enableHermesForVariant = config.enableHermesForVariant ?: {
def variant -> config.enableHermes ?: false
}
android {
buildTypes.all {
resValue "integer", "react_native_dev_server_port", reactNativeDevServerPort()
resValue "integer", "react_native_inspector_proxy_port", reactNativeInspectorProxyPort()
}
}
afterEvaluate {
def isAndroidLibrary = plugins.hasPlugin("com.android.library")
def variants = isAndroidLibrary ? android.libraryVariants : android.applicationVariants
variants.all { def variant ->
// Create variant and target names
def targetName = variant.name.capitalize()
def targetPath = variant.dirName
// React js bundle directories
def jsBundleDir = file("$buildDir/generated/assets/react/${targetPath}")
def resourcesDir = file("$buildDir/generated/res/react/${targetPath}")
def jsBundleFile = file("$jsBundleDir/$bundleAssetName")
def jsSourceMapsDir = file("$buildDir/generated/sourcemaps/react/${targetPath}")
def jsIntermediateSourceMapsDir = file("$buildDir/intermediates/sourcemaps/react/${targetPath}")
def jsPackagerSourceMapFile = file("$jsIntermediateSourceMapsDir/${bundleAssetName}.packager.map")
def jsCompilerSourceMapFile = file("$jsIntermediateSourceMapsDir/${bundleAssetName}.compiler.map")
def jsOutputSourceMapFile = file("$jsSourceMapsDir/${bundleAssetName}.map")
// Additional node and packager commandline arguments
def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"]
def extraPackagerArgs = config.extraPackagerArgs ?: []
def enableHermes = enableHermesForVariant(variant)
def currentBundleTask = tasks.create(
name: "bundle${targetName}JsAndAssets",
type: Exec) {
group = "react"
description = "bundle JS and assets for ${targetName}."
// Create dirs if they are not there (e.g. the "clean" task just ran)
doFirst {
jsBundleDir.deleteDir()
jsBundleDir.mkdirs()
resourcesDir.deleteDir()
resourcesDir.mkdirs()
jsIntermediateSourceMapsDir.deleteDir()
jsIntermediateSourceMapsDir.mkdirs()
jsSourceMapsDir.deleteDir()
jsSourceMapsDir.mkdirs()
}
// Set up inputs and outputs so gradle can cache the result
inputs.files fileTree(dir: reactRoot, excludes: inputExcludes)
outputs.dir(jsBundleDir)
outputs.dir(resourcesDir)
// Set up the call to the react-native cli
workingDir(reactRoot)
// Set up dev mode
def devEnabled = !(config."devDisabledIn${targetName}"
|| targetName.toLowerCase().contains("release"))
def extraArgs = extraPackagerArgs;
if (bundleConfig) {
extraArgs = extraArgs.clone()
extraArgs.add("--config");
extraArgs.add(bundleConfig);
}
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", *nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
"--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
} else {
commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
"--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
}
if (enableHermes) {
doLast {
def hermesFlags;
def hbcTempFile = file("${jsBundleFile}.hbc")
exec {
if (targetName.toLowerCase().contains("release")) {
// Can't use ?: since that will also substitute valid empty lists
hermesFlags = config.hermesFlagsRelease
if (hermesFlags == null) hermesFlags = ["-O", "-output-source-map"]
} else {
hermesFlags = config.hermesFlagsDebug
if (hermesFlags == null) hermesFlags = []
}
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", getHermesCommand(), "-emit-binary", "-out", hbcTempFile, jsBundleFile, *hermesFlags)
} else {
commandLine(getHermesCommand(), "-emit-binary", "-out", hbcTempFile, jsBundleFile, *hermesFlags)
}
}
ant.move(
file: hbcTempFile,
toFile: jsBundleFile
);
if (hermesFlags.contains("-output-source-map")) {
ant.move(
// Hermes will generate a source map with this exact name
file: "${jsBundleFile}.hbc.map",
tofile: jsCompilerSourceMapFile
);
exec {
// TODO: set task dependencies for caching
// Set up the call to the compose-source-maps script
workingDir(reactRoot)
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", *nodeExecutableAndArgs, composeSourceMapsPath, jsPackagerSourceMapFile, jsCompilerSourceMapFile, "-o", jsOutputSourceMapFile)
} else {
commandLine(*nodeExecutableAndArgs, composeSourceMapsPath, jsPackagerSourceMapFile, jsCompilerSourceMapFile, "-o", jsOutputSourceMapFile)
}
}
}
}
}
enabled config."bundleIn${targetName}" != null
? config."bundleIn${targetName}"
: config."bundleIn${variant.buildType.name.capitalize()}" != null
? config."bundleIn${variant.buildType.name.capitalize()}"
: targetName.toLowerCase().contains("release")
}
// Expose a minimal interface on the application variant and the task itself:
variant.ext.bundleJsAndAssets = currentBundleTask
currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
// registerGeneratedResFolders for Android plugin 3.x
if (variant.respondsTo("registerGeneratedResFolders")) {
variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
} else {
variant.registerResGeneratingTask(currentBundleTask)
}
variant.mergeResourcesProvider.get().dependsOn(currentBundleTask)
// packageApplication for Android plugin 3.x
def packageTask = variant.hasProperty("packageApplication")
? variant.packageApplicationProvider.get()
: tasks.findByName("package${targetName}")
if (variant.hasProperty("packageLibrary")) {
packageTask = variant.packageLibrary
}
// pre bundle build task for Android plugin 3.2+
def buildPreBundleTask = tasks.findByName("build${targetName}PreBundle")
def resourcesDirConfigValue = config."resourcesDir${targetName}"
if (resourcesDirConfigValue) {
def currentCopyResTask = tasks.create(
name: "copy${targetName}BundledResources",
type: Copy) {
group = "react"
description = "copy bundled resources into custom location for ${targetName}."
from(resourcesDir)
into(file(resourcesDirConfigValue))
dependsOn(currentBundleTask)
enabled(currentBundleTask.enabled)
}
packageTask.dependsOn(currentCopyResTask)
if (buildPreBundleTask != null) {
buildPreBundleTask.dependsOn(currentCopyResTask)
}
}
def currentAssetsCopyTask = tasks.create(
name: "copy${targetName}BundledJs",
type: Copy) {
group = "react"
description = "copy bundled JS into ${targetName}."
if (config."jsBundleDir${targetName}") {
from(jsBundleDir)
into(file(config."jsBundleDir${targetName}"))
} else {
into ("$buildDir/intermediates")
into ("assets/${targetPath}") {
from(jsBundleDir)
}
// Workaround for Android Gradle Plugin 3.2+ new asset directory
into ("merged_assets/${variant.name}/merge${targetName}Assets/out") {
from(jsBundleDir)
}
// Workaround for Android Gradle Plugin 3.4+ new asset directory
into ("merged_assets/${variant.name}/out") {
from(jsBundleDir)
}
}
// mergeAssets must run first, as it clears the intermediates directory
dependsOn(variant.mergeAssetsProvider.get())
enabled(currentBundleTask.enabled)
}
packageTask.dependsOn(currentAssetsCopyTask)
if (buildPreBundleTask != null) {
buildPreBundleTask.dependsOn(currentAssetsCopyTask)
}
// Delete the VM related libraries that this build doesn't need.
// The application can manage this manually by setting 'enableVmCleanup: false'
//
// This should really be done by packaging all Hermes releated libs into
// two separate HermesDebug and HermesRelease AARs, but until then we'll
// kludge it by deleting the .so files out of the /transforms/ directory.
def isRelease = targetName.toLowerCase().contains("release")
def libDir = "$buildDir/intermediates/transforms/"
def vmSelectionAction = {
fileTree(libDir).matching {
if (enableHermes) {
// For Hermes, delete all the libjsc* files
include "**/libjsc*.so"
if (isRelease) {
// Reduce size by deleting the debugger/inspector
include '**/libhermes-inspector.so'
include '**/libhermes-executor-debug.so'
} else {
// Release libs take precedence and must be removed
// to allow debugging
include '**/libhermes-executor-release.so'
}
} else {
// For JSC, delete all the libhermes* files
include "**/libhermes*.so"
}
}.visit { details ->
def targetVariant = ".*/transforms/[^/]*/${targetPath}/.*"
def path = details.file.getAbsolutePath().replace(File.separatorChar, '/' as char)
if (path.matches(targetVariant) && details.file.isFile()) {
details.file.delete()
}
}
}
if (enableVmCleanup) {
def task = tasks.findByName("package${targetName}")
task.doFirst(vmSelectionAction)
}
}
}
其实这一步是将 rn 默认调用的 hermesvm 改成 herms-engine,我们看下对比
- 第四步 遵循官方提示,如果有修改 enableHermes,一定要在 Android 下执行./gradlew clean 之后重新编译
修改之后基本可以运行打包了,如果遇到
More than one file was found with OS independent path 'lib/x86/libc++_shared.so'
这样的错,意思是有多个lib/x86/libc++_shared.so
依赖,可以在 android/app/build.gradle 里的android{}
配置项里,添加
packagingOptions {
pickFirst '**/*.so'
}
在编译时遇到相同的依赖,选择第一个。
或者直接声明
packagingOptions {
pickFirst '**/x86/libc++_shared.so'
}
开启 Hermes 后打出来包明显比没开启时小很多。不仅仅是 apk 体积变小了,而且启动速度也有明显提升,占用内存也明显下降。关于性能调研的对比可以参考这篇文章RN 技术探索:Hermes Engine 初探
开启前:
开启后:
使用 Google Chrome 的 DevTools 调试 Hermes
Hermes 通过实现 Chrome 检查器协议来支持 Chrome 调试器。这意味着 Chrome 的工具可用于直接调试在模拟器或设备上的 Hermes 上运行的 JavaScript。
Chrome 通过 Metro 连接到在设备上运行的爱马仕,因此您需要知道 Metro 在哪里监听。通常,此功能将打开localhost:8081
,但这是可配置的。运行时yarn start
,启动时将地址写入 stdout。
知道 Metro 服务器正在侦听的位置后,您可以按照以下步骤连接 Chrome:
-
chrome://inspect
在 Chrome 浏览器实例中导航到。 -
使用
Configure...
按钮添加 Metro 服务器地址(通常localhost:8081
如上所述)。
3.现在,您应该看到带有“检查”链接的“ Hermes React Native”目标,可用于启动调试器。如果看不到“检查”链接,请确保 Metro 服务器正在运行。
4.现在,您可以使用 Chrome 调试工具。例如,要在下次运行某些 JavaScript 时断点,请单击“暂停”按钮,然后在您的应用中触发一个将导致 JavaScript 执行的操作。