ReactNative 0.60.4 Android 开启 Hermes

本贴最后更新于 1776 天前,其中的信息可能已经渤澥桑田

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,我们看下对比
image.png

image.png

  • 第四步 遵循官方提示,如果有修改 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 初探
开启前:
image.png
image.png
image.png

开启后:
image.png
image.png
image.png

使用 Google Chrome 的 DevTools 调试 Hermes

Hermes 通过实现 Chrome 检查器协议来支持 Chrome 调试器。这意味着 Chrome 的工具可用于直接调试在模拟器或设备上的 Hermes 上运行的 JavaScript。

Chrome 通过 Metro 连接到在设备上运行的爱马仕,因此您需要知道 Metro 在哪里监听。通常,此功能将打开 localhost:8081,但这是可配置的。运行时 yarn start,启动时将地址写入 stdout。

知道 Metro 服务器正在侦听的位置后,您可以按照以下步骤连接 Chrome:

  1. chrome://inspect 在 Chrome 浏览器实例中导航到。

  2. 使用 Configure... 按钮添加 Metro 服务器地址(通常 localhost:8081 如上所述)。
    Chrome DevTools 设备页面中的“配置”按钮
    添加 Chrome DevTools 网络目标的对话框

3.现在,您应该看到带有“检查”链接的“ Hermes React Native”目标,可用于启动调试器。如果看不到“检查”链接,请确保 Metro 服务器正在运行。
目标检查链接
4.现在,您可以使用 Chrome 调试工具。例如,要在下次运行某些 JavaScript 时断点,请单击“暂停”按钮,然后在您的应用中触发一个将导致 JavaScript 执行的操作。

调试工具中的“暂停”按钮

  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 417 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Postman

    Postman 是一款简单好用的 HTTP API 调试工具。

    4 引用 • 3 回帖
  • Hexo

    Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。

    21 引用 • 140 回帖 • 6 关注
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    20 引用 • 23 回帖 • 720 关注
  • 锤子科技

    锤子科技(Smartisan)成立于 2012 年 5 月,是一家制造移动互联网终端设备的公司,公司的使命是用完美主义的工匠精神,打造用户体验一流的数码消费类产品(智能手机为主),改善人们的生活质量。

    4 引用 • 31 回帖 • 3 关注
  • CAP

    CAP 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。

    11 引用 • 5 回帖 • 600 关注
  • 导航

    各种网址链接、内容导航。

    38 引用 • 169 回帖
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 50 关注
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 630 关注
  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    123 引用 • 168 回帖
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 389 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 232 回帖 • 1 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 417 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 393 关注
  • frp

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 TCP、UDP、 HTTP 和 HTTPS 协议。

    19 引用 • 7 回帖 • 1 关注
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    22 引用 • 70 回帖
  • BND

    BND(Baidu Netdisk Downloader)是一款图形界面的百度网盘不限速下载器,支持 Windows、Linux 和 Mac,详细介绍请看这里

    107 引用 • 1281 回帖 • 38 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 295 回帖
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 50 关注
  • Hprose

    Hprose 是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。你无需专门学习,只需看上几眼,就能用它轻松构建分布式应用系统。

    9 引用 • 17 回帖 • 614 关注
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    490 引用 • 914 回帖
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 147 关注
  • IBM

    IBM(国际商业机器公司)或万国商业机器公司,简称 IBM(International Business Machines Corporation),总公司在纽约州阿蒙克市。1911 年托马斯·沃森创立于美国,是全球最大的信息技术和业务解决方案公司,拥有全球雇员 30 多万人,业务遍及 160 多个国家和地区。

    17 引用 • 53 回帖 • 130 关注
  • Jenkins

    Jenkins 是一套开源的持续集成工具。它提供了非常丰富的插件,让构建、部署、自动化集成项目变得简单易用。

    52 引用 • 37 回帖 • 1 关注
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 1 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 268 回帖 • 85 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    676 引用 • 535 回帖 • 1 关注