Сборка apk из Android-приложения
от aNNiMON
Недавно я делал сборку apk в одном приложении, так что хочу рассказать, какие есть для этого решения.
Процесс сборки Android-приложения
Для начала о самом процессе сборки apk.
Когда вы запускаете сборку, первым делом читается AndroidManifest.xml, в нём есть важные параметры, такие как package (например, com.example.app) и targetSdkVersion.
Затем вызывается программа aapt (Android Asset Packaging Tool), которой передаётся AndroidManifest.xml, папка с ресурсами res/, assets/, путь к android.jar нужной target-версии. aapt проверяет ресурсы и компилирует их, создавая при этом класс R.java в котором содержатся идентификаторы ресурсов и файл resources.arsc в котором содержится информация об xml-ресурсах и их атрибутах.
Далее подхватываются все библиотеки, которые используются в проекте и запускается Java-компилятор javac. Полученные class-файлы передаются в программу dx, которая переводит их в Dalvik dex-формат. Причём для оптимизации, готовые библиотеки дексируются отдельно, а классы проекта отдельно (оптимизация в том, что дексированные библиотеки можно закешировать). Если собралось несколько dex-файлов, то они все объединяются при помощи Dex Merger Tool. В конечном итоге мы получаем единственный файл classes.dex (или несколько, если используется multidex).
Теперь у нас есть все компоненты и можно собирать apk. По сути это просто упаковка всех файлов в zip-архив, но используется специальная программа apkbuilder. После её выполнения получаем неподписанный apk-файл, то есть без папки META-INF внутри.
Последний этап - подпись apk. Берётся заранее сгенерированный ключ и передаётся в jarsigner вместе с неподписанным apk-файлом. На выходе имеем готовое приложение, которое можно устанавливать.
Картинка взята отсюда.
Сборка из Android-приложения
К счастью, Android достаточно мощная платформа, так что все вышеперечисленные приложения также портированы для неё.
aapt можно взять из этого репозитория. Там же другие собранные библиотеки.
Компилятор Java присутствует в ECJ.
Последнюю версию dx можно собрать из исходников в официальном репозитории.
Apkbuilder идёт как часть sdklib, но вместо него можно воспользоваться любой библиотекой для создания zip-архива.
Наконец, библиотека для подписи apk - zip-signer.
Следует заметить, что для полноценной сборки, нам понадобятся не только эти библиотеки, но ещё и android.jar последней версии. В итоге мы имеем 20 Мб зависимостей, что в прочем, не так уж и много. А тот же android.jar и aapt можно скачать при первом запуске, что значительно сэкономит место и трафик пользователям.
1. Запуск aapt в зависимости от архитектуры
Больше всего проблем с aapt, так как это не библиотека, а исполняемая программа. Нужно запускать совместимую версию, предварительно выставив права на запуск.
Аргументы программы можно посмотреть здесь.
2. Запуск компилятора ECJ
C ECJ всё проще, мы так же составляем аргументы командной строки, но теперь не создаём процесс, а передаём аргументы в метод compile класса org.eclipse.jdt.core.compiler.batch.BatchCompiler.
Аргументы можно посмотреть здесь.
3. Запуск dx
Дексирование - самый ресурсоёмкий процесс, поэтому важно оптимизировать его, заменив на объединение готовых dex-файлов при помощи DX Merger. Если есть возможность поставлять пользователю уже дексированные большие библиотеки, лучше это сделать, иначе процесс сборки будет долгим, а то и вовсе получим OutOfMemory.
Тем не менее, для запуска dx используются аргументы, передаваемые классу com.android.dx.command.dexer.Main.
Для объединения нескольких dex-файлов, запускаем DexMerger. Но так как он может объединять только два dex-файла, работу придётся проводить в цикле.
Объединение dex-файлов происходит значительно быстрее, чем дексирование.
Кстати, в dx.jar ещё много интересных инструментов, например поиск зависимостей, дампер и т.д.
4. Запуск ApkBuilder
ApkBuilder умеет подписывать приложение debug-ключом, если нужно собрать неподписанный apk, вместо debugKeyStoreFile можно передать null.
5. Запуск zip-signer
Подпись debug-ключом.
Подпись release-ключом (требуется библиотека bcprov).
Вот и всё, после выполнения всех этих шагов мы получим подписанный apk файл, который тут же можно и установить.
Часть кода была подсмотрена здесь.
Оптимизация
Если вам не нужно при сборке добавлять ресурсы в папку res/, так как они неизменны, то можно подумать об оптимизации - обойтись без aapt. Либо, если не требуется компилировать java-исходники, то можно вместо ECJ воспользоваться Jasmin, либо генерировать class-файлы при помощи библиотеки ASM или из smali-файлов ассемблировать классы, а то и вовсе брать готовые dex-файлы и просто объединять их при сборке.
В моём случае классы уже были готовы, поэтому я заранее дексировал их, а потом генерировал MainActivity с нужным package и создавал AndroidManifest.
Тело MainActivity у меня было постоянным, необходимо было менять только package. Поэтому основную часть класса я хранил в виде ресурса, а нужный package просто дописывал при сборке.
Путём нехитрых преобразований com.annimon.app.AutorunActivity превращается в com.test.app.MainActivity.
Гораздо интереснее было с AndroidManifest. При сборке его кодирует aapt, но ради одного манифеста добавлять 8 Мб зависимостей не хотелось, поэтому я воспользовался библиотекой xxml, которая может как декодировать бинарные ресурсы, так и создавать их динамически.
Идентификаторы взяты экспериментально путём обхода дерева на существующих AndroidManifest файлах.
Процесс сборки Android-приложения
Для начала о самом процессе сборки apk.
Когда вы запускаете сборку, первым делом читается AndroidManifest.xml, в нём есть важные параметры, такие как package (например, com.example.app) и targetSdkVersion.
Затем вызывается программа aapt (Android Asset Packaging Tool), которой передаётся AndroidManifest.xml, папка с ресурсами res/, assets/, путь к android.jar нужной target-версии. aapt проверяет ресурсы и компилирует их, создавая при этом класс R.java в котором содержатся идентификаторы ресурсов и файл resources.arsc в котором содержится информация об xml-ресурсах и их атрибутах.
Далее подхватываются все библиотеки, которые используются в проекте и запускается Java-компилятор javac. Полученные class-файлы передаются в программу dx, которая переводит их в Dalvik dex-формат. Причём для оптимизации, готовые библиотеки дексируются отдельно, а классы проекта отдельно (оптимизация в том, что дексированные библиотеки можно закешировать). Если собралось несколько dex-файлов, то они все объединяются при помощи Dex Merger Tool. В конечном итоге мы получаем единственный файл classes.dex (или несколько, если используется multidex).
Теперь у нас есть все компоненты и можно собирать apk. По сути это просто упаковка всех файлов в zip-архив, но используется специальная программа apkbuilder. После её выполнения получаем неподписанный apk-файл, то есть без папки META-INF внутри.
Последний этап - подпись apk. Берётся заранее сгенерированный ключ и передаётся в jarsigner вместе с неподписанным apk-файлом. На выходе имеем готовое приложение, которое можно устанавливать.
Картинка взята отсюда.
Сборка из Android-приложения
К счастью, Android достаточно мощная платформа, так что все вышеперечисленные приложения также портированы для неё.
aapt можно взять из этого репозитория. Там же другие собранные библиотеки.
Компилятор Java присутствует в ECJ.
Последнюю версию dx можно собрать из исходников в официальном репозитории.
Apkbuilder идёт как часть sdklib, но вместо него можно воспользоваться любой библиотекой для создания zip-архива.
Наконец, библиотека для подписи apk - zip-signer.
Следует заметить, что для полноценной сборки, нам понадобятся не только эти библиотеки, но ещё и android.jar последней версии. В итоге мы имеем 20 Мб зависимостей, что в прочем, не так уж и много. А тот же android.jar и aapt можно скачать при первом запуске, что значительно сэкономит место и трафик пользователям.
1. Запуск aapt в зависимости от архитектуры
Больше всего проблем с aapt, так как это не библиотека, а исполняемая программа. Нужно запускать совместимую версию, предварительно выставив права на запуск.
- private String getAaptBinaryName() {
- final String architecture = Build.CPU_ABI.substring(0, 3).toLowerCase(Locale.US);
- final boolean usePie = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
- final String pieSuffix = usePie ? "-pie" : "";
- switch (architecture) {
- case "mip": return "aapt-mips";
- case "x86": return "aapt-x86" + pieSuffix;
- case "arm":
- default: return "aapt-arm" + pieSuffix;
- }
- }
- // Распаковываем или качаем нужную версию во внутреннее хранилище
- final File aaptFile = new File(...);
- final FileOutputStream fos = new FileOutputStream(aaptFile);
- final InputStream is = context.getAssets().open(getAaptBinaryName());
- IOUtils.copy(is, fos);
- is.close();
- fos.close();
- // Устанавливаем права на запуск
- String[] args = {
- "chmod", "744", aaptFile.getAbsolutePath()
- };
- Runtime.getRuntime().exec(args).waitFor();
- // Запускаем aapt
- String[] args = {
- aaptFile.getAbsolutePath(), "p", "-f", "-m",
- "-S", resFolder,
- "-J", genFolder,
- "-A", assetsFolder,
- "-M", manifestFile,
- "-I", androidJarFile,
- "-F", resourcesArscFile
- };
- Runtime.getRuntime().exec(args).waitFor();
Аргументы программы можно посмотреть здесь.
2. Запуск компилятора ECJ
C ECJ всё проще, мы так же составляем аргументы командной строки, но теперь не создаём процесс, а передаём аргументы в метод compile класса org.eclipse.jdt.core.compiler.batch.BatchCompiler.
- String[] args = {
- "-extdirs", libsFolder,
- "-bootclasspath", androidJarFile,
- "-classpath", String.format("%s:%s:%s", srcFolder, genFolder, libsFolder),
- "-1.6", "-target", "1.6",
- "-d", binFolder,
- mainActivityFile
- }
- BatchCompiler.compile(args, new PrintWriter(System.out), new PrintWriter(System.err), null);
Аргументы можно посмотреть здесь.
3. Запуск dx
Дексирование - самый ресурсоёмкий процесс, поэтому важно оптимизировать его, заменив на объединение готовых dex-файлов при помощи DX Merger. Если есть возможность поставлять пользователю уже дексированные большие библиотеки, лучше это сделать, иначе процесс сборки будет долгим, а то и вовсе получим OutOfMemory.
Тем не менее, для запуска dx используются аргументы, передаваемые классу com.android.dx.command.dexer.Main.
- String[] args = {
- "--output=" + classesDexFile,
- classesFolder
- };
- Main.Arguments dxArgs = new Main.Arguments();
- dxArgs.parse(args);
- Main.run(dxArgs);
Для объединения нескольких dex-файлов, запускаем DexMerger. Но так как он может объединять только два dex-файла, работу придётся проводить в цикле.
- final List<String> dexs = getDexFiles();
- for (String currentDex : dexs) {
- DexMerger.main(new String[] {
- classesDexFile,
- currentDex,
- classesDexFile,
- });
- }
Объединение dex-файлов происходит значительно быстрее, чем дексирование.
Кстати, в dx.jar ещё много интересных инструментов, например поиск зависимостей, дампер и т.д.
4. Запуск ApkBuilder
- ApkBuilder builder = new ApkBuilder(
- apkOutputFile,
- resFolder,
- classesDexFile,
- debugKeyStoreFile,
- new PrintWriter(System.out)
- );
- builder.addSourceFolder(srcFolder);
- // builder.addNativeLibraries(..);
- // builder.addFile(..)
- builder.sealApk();
ApkBuilder умеет подписывать приложение debug-ключом, если нужно собрать неподписанный apk, вместо debugKeyStoreFile можно передать null.
5. Запуск zip-signer
Подпись debug-ключом.
- ZipSigner signer = new ZipSigner();
- signer.setKeymode("test");
- signer.signZip(unsignedApk, signedApk);
Подпись release-ключом (требуется библиотека bcprov).
- Security.addProvider(new BouncyCastleProvider());
- ZipSigner signer = new ZipSigner();
- CustomKeySigner.signZip(signer, keystore, keystorePassword, keyAlias, keyAliasPassword, "SHA1WITHRSA", unsignedApk, signedApk);
Вот и всё, после выполнения всех этих шагов мы получим подписанный apk файл, который тут же можно и установить.
Часть кода была подсмотрена здесь.
Оптимизация
Если вам не нужно при сборке добавлять ресурсы в папку res/, так как они неизменны, то можно подумать об оптимизации - обойтись без aapt. Либо, если не требуется компилировать java-исходники, то можно вместо ECJ воспользоваться Jasmin, либо генерировать class-файлы при помощи библиотеки ASM или из smali-файлов ассемблировать классы, а то и вовсе брать готовые dex-файлы и просто объединять их при сборке.
В моём случае классы уже были готовы, поэтому я заранее дексировал их, а потом генерировал MainActivity с нужным package и создавал AndroidManifest.
Тело MainActivity у меня было постоянным, необходимо было менять только package. Поэтому основную часть класса я хранил в виде ресурса, а нужный package просто дописывал при сборке.
- DataOutputStream dos = new DataOutputStream(
- new FileOutputStream(new File(packageDir, "MainActivity.class")));
- dos.writeInt(0xCAFEBABE);
- dos.writeInt(0x00000032);
- dos.write(new byte[] {0x00, (byte) 0xA6, 0x01});
- dos.writeUTF(String.format("%s/MainActivity", slashesPackage));
- copyResource("activity", dos);
- dos.flush();
- dos.close();
Путём нехитрых преобразований com.annimon.app.AutorunActivity превращается в com.test.app.MainActivity.
Гораздо интереснее было с AndroidManifest. При сборке его кодирует aapt, но ради одного манифеста добавлять 8 Мб зависимостей не хотелось, поэтому я воспользовался библиотекой xxml, которая может как декодировать бинарные ресурсы, так и создавать их динамически.
Открыть спойлер
Идентификаторы взяты экспериментально путём обхода дерева на существующих AndroidManifest файлах.