Загрузка пользовательских классов в Dalvik с Gradle (новая система сборки Android)

В соответствии с введением пользовательской загрузки классов в Dalvik Фредом Чангом в блоге разработчиков Android:

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

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

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

3 ответа

Решение

Моя команда и я недавно достигли ссылок на метод 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, как мы это делали, но это выходит за рамки этого вопроса) и следуйте остальной части поста в блоге.

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

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

Я создал порт исходного примера приложения в блоге Фреда Чанга для Android Studio на моей странице github, используя плагин библиотеки Android, а не плагин Java. Вместо того, чтобы пытаться изменить существующий процесс dex, чтобы разделить его на два модуля, как в блоге, я поместил код, который мы хотим поместить в файл jar, в его собственный модуль, и добавил пользовательскую задачу assembleExternalJar который восстанавливает необходимые файлы классов после основного 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.

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

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