- PVSM.RU - https://www.pvsm.ru -
В гольфе выигрывает тот, у кого меньше очков.
Применим этот принцип в Android. Мы собираемся поиграть в APK-гольф и создать приложение минимально возможного размера, которое можно установить на Android 8.0 Oreo.
Начнём с дефолтного приложения, который генерирует Android Studio. Создадим хранилище ключей [1], подпишем приложение и измерим размер файла в байтах командой stat -f%z $filename.
Затем установим APK на смартфон Nexus 5x под Oreo, чтобы убедиться, что всё работает.

Прекрасно. Наш APK весит примерно полтора мегабайта.
Полтора мегабайта кажутся слишком большим размером с учётом того, что делает наше приложение (а оно ничего не делает), так что давайте изучим проект и поищем, где по-быстрому сэкономить на объёме. Вот что сгенерировал Android Studio:
MainActivity, который расширяет AppCompatActivity.ConstraintLayout для главного окна.AppCompat и ConstraintLayout.AndroidManifest.xml.
Пожалуй, проще всего разобраться с иконками, учитывая, что там в общей сложности 15 изображений и два XML-файла под mipmap-anydpi-v26. Давайте посчитаем всё это в APK Analyser [2] из Android Studio.

Вопреки нашим первоначальным предположениям, похоже, что самый большой файл — Dex, а на ресурсы приходится всего 20% от размера APK.
| Файл | Размер |
|---|---|
classes.dex |
74% |
res |
20% |
resources.arsc |
4% |
META-INF |
2% |
AndroidManifest.xml |
<1% |
Исследуем по отдельности, что делает каждый файл.
classes.dex — главный виновник раздутого APK, он занимает 73% всего объёма и поэтому станет первой целью оптимизации. Этот файл содержит весь наш скомпилированный код в формате Dex, а также список внешних методов во фреймворке Android и библиотеку поддержки.
В пакете android.support перечисляется более 13 000 методов, что кажется излишним для приложения типа "Hello World".
В директории res находится большое количество файлов шаблонов, чертежей и анимаций, которые сразу не видны в интерфейсе Android Studio. Опять же, они вытянуты из библиотеки поддержки и занимают около 20% размера APK.

Файл resources.arsc также содержит список всех этих ресурсов.
В папке META-INF находятся файлы CERT.SF, MANIFEST.MF и CERT.RSA, которые нужны для подписи v1 APK [3]. Если злоумышленник изменит код внутри APK, то подписи не совпадут, что защищает пользователя от запуска постороннего зловреда.
В MANIFEST.MF перечисляются файлы из APK, а CERT.SF содержит контрольные суммы манифеста и каждого отдельного файла. В CERT.RSA хранится открытый ключ, которым проверяется цельность CERT.SF.

Здесь нет очевидных целей для оптимизации.
AndroidManifest очень похож на наш оригинальный файл. Единственное отличие — вместо ресурсов вроде строк и чертежей здесь указаны их целочисленные идентификаторы, начиная с 0x7F.
Мы ещё не пробовали включить опцию минификации и сжатия ресурсов в файле build.gradle для нашего приложения. Сделаем это.
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(
'proguard-android.txt'), 'proguard-rules.pro'
}
}
}
-keep class com.fractalwrench.** { *; }
Если установить minifyEnabled в значение true, то активируется Proguard [4], который очищает приложение от ненужного кода. А также обфусцирует имена символов, затрудняя обратную разработку приложения.
shrinkResources удалит из APK любые ресурсы, на которые нет прямой ссылки. Могут возникнуть проблемы, если вы получаете доступ к ресурсам не напрямую, но к нашему приложению это не относится.
Мы наполовину уменьшили размер APK без видимого изменения в работе программы.

Если вы ещё не включили minifyEnabled и shrinkResources в своём приложении, это самая главная вещь, которую следует вынести из этой статьи. Можно легко сэкономить несколько мегабайт, потратив всего парочку часов на конфигурацию и тестирование.
classes.dex теперь занимает 57% всего APK. Основная часть списка методов из файла Dex принадлежит пакету android.support, так что мы собираемся удалить библиотеку поддержки. Для этого нужно сделать следующее:
build.gradle.
dependencies {
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}
android.app.Activity.
public class MainActivity extends Activity
TextView.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Hello World!" />
styles.xml и аттрибут android:theme из элемента <application> в AndroidManifest.colors.xml.Матерь божья, файл уменьшился почти в десять раз: с 786 КБ до 108 КБ. Единственным заметным изменением стало только изменение цвета тулбара, который окрасился в дефолтную тему ОС.

На директорию res теперь приходится 95% размера APK из-за всех этих иконок лаунчера. Если бы эти иконки делал наш дизайнер, мы бы попытались конвертировать их в WebP [5], более эффективный формат, который поддерживается в API 15 и более поздних версиях.
К счастью, Google уже оптимизировала наши чертежи, хотя в противном случае мы бы и сами могли оптимизировать их и удалить из PNG ненужные метаданные с помощью ImageOptim [6].
Давайте поступим нешаблонно — и заменим все наши иконки запуска единственной однопиксельной чёрной точкой в папке res/drawable. Эта картинка весит 67 байт.
Мы избавились почти от всех ресурсов, так что неудивительно, что размер APK уменьшился примерно на 95%. В файле resources.arsc по-прежнему упоминаются следующие ресурсы:
Пойдём сверху вниз.
Фреймворк Android раздувает наш файл XML [7] и автоматически создаёт объект TextView, который используется как contentView для Activity.
Попробуем обойтись без этого посредника, удалив файл XML и программно задав contentView. Объём ресурсов уменьшится, потому что исчезнет файл XML, но увеличится размер файла Dex, поскольку мы упоминаем там дополнительные методы TextView.
TextView textView = new TextView(this);
textView.setText("Hello World!");
setContentView(textView);
Выглядит как неплохой обмен.
Давайте удалим strings.xml и заменим android:label в манифесте AndroidManifest буквой "A". Это кажется маленьким изменением, но удаление записи из resources.arsc уменьшает количество символов в манифесте и удаляет файл из директории res. Каждая мелочь идёт на пользу — мы только что сэкономили 228 байт.
Документация для resources.arsc [8] в репозитории Android Platform объясняет, что каждый ресурс APK упоминается в resources.arsc с целочисленным идентификатором. У этих ID два пространства имён:
0x01: системные ресурсы (предустановленные в framework-res.apk)
0x7f: ресурсы приложения (в файле .apk приложения)
Так что произойдёт с нашим APK, если мы поставил ссылку на ресурс в пространстве имён 0x01? По идее, мы получим более красивую иконку и одновременно уменьшим размер своего файла.
android:icon="@android:drawable/btn_star"

Само собой, вам никогда не следует доверять системным ресурсам вроде иконок в реальном рабочем приложении. Такой метод провалит валидацию в Google Play, а некоторые производители ещё и по-своему определяют белый цвет [9], так что действуйте осторожно.
Мы ещё не трогали манифест.
android:allowBackup="true"
android:supportsRtl="true"
Удаление этих аттрибутов экономит 48 байт.
Похоже, что классы BuildConfig и R ещё остались в файле Dex.
-keep class com.fractalwrench.MainActivity { *; }
Уточнение правила Proguard удалит ненужные классы.
Обфусцируем имя для класса Activity. Для обычных классов Proguard автоматически делает это, но поскольку имя класса Activity вызывается через Intents, его не обфусцировали по умолчанию.
MainActivity -> c.java
com.fractalwrench.apkgolf -> c.c
В данный момент мы подписываем приложение одновременно подписями v1 и v2. Это кажется лишней тратой ресурсов, потому что v2 обеспечивает превосходную защиту и производительность [10], хешируя весь APK целиком.
Подпись v2 не видна из APK Analyser, поскольку включена в бинарный блок в самом файле APK. Подпись v1 видна, в виде файлов CERT.RSA и CERT.SF.
Давайте уберём галочку для подписи v1 в интерфейсе Android Studio и сгенерируем подписанный APK. Попробуем сделать и наоборот.
| Подпись | Размер |
|---|---|
| v1 | 3511 |
| v2 | 3307 |
Похоже, теперь мы будем использовать v2.
Пришло время редактировать APK вручную. Используем следующие команды:
# 1. Создать неподписанный apk
./gradlew assembleRelease
# 2. Разархивировать архив
unzip app-release-unsigned.apk -d app
# Сделать необходимые правки
# 3. Заархивировать архив
zip -r app app.zip
# 4. Запустить zipalign
zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk
# 5. Запустить apksigner с подписью v2
apksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk
# 6. Проверить подпись
apksigner verify signed-release.apk
Детальный обзор процесса подписи APK см. здесь [11]. В общем, Gradle генерирует неподписанный архив, zipalign делает выравнивание по границе байта для несжатых ресурсов, чтобы оптимизировать потребление RAM после загрузки APK, и в конце запускается криптографическая процедура подписи APK.
Неподписанный и невыровненный APK весит 1902 байт, то есть процедура добавляет примерно 1 килобайт.
Странно! Если разархивировать невыровненный APK и подписать его вручную, то пропадает файл META-INF/MANIFEST.MF, что экономит 543 байта. Если кто-то знает, почему так происходит, то дайте знать!
Теперь у нас в подписанном APK осталось три файла. Но ведь мы можем ещё избавиться от файла resources.arsc, потому что не устанавливаем никаких ресурсов!
После этого остаётся только манифест и файл classes.dex, оба примерно одинакового размера.
Теперь изменим все оставшиеся строки на ‘c’, обновив версии до 26, а затем сгенерируем подписанный APK.
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "c.c"
minSdkVersion 26
targetSdkVersion 26
versionCode 26
versionName "26"
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="c.c">
<application
android:icon="@android:drawable/btn_star"
android:label="c"
>
<activity android:name="c.c.c">
Это уменьшает размер ещё на 9 байт.
Хотя количество символов в файле не изменилось, но дело в том, что увеличилась частотность символа ‘c’. В результате алгоритм сжатия сработал более эффективно.
Можно ещё сильнее оптимизировать манифест, удалив фильтр намерения Launch для класса Activity. С этого момента будем запускать приложение следующей командой:
adb shell am start -a android.intent.action.MAIN -n c.c/.c
Вот новый манифест:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="c.c">
<application>
<activity
android:name="c"
android:exported="true" />
</application>
</manifest>
Мы также избавились от иконки лаунчера.
По изначальным условиям, мы должны подготовить APK, который способен установиться на устройство.
Наше приложение перечисляет методы в классах TextView, Bundle и Activity. Можно уменьшить размер файла Dex, удалив эти ссылки и заменив их новым классом Application. Таким образом, файл Dex теперь будет ссылаться на единственный метод — конструктор класса Application.
Исходные файлы теперь выглядят следующим образом:
package c.c;
import android.app.Application;
public class c extends Application {}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="c.c">
<application android:name=".c" />
</manifest>
Используем adb для проверки, что APK успешно установился, это можно также проверить через «Настройки».

Я потратил несколько часов, изучая формат файла Dex ради этой оптимизации, поскольку разные механизмы вроде контрольных сумм и смещений затрудняют ручное редактирование.
Если вкратце, в итоге выяснилось, что единственным требованием для установки APK является факт существования файла classes.dex. Поэтому мы просто удалим оригинальный файл, запустим touch classes.dex в консоли и сэкономим 10% от размера, используя пустой файл.
Иногда глупейшее решение — самое лучшее.
Манифест неподписанного APK — это файл в бинарном формате XML, который вроде бы официально не документирован. Можно изменить содержимое файла с помощью редактора HexFiend [12].
В заголовке файла угадываются некоторые интересные элементы — первые четыре байта кодируют 38, что совпадает с номером версии файла Dex. Следующие два байта кодируют 660, что совпадает с размером файла.
Попробуем удалить один байт, установив targetSdkVersion на 1, и изменив размер файла в заголовке на 659. К сожалению, система Android отвергает новый файл как неправильный APK. Похоже, тут всё устроено как-то посложнее…
А попробуем набросать случайных символов по всему файлу, а затем установить APK, не изменяя указанный размер файла. Так мы проверим, осуществляется ли проверка контрольной суммы, и как наши изменения повлияют на смещения в заголовке файла.
Удивительно, но такой манифест воспринят как валидный APK на Nexus 5X под Oreo:

Мне кажется, я только что услышал, как разработчик фреймворка Android, ответственный за поддержку BinaryXMLParser.java, очень громко закричал в подушку.
Для максимальной выгоды нужно заменить все эти глупые символы нулевыми байтами. Это поможет распознать важные части файла в HexFiend, а также сократит несколько байт благодаря хаку сжатия, упомянутому выше.
Вот важные компоненты Manifest, без которых APK не установится.

Некоторые вещи очевидны, такие как теги манифеста и пакета. В пуле строк видны versionCode и название пакета.

Просмотр файла в шестнадцатиричном виде показывает значения в заголовке файла, которые описывают пул строк и другие значения, вроде размера файла 0x9402. Строки тоже интересно закодированы — если они больше 8 байт, то общая длина указывается в двух предыдущих байтах.
Но вряд ли здесь можно найти другие варианты для оптимизации.
Изучим окончательный APK.

В течение всего этого имени в APK было указано моё имя в подписи v2. Создадим новое хранилище ключей, в котором используется хак для сжатия.

Мы сэкономили 20 байт.
1757 байт — это очень мало, чёрт возьми. И насколько я знаю, это самый маленький существующий APK.
Однако я разумно полагаю, что кто-нибудь из Android-сообщества способен выполнить дальнейшие оптимизации и ещё улучшить результат. Если вы умудритесь уменьшить файл с нынешних 1757 байт, присылайте пулл-реквест в репозиторий [13], где самый маленький APK, или сообщайте в твиттере [15]. (С момента публикации статьи файл уже уменьшили до 820 байт — прим. пер.)
Автор: m1rko
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/otladka/265508
Ссылки в тексте:
[1] Создадим хранилище ключей: https://developer.android.com/studio/publish/app-signing.html#generate-key
[2] APK Analyser: https://developer.android.com/studio/build/apk-analyzer.html
[3] подписи v1 APK: https://source.android.com/security/apksigning/v2#v1-verification
[4] Proguard: https://www.guardsquare.com/en/proguard
[5] конвертировать их в WebP: https://developer.android.com/studio/write/convert-webp.html
[6] ImageOptim: https://imageoptim.com/mac
[7] раздувает наш файл XML: https://developer.android.com/reference/android/view/LayoutInflater.html
[8] Документация для resources.arsc: https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/README
[9] по-своему определяют белый цвет: https://www.reddit.com/r/androiddev/comments/71fpru/android_color_resources_not_safe/
[10] превосходную защиту и производительность: https://source.android.com/security/apksigning/#apk-signing-schemes
[11] здесь: https://developer.android.com/studio/publish/app-signing.html#sign-manually
[12] HexFiend: https://github.com/ridiculousfish/HexFiend
[13] присылайте пулл-реквест в репозиторий: https://github.com/fractalwrench/ApkGolf
[14] хостится: https://www.reg.ru/?rlink=reflink-717
[15] сообщайте в твиттере: https://twitter.com/fractalwrench
[16] Источник: https://habrahabr.ru/post/339938/
Нажмите здесь для печати.