Android R8 耗时优化

介绍

终于我们全源码编译的 Release 包已经超过了40分钟了,感觉离提桶跑路不远了。

使用 --scan 分析了编译过程,发现 minifyReleaseWithR8 任务耗时近14分钟。

流程分析

从网上搜一下,基本上没有什么卵用,都是一些配置。

从官网捞一点介绍,有一个 r8 runInSeparateProcess, 说有一点作用,测试了下。

具体可以参照下 AGP8介绍。项目中实验了下,参考如下配置,在 settings.gradle 中配置一下,仅在 CI 服务器上生效。

gradle 复制代码
// settings.gradle

// r8 编译新特性
if (System.getenv().containsKey("CI_SERVER") && System.getenv("R8_PROFILE") == "1") {
    apply from: "settings-ci-r8.gradle"
}
gradle 复制代码
// settings-ci-r8.gradle
apply plugin: 'com.android.settings'
android {
    execution {
        profiles {
            dev {
                r8.runInSeparateProcess false
            }
            server {
                r8 {
                    jvmOptions += ["-Xms50g", "-Xmx200g", "-XX:+HeapDumpOnOutOfMemoryError"]
                    runInSeparateProcess true
                }
            }
        }

        // ci
        if (System.getenv().containsKey("CI_SERVER")) {
            defaultProfile "server"
        } else {
            defaultProfile "dev"
        }
    }
}

logger.quiet("android.execution.defaultProfile = ${android.execution.defaultProfile}")

最后尝试了下,并没有什么效果,只是 gradle 的进程内存占用少了点, GC 时间数据也好点,编译耗时基本没有效果。内存占用少主要是因为 r8 任务是单独的进程执行的,所以内存占用会比串行执行要少一点。

翻了翻 agp 源码,发现 r8 以下逻辑的。

kotlin 复制代码
// 该源码有删减,只是为了展示关键逻辑
@CacheableTask
@BuildAnalyzer(primaryTaskCategory = TaskCategory.OPTIMIZATION)
abstract class R8Task @Inject constructor(
    projectLayout: ProjectLayout
): ProguardConfigurableTask(projectLayout) {


    override fun doTaskAction() {
        if (executionOptions.get().runInSeparateProcess) {
            workerExecutor.processIsolation { spec ->
                spec.forkOptions { forkOptions ->
                    forkOptions.jvmArgs(executionOptions.get().jvmArgs)
                }
            }.submit(R8Runnable::class.java, workerAction)
        } else {
            workerExecutor.noIsolation().submit(R8Runnable::class.java, workerAction)
        }
    }

    companion object {
        fun shrink(
            bootClasspath: List<File>,
            minSdkVersion: Int,
            isDebuggable: Boolean,
            enableDesugaring: Boolean,
            disableTreeShaking: Boolean,
            disableMinification: Boolean,
            mainDexListFiles: List<File>,
            mainDexRulesFiles: List<File>,
            mainDexListOutput: File?,
            legacyMultiDexEnabled: Boolean,
            useFullR8: Boolean,
            referencedInputs: List<File>,
            classes: List<File>,
            resourcesJar: File,
            proguardConfigurationFiles: Collection<File>,
            inputProguardMapping: File?,
            proguardConfigurations: MutableList<String>,
            isAar: Boolean,
            mappingFile: File,
            proguardSeedsOutput: File,
            proguardUsageOutput: File,
            proguardConfigurationOutput: File,
            missingKeepRulesOutput: File,
            output: File,
            outputResources: File,
            featureClassJars: List<File>,
            featureJavaResourceJars: List<File>,
            featureDexDir: File?,
            featureJavaResourceOutputDir: File?,
            libConfiguration: String?,
            errorFormatMode: SyncOptions.ErrorFormatMode,
            inputArtProfile: File?,
            outputArtProfile: File?,
            enableDexStartupOptimization: Boolean,
            inputProfileForDexStartupOptimization: File?,
        ) {
           

            // When invoking R8 we filter out missing files. E.g. javac output may not exist if
            // there are no Java sources. See b/151605314 for details.
            runR8(
                filterMissingFiles(classes, logger),
                output.toPath(),
                resourcesJar.toPath(),
                outputResources.toPath(),
                bootClasspath.map { it.toPath() },
                filterMissingFiles(referencedInputs, logger),
                toolConfig,
                proguardConfig,
                mainDexListConfig,
                MessageReceiverImpl(errorFormatMode, Logging.getLogger(R8Runnable::class.java)),
                useFullR8,
                featureClassJars.map { it.toPath() },
                featureJavaResourceJars.map { it.toPath() },
                featureDexDir?.toPath(),
                featureJavaResourceOutputDir?.toPath(),
                libConfiguration,
                inputArtProfile?.toPath(),
                outputArtProfile?.toPath(),
                enableDexStartupOptimization,
                inputProfileForDexStartupOptimization?.toPath(),
            )
        }
    }

    abstract class R8Runnable : WorkAction<R8Runnable.Params> {

        abstract class Params : WorkParameters {
            abstract val bootClasspath: ConfigurableFileCollection
            abstract val minSdkVersion: Property<Int>
            abstract val debuggable: Property<Boolean>
            abstract val disableTreeShaking: Property<Boolean>
            abstract val enableDesugaring: Property<Boolean>
            abstract val disableMinification: Property<Boolean>
            abstract val mainDexListFiles: ConfigurableFileCollection
            abstract val mainDexRulesFiles: ConfigurableFileCollection
            abstract val mainDexListOutput: RegularFileProperty
            abstract val legacyMultiDexEnabled: Property<Boolean>
            abstract val useFullR8: Property<Boolean>
            abstract val referencedInputs: ConfigurableFileCollection
            abstract val classes: ConfigurableFileCollection
            abstract val resourcesJar: RegularFileProperty
            abstract val proguardConfigurationFiles: ConfigurableFileCollection
            abstract val inputProguardMapping: RegularFileProperty
            abstract val proguardConfigurations: ListProperty<String>
            abstract val aar: Property<Boolean>
            abstract val mappingFile: RegularFileProperty
            abstract val proguardSeedsOutput: RegularFileProperty
            abstract val proguardUsageOutput: RegularFileProperty
            abstract val proguardConfigurationOutput: RegularFileProperty
            abstract val missingKeepRulesOutput: RegularFileProperty
            abstract val output: RegularFileProperty
            abstract val outputResources: RegularFileProperty
            abstract val featureClassJars: ConfigurableFileCollection
            abstract val featureJavaResourceJars: ConfigurableFileCollection
            abstract val featureDexDir: DirectoryProperty
            abstract val featureJavaResourceOutputDir: DirectoryProperty
            abstract val libConfiguration: Property<String>
            abstract val errorFormatMode: Property<SyncOptions.ErrorFormatMode>
            abstract val inputArtProfile: RegularFileProperty
            abstract val outputArtProfile: RegularFileProperty
            abstract val enableDexStartupOptimization: Property<Boolean>
            abstract val inputProfileForDexStartupOptimization: RegularFileProperty
        }

        override fun execute() {
            shrink(
                parameters.bootClasspath.files.toList(),
                parameters.minSdkVersion.get(),
                parameters.debuggable.get(),
                parameters.enableDesugaring.get(),
                parameters.disableTreeShaking.get(),
                parameters.disableMinification.get(),
                parameters.mainDexListFiles.files.toList(),
                parameters.mainDexRulesFiles.files.toList(),
                parameters.mainDexListOutput.orNull?.asFile,
                parameters.legacyMultiDexEnabled.get(),
                parameters.useFullR8.get(),
                parameters.referencedInputs.files.toList(),
                parameters.classes.files.toList(),
                parameters.resourcesJar.asFile.get(),
                parameters.proguardConfigurationFiles.files.toList(),
                parameters.inputProguardMapping.orNull?.asFile,
                parameters.proguardConfigurations.get(),
                parameters.aar.get(),
                parameters.mappingFile.get().asFile,
                parameters.proguardSeedsOutput.get().asFile,
                parameters.proguardUsageOutput.get().asFile,
                parameters.proguardConfigurationOutput.get().asFile,
                parameters.missingKeepRulesOutput.get().asFile,
                parameters.output.get().asFile,
                parameters.outputResources.get().asFile,
                parameters.featureClassJars.files.toList(),
                parameters.featureJavaResourceJars.files.toList(),
                parameters.featureDexDir.orNull?.asFile,
                parameters.featureJavaResourceOutputDir.orNull?.asFile,
                parameters.libConfiguration.orNull,
                parameters.errorFormatMode.get(),
                parameters.inputArtProfile.orNull?.asFile,
                parameters.outputArtProfile.orNull?.asFile,
                parameters.enableDexStartupOptimization.get(),
                parameters.inputProfileForDexStartupOptimization.orNull?.asFile,
            )
        }
    }
}

通过上述代码,发现 通过参数 runInSeparateProcess,然后丢给了 workerExecutor,然后执行 submit 提交任务,一个是单独的processIsolation,一个是直接运行。

csharp 复制代码
if (executionOptions.get().runInSeparateProcess) {
    workerExecutor.processIsolation { spec ->
        spec.forkOptions { forkOptions ->
            forkOptions.jvmArgs(executionOptions.get().jvmArgs)
        }
    }.submit(R8Runnable::class.java, workerAction)
} else {
    workerExecutor.noIsolation().submit(R8Runnable::class.java, workerAction)
}

不管是直接运行,还是单独的进程运行,最后都是调用shrink 方法, 方法中最后执行 runR8 方法。捞了一下 runR8,在 r8Tool.kt 中。

kotlin 复制代码
fun runR8(
    inputClasses: Collection<Path>,
    output: Path,
    inputJavaResJar: Path,
    javaResourcesJar: Path,
    libraries: Collection<Path>,
    classpath: Collection<Path>,
    toolConfig: ToolConfig,
    proguardConfig: ProguardConfig,
    mainDexListConfig: MainDexListConfig,
    messageReceiver: MessageReceiver,
    useFullR8: Boolean = false,
    featureClassJars: Collection<Path>,
    featureJavaResourceJars: Collection<Path>,
    featureDexDir: Path?,
    featureJavaResourceOutputDir: Path?,
    libConfiguration: String? = null,
    inputArtProfile: Path? = null,
    outputArtProfile: Path? = null,
    enableMinimalStartupOptimization: Boolean = false,
    inputProfileForDexStartupOptimization: Path? = null,
) {
    val logger: Logger = Logger.getLogger("R8")
    if (logger.isLoggable(Level.FINE)) {
        logger.fine("*** Using R8 to process code ***")
        logger.fine("Main dex list config: $mainDexListConfig")
        logger.fine("Proguard config: $proguardConfig")
        logger.fine("Tool config: $toolConfig")
        logger.fine("Program classes: $inputClasses")
        logger.fine("Java resources: $inputJavaResJar")
        logger.fine("Library classes: $libraries")
        logger.fine("Classpath classes: $classpath")
    }
    val r8CommandBuilder =
        R8Command.builder(
            R8DiagnosticsHandler(
                proguardConfig.proguardOutputFiles.missingKeepRules,
                messageReceiver,
                "R8"
            )
        )

    if (!useFullR8) {
        r8CommandBuilder.setProguardCompatibility(true);
    }

    if (toolConfig.r8OutputType == R8OutputType.DEX) {
        r8CommandBuilder.minApiLevel = toolConfig.minSdkVersion
        if (toolConfig.minSdkVersion < 21) {
            // specify main dex related options only when minSdkVersion is below 21
            r8CommandBuilder
                .addMainDexRulesFiles(mainDexListConfig.mainDexRulesFiles)
                .addMainDexListFiles(mainDexListConfig.mainDexListFiles)

            if (mainDexListConfig.mainDexRules.isNotEmpty()) {
                r8CommandBuilder.addMainDexRules(mainDexListConfig.mainDexRules, Origin.unknown())
            }
            mainDexListConfig.mainDexListOutput?.let {
                r8CommandBuilder.setMainDexListConsumer(StringConsumer.FileConsumer(it))
            }
        }
        if (libConfiguration != null) {
            r8CommandBuilder.addSpecialLibraryConfiguration(libConfiguration)
        }
        if (toolConfig.isDebuggable) {
            r8CommandBuilder.addAssertionsConfiguration(
                AssertionsConfiguration.Builder::compileTimeEnableAllAssertions
            )
        }
    }

    r8CommandBuilder
        .addProguardConfigurationFiles(
            proguardConfig.proguardConfigurationFiles.filter { Files.isRegularFile(it) }
        )
        .addProguardConfiguration(proguardConfig.proguardConfigurations, Origin.unknown())

    if (proguardConfig.proguardMapInput != null
        && Files.exists(proguardConfig.proguardMapInput)
    ) {
        r8CommandBuilder.addProguardConfiguration(
            listOf("-applymapping \"${proguardConfig.proguardMapInput}\""),
            Origin.unknown()
        )
    }

    val proguardOutputFiles = proguardConfig.proguardOutputFiles
    Files.deleteIfExists(proguardOutputFiles.proguardMapOutput)
    Files.deleteIfExists(proguardOutputFiles.proguardSeedsOutput)
    Files.deleteIfExists(proguardOutputFiles.proguardUsageOutput)
    Files.deleteIfExists(proguardOutputFiles.proguardConfigurationOutput)
    Files.deleteIfExists(proguardOutputFiles.missingKeepRules)

    Files.createDirectories(proguardOutputFiles.proguardMapOutput.parent)
    r8CommandBuilder.setProguardMapOutputPath(proguardOutputFiles.proguardMapOutput)
    r8CommandBuilder.setProguardSeedsConsumer(
        StringConsumer.FileConsumer(proguardOutputFiles.proguardSeedsOutput)
    )
    r8CommandBuilder.setProguardUsageConsumer(
        StringConsumer.FileConsumer(proguardOutputFiles.proguardUsageOutput)
    )
    r8CommandBuilder.setProguardConfigurationConsumer(
        StringConsumer.FileConsumer(
            proguardOutputFiles.proguardConfigurationOutput
        )
    )

    val compilationMode =
        if (toolConfig.isDebuggable) CompilationMode.DEBUG else CompilationMode.RELEASE

    val dataResourceConsumer = JavaResourcesConsumer(javaResourcesJar)
    val programConsumer =
        if (toolConfig.r8OutputType == R8OutputType.CLASSES) {
            val baseConsumer: ClassFileConsumer = if (Files.isDirectory(output)) {
                ClassFileConsumer.DirectoryConsumer(output)
            } else {
                ClassFileConsumer.ArchiveConsumer(output)
            }
            object : ClassFileConsumer.ForwardingConsumer(baseConsumer) {
                override fun getDataResourceConsumer(): DataResourceConsumer? {
                    return dataResourceConsumer
                }
            }
        } else {
            val baseConsumer: DexIndexedConsumer = if (Files.isDirectory(output)) {
                DexIndexedConsumer.DirectoryConsumer(output)
            } else {
                DexIndexedConsumer.ArchiveConsumer(output)
            }
            object : DexIndexedConsumer.ForwardingConsumer(baseConsumer) {
                override fun getDataResourceConsumer(): DataResourceConsumer? {
                    return dataResourceConsumer
                }
            }
        }

    @Suppress("UsePropertyAccessSyntax")
    r8CommandBuilder
        .setDisableMinification(toolConfig.disableMinification)
        .setDisableTreeShaking(toolConfig.disableTreeShaking)
        .setDisableDesugaring(toolConfig.disableDesugaring)
        .setMode(compilationMode)
        .setProgramConsumer(programConsumer)

    // Use this to control all resources provided to R8
    val r8ProgramResourceProvider = R8ProgramResourceProvider()

    for (path in inputClasses) {
        when {
            Files.isRegularFile(path) -> r8ProgramResourceProvider.addProgramResourceProvider(
                ArchiveProgramResourceProvider.fromArchive(path)
            )

            Files.isDirectory(path) -> Files.walk(path).use { stream ->
                stream.filter {
                    val relativePath = path.relativize(it).toString()
                    Files.isRegularFile(it) && ClassFileInput.CLASS_MATCHER.test(relativePath)
                }
                    .forEach { r8CommandBuilder.addProgramFiles(it) }
            }

            else -> throw IOException("Unexpected file format: $path")
        }
    }

    r8ProgramResourceProvider.dataResourceProviders.add(
        ResourceOnlyProvider(
            ArchiveResourceProvider.fromArchive(inputJavaResJar, true)
        ).dataResourceProvider
    )

    r8CommandBuilder.addProgramResourceProvider(r8ProgramResourceProvider)

    val featureClassJarMap =
        featureClassJars.associateBy({ it.toFile().nameWithoutExtension }, { it })
    val featureJavaResourceJarMap =
        featureJavaResourceJars.associateBy({ it.toFile().nameWithoutExtension }, { it })
    // Check that each feature class jar has a corresponding feature java resources jar, and vice
    // versa.
    check(
        featureClassJarMap.keys.containsAll(featureJavaResourceJarMap.keys)
                && featureJavaResourceJarMap.keys.containsAll(featureClassJarMap.keys)
    ) {
        """
            featureClassJarMap and featureJavaResourceJarMap must have the same keys.

            featureClassJarMap keys:
            ${featureClassJarMap.keys.sorted()}

            featureJavaResourceJarMap keys:
            ${featureJavaResourceJarMap.keys.sorted()}
            """.trimIndent()
    }
    if (featureClassJarMap.isNotEmpty()) {
        check(featureDexDir != null && featureJavaResourceOutputDir != null) {
            "featureDexDir == null || featureJavaResourceOutputDir == null."
        }
        Files.createDirectories(featureJavaResourceOutputDir)
        check(toolConfig.r8OutputType == R8OutputType.DEX) {
            "toolConfig.r8OutputType != R8OutputType.DEX."
        }
        for (featureKey in featureClassJarMap.keys) {
            r8CommandBuilder.addFeatureSplit {
                it.addProgramResourceProvider(
                    ArchiveProgramResourceProvider.fromArchive(featureClassJarMap[featureKey])
                )
                it.addProgramResourceProvider(
                    ArchiveResourceProvider.fromArchive(featureJavaResourceJarMap[featureKey], true)
                )
                val javaResConsumer = JavaResourcesConsumer(
                    featureJavaResourceOutputDir.resolve("$featureKey$DOT_JAR")
                )
                it.setProgramConsumer(
                    object : DexIndexedConsumer.DirectoryConsumer(
                        Files.createDirectories(featureDexDir.resolve(featureKey))
                    ) {
                        override fun getDataResourceConsumer(): DataResourceConsumer {
                            return javaResConsumer
                        }
                    }
                )
                return@addFeatureSplit it.build()
            }
        }
    }
    // handle art-profile rewriting if enabled
    inputArtProfile?.let {input ->
        if (input.exists() && outputArtProfile != null) {
            wireArtProfileRewriting(r8CommandBuilder, input, outputArtProfile)
        }
    }
    if (enableMinimalStartupOptimization) {
        check(inputProfileForDexStartupOptimization != null) {
            """
        'android.experimental.r8.dex-startup-optimization' flag has been turned on but there are no
        baseline profile source file in this project. Remove the flag or add a source file.
        """.trimIndent()
        }
        wireMinimalStartupOptimization(r8CommandBuilder, inputProfileForDexStartupOptimization)
    }

    // Enable workarounds for missing library APIs in R8 (see b/231547906).
    r8CommandBuilder.setEnableExperimentalMissingLibraryApiModeling(true);
    ClassFileProviderFactory(libraries).use { libraryClasses ->
        ClassFileProviderFactory(classpath).use { classpathClasses ->
            r8CommandBuilder.addLibraryResourceProvider(libraryClasses.orderedProvider)
            r8CommandBuilder.addClasspathResourceProvider(classpathClasses.orderedProvider)
            R8.run(r8CommandBuilder.build())
        }
    }

    proguardConfig.proguardOutputFiles.proguardMapOutput.let {
        if (Files.notExists(it)) {
            // R8 might not create a mapping file, so we have to create it, http://b/37053758.
            Files.createFile(it)
        }
    }
}

大致的意思是,一些输入文件和执行配置,以及输出文件。在执行前,需要一些校验。如果输出文件存在则先删除。然后根据配置构建R8命令并执行 R8.run(r8CommandBuilder.build())

这里的 R8.run 中的方法就是单独的 r8.jar 文件了,r8 是一个字节码混淆压缩工具,可以参考 r8

现在需要优化的点就在这里,可以考虑以下几种方案:

  • 增加硬件配置,可以有效提高编译速度,但不太实际,硬件不可能无限增加
  • 使用缓存,考虑到文件变更过于频繁,缓存的意义不大
  • 给 Google 提供一些建议,让 R8 执行更快,感觉也不太现实
  • 尝试升级 agp 或者 r8 版本,目前也试过,效果不明显
  • 使用 c++ 或者 rust 重写一套 r8,需要投入更多精力,不太实际
  • 优化r8,提高执行效率

我这边选择的是最后一条,从后端同学那边得到的思路,可以使用GraalVM,把jar文件编译成二进制文件,可以有效提高运行速度。

r8 优化

参数传递

刚刚上面提到过,可以单独运行一个进程,那么直接把jar文件变成二进制文件,然后执行二进制文件也不是不行,关键是参数怎么传递。

分析上面的运行流程,发现主要的参数是文件列表和一些运行配置,翻了翻源码,发现都是 data class,这些这些好办了,直接把这些参数放在一个大的结构体中, 然后序列化成一个json字符串,最后写入文件中,传入给二进制的参数只有一个文件路径,然后执行二进制文件,读取文件内容,反序列化,最后再执行原先的逻辑,相当于一个简单的 rpc 过程。 有一些参数类型的是Path,这个需要单独处理写,直接转成字符串即可。

kotlin 复制代码
class PathSerializer : JsonSerializer<Path> {
    override fun serialize(path: Path, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
        return context.serialize(path.toString())
    }
}

class PathDeserializer : JsonDeserializer<Path> {
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext?): Path {
        return Paths.get(json.asString)
    }
}

class R8Args(
    var inputClasses: Collection<Path>,
    var output: Path,
    var inputJavaResJar: Path,
    var javaResourcesJar: Path,
    var libraries: Collection<Path>,
    var classpath: Collection<Path>,
    var toolConfig: ToolConfig,
    var proguardConfig: ProguardConfig,
    var mainDexListConfig: MainDexListConfig,
    var useFullR8: Boolean = false,
    var featureClassJars: Collection<Path>,
    var featureJavaResourceJars: Collection<Path>,
    var featureDexDir: Path?,
    var featureJavaResourceOutputDir: Path?,
    var libConfiguration: String? = null,
    var inputArtProfile: Path? = null,
    var outputArtProfile: Path? = null,
    var enableMinimalStartupOptimization: Boolean = false,
    var inputProfileForDexStartupOptimization: Path? = null,
) {
    // var messageReceiver: MessageReceiver? = null
}

R8 获取

可以从官网的获取的 r8的二进制,r8 maven,不过下载下来后,jar中的代码是混淆,不能直接使用。

那么只能从官网下载源码,然后自己编译成一个没有混淆的 jar 文件。

官网给的编译方法是这样的,不过需要安装 depot_tools,配置好 depot_tools 即可。

ruby 复制代码
// depot_tools
// https://www.chromium.org/developers/how-tos/install-depot-tools/

$ git clone https://r8.googlesource.com/r8
$ cd r8
$ tools/gradle.py r8

然后把编译后的 r8.jar 复制到项目中。

二进制生成

创建一个普通Java项目,然后配置好 build.gradle 文件,以下是我这边的配置。

scss 复制代码
plugins {
    id("java")
    id("application")
    kotlin("jvm") version "1.9.23"
    id("org.graalvm.buildtools.native") version "0.10.3"
}

version = "0.0.1"

application {
    mainClass.set("com.xxxx.r8opt.Main")
}

dependencies {
    // 刚刚编译好的r8.jar
    implementation(files("libs/r8.jar"))
    implementation("com.google.code.gson:gson:2.8.9")
    implementation("com.google.guava:guava:31.0.1-jre")

    testImplementation(kotlin("test"))
}

kotlin {
    jvmToolchain(17)
}

graalvmNative {
    binaries {
        named("main") {
            imageName.set("r8opt")
            mainClass.set("com.xxxx.r8opt.Main")
            buildArgs.add("--gc=G1")
            buildArgs.addAll("-J-Xmx250g", "-J-Xms150g")
            buildArgs.addAll("--enable-all-security-services", "--no-fallback", "-Dgraalvm.native.threads=64")

            jvmArgs.add("--add-exports=jdk.internal.vm.compiler/org.graalvm.compiler.options=ALL-UNNAMED")
            jvmArgs.add("--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.option=ALL-UNNAMED")
        }
        named("test") {
            buildArgs.add("-O0")
        }
    }
    binaries.all {
        buildArgs.add("--verbose")
    }
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<JavaExec> {
    jvmArgs("--add-opens", "java.base/java.util=ALL-UNNAMED")
    jvmArgs("--add-exports=jdk.internal.vm.compiler/org.graalvm.compiler.options=ALL-UNNAMED")
}

tasks.withType<Test> {
    jvmArgs("--add-opens", "java.base/java.util=ALL-UNNAMED")
}

简单介绍写 的 Main 文件,就是反序列化参数,然后执行原来的逻辑。把AGP中的 runR8方法逻辑全部复制过来,最后再调用 R8.run方法中,执行原来的逻辑。

kotlin 复制代码
fun main(args: Array<String>) {
    if (args.isNotEmpty()) {
        val file = File(args[0])
        if (!file.exists()) {
            System.err.println("args file = $file is not exists")
            exitProcess(1)
        }
        runCatching {
            val r8Args = parseArgs(file)
            r8opt(r8Args)
        }.onFailure {
            println("run r8opt fail, e = ${it.message}")
            it.printStackTrace()
        }.getOrThrow()
    } else {
        System.err.println("no file provided")
        exitProcess(1)
    }
}

fun r8opt(args: R8Args) {
    runR8(
        args.inputClasses,
        args.output,
        args.inputJavaResJar,
        args.javaResourcesJar,
        args.libraries,
        args.classpath,
        args.toolConfig,
        args.proguardConfig,
        args.mainDexListConfig,
        args.useFullR8,
        args.featureClassJars,
        args.featureJavaResourceJars,
        args.featureDexDir,
        args.featureJavaResourceOutputDir,
        args.libConfiguration,
        args.inputArtProfile,
        args.outputArtProfile,
        args.enableMinimalStartupOptimization,
        args.inputProfileForDexStartupOptimization,
    )
}

关于GraalVM的配置,有一个需要注意的点,生成的二进制文件不能包含反射,如果有反射,需要提前配置,否则运行会报错。

src/main/resources/META-INF/native-image/reflect-config.json 文件中添加配置,因为我这边用的是 gson,所以序列化的时候需要注意。

json 复制代码
[
  {
    "name": "com.xxx.r8opt.R8Args",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.builder.dexing.ToolConfig",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.builder.dexing.ProguardConfig",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.builder.dexing.MainDexListConfig",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.builder.dexing.ProguardOutputFiles",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.builder.dexing.R8OutputType",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.tools.r8.threading.providers.blocking.ThreadingModuleBlockingProvider",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true,
    "unsafeAllocated": true
  },
  {
    "name": "com.android.tools.r8.threading.providers.singlethreaded.ThreadingModuleSingleThreadedProvider",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true,
    "unsafeAllocated": true
  }
]

最后在 linux 平台下,执行 ./gradlew clean && ./gradlew :r8opt:nativeCompile 生成二进制文件。这个二级制文件是和平台相关的,本地是mac,所以需要再在 linux 平台下生成,才能使用。

最后 hook AGP 中的 runR8方法,然后执行这个二进制文件即可。

kotlin 复制代码
clazz.getDeclaredMethod("runR8").aopIntercept(object : MethodInterceptCallback {
    override fun invoke(self: Any, method: String, args: List<Any?>): InterceptResult {
        val watch = Stopwatch.createStarted()
        val optimize = extension.checkR8opt()
        logger.quiet("runR8 begin, optimize = $optimize")
        if (!optimize) {
            logger.quiet("skip runR8 hook")
            return InterceptResult(false)
        }
        val ret = kotlin.runCatching {
            R8optRunner(p, extension).runR8opt(
                self,
                args[0] as Collection<Path>,
                args[1] as Path,
                args[2] as Path,
                args[3] as Path,
                args[4] as Collection<Path>,
                args[5] as Collection<Path>,
                args[6] as ToolConfig,
                args[7] as ProguardConfig,
                args[8] as MainDexListConfig,
                args[9] as MessageReceiver,
                args[10] as Boolean,
                args[11] as Collection<Path>,
                args[12] as Collection<Path>,
                args[13] as Path?,
                args[14] as Path?,
                args[15] as String?,
                args[16] as Path?,
                args[17] as Path?,
                args[18] as Boolean,
                args[19] as Path?,
            )
        }.onFailure {
            logger.error("runR8 hook run error, e = ${it.message}")
        }.onFinally {
            logger.quiet("runR8 complete, ret = ${it.isSuccess}, duration = ${watch.stop()}")
        }

        return InterceptResult(ret.isSuccess)
    }
})

总结

打完收工,发现通过二进制运行,速度有一定的提升,优化前 13m40s, 优化后 7m45s,提升 40% 左右。优化后能在40分钟内完成。 目前正在灰度使用,有问题再更新,如果你有更优秀的方式,欢迎交流。

参考

r8

r8 maven

AGP8

GraalVM Gradle

相关推荐
purrrew24 分钟前
【Java ee初阶】jvm(3)
java·jvm
不爱吃饭爱吃菜26 分钟前
uniapp微信小程序一键授权登录
前端·javascript·vue.js·微信小程序·uni-app
90后小陈老师1 小时前
3D个人简历网站 5.天空、鸟、飞机
前端·javascript·3d
Hello World......2 小时前
互联网大厂Java面试:从Spring到微服务的全面探讨
java·spring boot·spring cloud·微服务·面试·技术栈·互联网大厂
拾贰_C2 小时前
【SpringBoot】MyBatisPlus(MP | 分页查询操作
java·spring boot·后端·spring·maven·apache·intellij-idea
猛踹瘸子那条好腿の2 小时前
Spring-boot初次使用
java·springboot
我不是程序猿儿5 小时前
【C#】 lock 关键字
java·开发语言·c#
不爱吃糖的程序媛5 小时前
浅谈前端架构设计与工程化
前端·前端架构设计
tmacfrank6 小时前
网络编程中的直接内存与零拷贝
java·linux·网络
weixin_472339466 小时前
Maven 下载安装与配置教程
java·maven