18

根据Fred Chung 在 Android 开发者博客上对Dalvik中自定义类加载的介绍:

Dalvik VM 为开发人员提供了执行自定义类加载的工具。与从默认位置加载 Dalvik 可执行(“dex”)文件不同,应用程序可以从其他位置(例如内部存储或通过网络)加载它们。

但是,没有多少开发人员需要进行自定义类加载。但是那些按照该博客文章中的说明进行操作的人可能会在使用 Gradle 模仿相同行为时遇到一些问题,Gradle 是 Google I/O 2013 中引入的新的 Android 构建系统。

究竟如何调整新的构建系统以执行与旧的(基于 Ant 的)构建系统相同的中间步骤?

4

3 回答 3

27

我和我的团队最近在我们的应用程序中达到了 64K 方法引用,这是 dex 文件中支持的最大数量。为了绕过这个限制,我们需要将程序的一部分分割成多个二级dex文件,并在运行时加载它们。

我们按照问题中提到的旧的、基于 Ant 的构建系统的博客文章进行操作,一切正常。但我们最近觉得有必要迁移到基于 Gradle 的新构建系统。

此答案并不打算用完整的示例替换完整的博客文章。相反,它将简单地解释如何使用 Gradle 来调整构建过程并实现相同的目标。请注意,这可能只是其中一种方式,也是我们目前在团队中的方式。这并不一定意味着它是唯一的方法。

我们的项目结构略有不同,这个示例作为一个单独的 Java 项目工作,它将所有源代码编译成 .class 文件,将它们组装成一个 .dex 文件,最后,将该单个 .dex 文件打包成一个 .jar文件。

开始吧...

在根build.gradle中,我们有以下代码来定义一些默认值:

ext.androidSdkDir = System.env.ANDROID_HOME

if(androidSdkDir == null) {
    Properties localProps = new Properties()
    localProps.load(new FileInputStream(file('local.properties')))

    ext.androidSdkDir = localProps['sdk.dir']
}

ext.buildToolsVersion = '18.0.1'
ext.compileSdkVersion = 18

我们需要上面的代码,因为尽管该示例是一个单独的 Java 项目,但我们仍然需要使用 Android SDK 中的组件。稍后我们还将需要其他一些属性......所以,在主项目的build.gradle上,我们有这个依赖项:

dependencies {
    compile files("${androidSdkDir}/platforms/android-${compileSdkVersion}/android.jar")
}

我们还简化了这个项目的源代码集,这对于您的项目可能不是必需的:

sourceSets {
    main {
        java.srcDirs = ['src']
    }
}

接下来,我们将内置jar任务的默认配置更改为仅包含classes.dex文件而不是所有 .class 文件:

configure(jar) {
    include 'classes.dex'
}

现在我们需要有一个新任务,它将所有 .class 文件实际组装成一个 .dex 文件。在我们的例子中,我们还需要将 Protobuf 库 JAR 包含到 .dex 文件中。所以我在这里的例子中包括了它:

task dexClasses << {
    String protobufJarPath = ''

    String cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''

    configurations.compile.files.find {
        if(it.name.startsWith('protobuf-java')) {
            protobufJarPath = it.path
        }
    }

    exec {
        commandLine "${androidSdkDir}/build-tools/${buildToolsVersion}/dx${cmdExt}", '--dex',
                    "--output=${buildDir}/classes/main/classes.dex",
                    "${buildDir}/classes/main", "${protobufJarPath}"
    }
}

此外,请确保您在build.gradle文件的某处(当然通常在顶部)有以下导入:

import org.apache.tools.ant.taskdefs.condition.Os

现在我们必须使jar任务依赖于我们的dexClasses任务,以确保我们的任务在最终的 .jar 文件组装之前执行。我们用一行简单的代码来做到这一点:

jar.dependsOn(dexClasses)

我们完成了......只需使用通常的assemble任务调用 Gradle,您的最终 .jar 文件${buildDir}/libs/${archivesBaseName}.jar将包含一个classes.dex文件(除了 MANIFEST.MF 文件)。只需将其复制到您的应用程序资产文件夹中(您始终可以像我们所做的那样使用 Gradle 自动执行该操作,但这超出了此问题的范围)并按照博客文章的其余部分进行操作。

如果您有任何问题,请在评论中大声疾呼。我会尽我所能提供帮助。

于 2013-08-11T16:15:46.940 回答
2

Android Studio Gradle 插件现在提供原生multidex 支持,它有效地解决了 Android 65k 方法限制,而无需手动从 jar 文件加载类,因此让 Fred Chung 的博客为此目的而过时。但是,在 Android 运行时从 jar 文件加载自定义类对于可扩展性的目的仍然有用(例如,为您的应用程序制作插件框架),因此我将在下面解决该使用场景:

我已经使用 Android 库插件而不是 Java 插件,在 Fred Chung 的博客上将原始示例应用程序移植到我的github 页面上的 Android Studio。我没有像在博客中那样尝试修改现有的 dex 进程以拆分为两个模块,而是将我们想要进入 jar 文件的代码放入它自己的模块中,并添加了一个自定义任务assembleExternalJar来 dexes 必要的类主要assemble任务完成后的文件。

这是该库的 build.gradle 文件的相关部分。如果您的库模块有任何不在主项目中的依赖项,那么您可能需要修改此脚本以添加它们。

apply plugin: 'com.android.library'
// ... see github project for the full build.gradle file

// Define some tasks which are used in the build process
task copyClasses(type: Copy) { // Copy the assembled *.class files for only the current namespace into a new directory
    // get directory for current namespace (PLUGIN_NAMESPACE = 'com.example.toastlib')
    def namespacePath = PLUGIN_NAMESPACE.replaceAll("\\.","/")
    // set source and destination directories
    from "build/intermediates/classes/release/${namespacePath}/"
    into "build/intermediates/dex/${namespacePath}/"

    // exclude classes which don't have a corresponding .java entry in the source directory
    def remExt = { name -> name.lastIndexOf('.').with {it != -1 ? name[0..<it] : name} }
    eachFile {details ->
        def thisFile = new File("${projectDir}/src/main/java/${namespacePath}/", remExt(details.name)+".java")
        if (!(thisFile.exists())) {
            details.exclude()
        }
    }
}

task assembleExternalJar << {
    // Get the location of the Android SDK
    ext.androidSdkDir = System.env.ANDROID_HOME
    if(androidSdkDir == null) {
        Properties localProps = new Properties()
        localProps.load(new FileInputStream(file('local.properties')))
        ext.androidSdkDir = localProps['sdk.dir']
    }
    // Make sure no existing jar file exists as this will cause dx to fail
    new File("${buildDir}/intermediates/dex/${PLUGIN_NAMESPACE}.jar").delete();
    // Use command line dx utility to convert *.class files into classes.dex inside jar archive
    String cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
    exec {
        commandLine "${androidSdkDir}/build-tools/${BUILD_TOOLS_VERSION}/dx${cmdExt}", '--dex',
                    "--output=${buildDir}/intermediates/dex/${PLUGIN_NAMESPACE}.jar",
                    "${buildDir}/intermediates/dex/"
    }
    copyJarToOutputs.execute()
}

task copyJarToOutputs(type: Copy) {
    // Copy the built jar archive to the outputs folder
    from 'build/intermediates/dex/'
    into 'build/outputs/'
    include '*.jar'
}


// Set the dependencies of the build tasks so that assembleExternalJar does a complete build
copyClasses.dependsOn(assemble)
assembleExternalJar.dependsOn(copyClasses)

有关更多详细信息,请参阅我的 github 上示例应用程序的完整源代码。

于 2014-12-02T02:49:38.673 回答
0

在这里查看我的答案。关键点是:

  • 使用additionalParameters动态创建的dexCamelCase任务上的属性来传递--multi-dexdx创建多个dex 文件。
  • 使用multidex类加载器来使用多个 dex 文件。
于 2014-09-22T14:25:27.583 回答