Загрузка пользовательского класса в Dalvik с помощью Gradle (Android New Build System)

В соответствии с введением Custom Class Loading в Dalvik от Fred Chung в блоге разработчиков Android:

Dalvik VM предоставляет средства для разработчиков для класс загрузки. Вместо загрузки исполняемых файлов Dalvik ( "dex" ) из местоположение по умолчанию, приложение может загружать их из альтернативных таких как внутреннее хранилище или сеть.

Однако не многим разработчикам необходимо выполнять загрузку пользовательского класса. Но те, кто выполняет и следуют инструкциям на этом блоге, могут столкнуться с некоторыми проблемами, имитирующими то же поведение с помощью Gradle, новой системы сборки для Android, представленной в Google I/O 2013.

Как точно можно адаптировать новую систему сборки для выполнения тех же промежуточных шагов, что и в старой (Ant) системе сборки?

Ответ 1

Моя команда и я недавно достигли ссылок на методы 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. В нашем случае нам также необходимо включить JAR протокола Protobuf в файл .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)

И мы закончили... Просто запустите Gradle с обычной задачей assemble, а ваш окончательный файл .jar, ${buildDir}/libs/${archivesBaseName}.jar будет содержать один файл classes.dex(кроме файла MANIFEST.MF). Просто скопируйте это в свою папку с вашими приложениями (вы всегда можете автоматизировать это с помощью Gradle, как мы это сделали, но это выходит за рамки этого вопроса) и следуйте за остальной частью сообщения в блоге.

Если у вас есть вопросы, просто кричите в комментариях. Я постараюсь помочь в моих силах.

Ответ 2

Плагин Android Studio Gradle теперь предоставляет встроенную поддержку multidex, которая эффективно решает метод метода Android 65k без необходимости вручную загружать классы из файла jar, и, таким образом, блог Фреда Чунга устарел для этой цели. Однако загрузка пользовательских классов из файла jar во время выполнения в Android по-прежнему полезна для расширения (например, создания плагинов для вашего приложения), поэтому я буду адресуйте этот сценарий использования ниже:

Я создал порт оригинального примера приложения в блоге Fred Chung для Android Studio на моей странице github здесь с помощью библиотеки Android плагин, а не плагин Java. Вместо того, чтобы пытаться изменить существующий процесс dex, чтобы разделить на два модуля, например, в блоге, я поместил код, который мы хотим переместить в файл jar, в свой собственный модуль и добавил пользовательскую задачу assembleExternalJar, которая dexes необходимые файлы классов после завершения основной задачи assemble.

Вот важная часть файла build.gradle для библиотеки. Если ваш библиотечный модуль имеет какие-либо зависимости, которые не входят в основной проект, вам, вероятно, потребуется изменить этот script, чтобы добавить их.

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.

Ответ 3

Смотрите мой ответ здесь. Ключевыми моментами являются:

  • Используйте свойство additionalParameters для динамически созданных задач dexCamelCase для передачи --multi-dex в dx и создания нескольких файлов dex.
  • Используйте multidex загрузчик классов, чтобы использовать несколько файлов dex.