Оптимизация сборок Android приложений: ProGuard, D8, R8. Тайны обфускации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Оптимизация сборки — вишенка на торте мобильного приложения. К счастью, существуют инструменты, проверенные временем и заслужившие доверие сообщества. К сожалению, ее не всегда воспринимают всерьез и не уделяют ей должного внимания. Почему в оптимизации должны быть заинтересованы все? Как выжать максимум из мобильного приложения? Как работают инструменты, которыми мы привыкли пользоваться в паре строк? И что нам продают под словом «обфускация»?

О статье

Статья рассчитана на ознакомление с темой оптимизации сборок Android приложений. В статье:

  • Постановка проблемы. Обозначим общие требования к сборкам мобильных приложений;

  • Оптимизация сборки. Поговорим о том, как приложения собираются и как этот процесс можно оптимизировать;

  • ProGuard. Познакомимся с ProGuard и тем, как он оптимизирует сборки;

  • Тайны обфускации. Углубимся в проблему обфускации кода;

  • D8, R8. Узнаем, что такое D8, R8 и обозначим их место в процессе сборки.

После прочтения статьи у вас должно сложиться широкое понимание того, как работает оптимизация и какие инструменты для этого используются. Этих знаний будет достаточно для координации в предметной области и поиска решений конкретных задач, связанных со сборкой Android приложений.

Постановка проблемы

Сейчас почти каждый пользуется мобильными приложениями, а некоторые даже их разрабатывают. Так или иначе вы, как (пользователь / разработчик), стремитесь (иметь / создать) хороший продукт. Что значит “хороший продукт” в мире мобильных приложений? Хорошее мобильное приложение стремится:

  • быть размером 0 бит;

  • работать быстрее света;

  • быть безопаснее Зоны 51.

Чтобы понять, как этого добиться, необходимо понять, как работают мобильные приложения. Во-первых, это задача, которую мобильное приложение решает. Во-вторых, это сторонние сервисы, помогающие мобильному приложению решить поставленную задачу. В-третьих, это само мобильное приложение, решающее задачу при помощи сторонних сервисов. 

От реализации каждой из этих компонент зависит степень соответствия установленным требованиям.

Оптимизация сборки

Мы выделили три компонента, необходимых для работы мобильного приложения:

  • задача, решаемая приложением (бизнес логика);

  • сторонние сервисы, помогающие в решении поставленной задачи;

  • мобильное приложение.

И три требования, удовлетворить которые мы стремимся:

  • минимизировать потребяемую память

  • повысить скорость работы

  • повысить безопасность

Сконцентрируемся на мобильных приложениях. Как правило, в команде разработчик выполняет одну роль, поэтому будем считать, что бизнес логика и сторонние сервисы прорабатываются не нами. Но не будем забывать, что мобильное приложение реализует в себе часть бизнес логики, взаимодействует со сторонними сервисами, и ответственность за это несет мобильный разработчик. Также не будем забывать, что от того, как реализованы эти компоненты (вне мобильного приложения), зависит качество вашего продукта, поэтому мотивируйте своих боевых товарищей (или самого себя, в случае full stack) делать качественно :)

Мобильная разработка. Приведу примеры способов оптимизации мобильных приложений согласно общим требованиям:

  • Минимизировать потребяемую память: использовать сборки, максимально соответствующие конфигурации устройства (split apks, app bundle); по возможности хранить данные на удаленном сервере; использовать Dynamic Delivery; удалять неиспользуемый код / ресурсы; и так далее.

Замечание

Учтите, что мобильное приложение использует память не только в процессе работы, но и по факту установки на устройство.

  • Повысить скорость работы: оптимизировать бизнес логику; правильно выбирать алгоритмы и структуры данных; делать сборки максимально соответствующие конфигурации устройства; мотивировать пользователя использовать производительные устройства оптимизировать код; и т.д…

  • Повысить безопасность: шифровать данные приложения; использовать безопасные каналы связи; ограничивать права; запутывать код; и т.д…

Мобильное приложение — специальным образом скомпилированный код, написанный разработчиком, установленный на устройство. Специальным образом скомпилированный код называют сборкой. От того, как производится сборка зависит: 

  • сколько места займет приложение на устройстве;

  • как быстро оно будет работать;

  • насколько безопасным будет его использование.

Дальнейшее повествование будет вестись в разрезе Android разработки. Остальные — не переключайтесь.

Как производится сборка?

Этапы сборки Android приложения.
Этапы сборки Android приложения.

На схеме представлены этапы сборки Android приложения: от компиляции исходного кода до установки на устройство (Подробнее можно посмотреть тут). Каждый из этапов имеет значение для достижения исходной цели (память, скорость, безопасность). Но не в каждый из них можно внести качественные изменения: java, kotlin, DX, Android компиляторы, как правило, модифицируются их создателями (Oracle, JetBrains, Google). 

Это не значит, что вы, как разработчик, никак не можете повлиять на вид итоговой сборки: есть возможность добавить промежуточные этапы согласно описанным контрактам между основными этапами. Пример такого контракта: DX компилятор принимает на вход Java байт-код. DX компилятору без разницы, как и откуда он получит этот байт-код, поэтому с ним можно сделать что угодно. Здесь подключаемся мы: модифицируем байт-код так, как нужно нам, и отдаем DX - вот и все!

Итого. Размер мобильного приложения, скорость его работы, безопасность его использования в конечном итоге зависят от того, какая сборка будет установлена на устройство. Все действия, призванные улучшить данные характеристики будем называть оптимизацией. Будем считать, что влиять на процесс сборки мы можем только посредством добавления промежуточных этапов, не нарушающих контракты между  основными. К оптимизации!

ProGuard

Конечно, мы не первые, кто задается вопросами оптимизации Android приложений. В целом  этот процесс состоит из этапов: сокращение кода, сокращение ресурсов, оптимизация кода, обфускация и не меняется от технологии к технологии. До недавних пор (см. раздел D8, R8), одним из самых популярных решений задачи оптимизации сборки Android приложений был — ProGuard (далее PG).

Определение

«ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier.» (PG - это инструмент для сокращения, оптимизации (кода приложения), обфускации, верификации Java байт-кода). Такое определение своему продукту даёт Guardsquare. Кто такие GuardSquare?

О производителе

Guardsquare - компания, занимающаяся разработкой программ для защиты мобильных  приложений. Среди них:

  • iXGuard - защиты iOS приложений и SDK

  • ProGuard - оптимизация Java и Android приложений

  • DexGuard - улучшенная версия ProGuard

  • ThreatCast - мониторинг безопасности iOS и Android приложений

Место ProGuard в процессе сборки приложения

Вспомните схему сборки Android приложения. Ниже представлен этап компиляции .class файлов в .dex.

Встраивание PG в процесс сборки приложения
Встраивание PG в процесс сборки приложения

PG встраивается перед этапом dex компиляции и поддерживает контракт: dex компилятор принимает на вход .class файлы, выдает .dex файлы. Данная “инъекция”  не нарушает контрактов между основными этапами сборки: на выходе вы получите приложение, но с оптимизацией :)

Процесс оптимизации

Общее описание

Ниже представлена схема того, из каких этапов состоит PG оптимизация.

Схема PG оптимизации
Схема PG оптимизации

На вход PG подаются .jar файлы. Вспомним контракт c dex компилятором: на вход подается .class, но здесь .jar. На самом деле .jar — архив с .class файлами, метаинформацией, статическими ресурсами. Зачем PG понадобились статические ресурсы узнаем в разделе Shrink. Обратите внимание, что и на выходе также получаются .jar файлы. С этим нет проблем, так как это принятый в Java среде формат дистрибуции кода (приложений, библиотек), понятный PG и dex. В отличие от “Input jars”, которые изменяется в процессе, “Library jars” не изменяются PG и используются им в качестве контекста (Android Framework, Java SE, Java ME) для “Input jars” (Например, для Android Framework это android.jar, который находится на устройстве и недоступен в момент работы PG).

Все артефакты процесса оптимизации обозначены прямоугольниками, а сами оптимизации стрелками с соответствующими названиями. То есть на вход “shrink” подаются .jar файлы, они же (но уже измененные) подаются на вход “optimize” и так далее. Каждая из оптимизаций: “shrink”, “optimize”, “obfuscate”, “preverify” — опциональная. Например, вы можете отказаться от “obfuscate” или “shrink”, если это вам не нужно. Однако, порядок выполнение операций строго фиксирован. В текущей итерации PG вы, например, не можете выполнить “optimize” до “shrink”. Это правило существует для сокращения лишней работы. Смысл такой: оптимальнее сначала содрать обои,  сделать уборку, а не сделать уборку, содрать обои (вы проникнитесь этой идеей при рассмотрении конкретных оптимизаций).

Shrink

Переводится как “сокращать”. Shrink сначала удаляет неиспользуемый приложением код затем ресурсы, тем самым сокращает итоговый вес .class файлов и статических ресурсов приложения. Такой порядок выполнения операций гарантирует, что ресурсы, к которым обращается неиспользуемый код (то есть используемые неиспользуемым кодом) будут удалены.

Code shrinking

Для того, чтобы понять, какой код следует удалить, нужно его классифицировать на достижимый и недостижимый. PG строит дерево достижимого кода, корнем которого является специальные входные точки, называемые seeds (от английского семена, источники).

  1. На вход подаются seeds, которые по умолчанию считаются достижимыми и формируются в зависимости от среды исполнения кода (например, Activity в Android) и пользовательских правил.

  2. Код каждого seed просматривается на предмет использования кода из других файлов. Весь используемый код помечается как достижимый и добавляется в очередь на проверку.

  3. Для всех достижимых файлов из очереди выполняется тоже самое,  что и для seed.

Весь достижимый код — сохраняется, весь недостижимый код — удаляется (tree shaking).

Схематичное представление дерева достижимого кода
Схематичное представление дерева достижимого кода

В чем особенность code shrinking в Android? Seeds формируются на основе:

  • Правил сгенерированных Android Gradle Plugin (далее AGP). Часть директив используемых по умолчанию находится в файлах: proguard-android.txt, proguard-android-optimize.txt.

  • Правил из подключенных библиотек, описанных в <library-dir>/proguard.txt (для .aar) или <library-dir>/META-INF/proguard/ (для .jar)

  • Пользовательских правил. Например, <module-dir>/proguard-rules.pro или <module-dir>/consumer-proguard-rules.pro

Resource shrinking

Принцип классификации ресурсов на достижимые и недостижимые схож с классификацией кода.

  1. Сканируется весь достижимый код на факт использования ресурсов.

  2. Используемые ресурсы помечаются как достижимые. Причем внутри одного файла могут  быть как достижимые, так и недостижимые ресурсы.

  3. Все достижимые ресурсы — сохраняются, все недостижимые ресурсы — удаляются (tree shaking).

Схематичное представление дерева достижимых ресурсов
Схематичное представление дерева достижимых ресурсов

Особенности resource shrinking в Android:

Ресурс считается достижимым, если используемые напрямую

val name = resources.getString(R.string.name)) 

или  косвенно

val name = String.format("img_%1d", N + 1)
val res = resources.getIdentifier(name,"drawable",packageName)

Режимы работы resource shrinking:

  • Defense сохранит ресурсы используемые напрямую или косвенно

  • Strict сохранит ресурсы используемые напрямую

Поддерживает пользовательские правила. Например, в keep.xml.

Optimize

Данный этап нужно отличать от оптимизации сборки в целом. Optimize производит манипуляции с Java байт-кодом с целью его оптимизации с программной точки зрения.

Существуют различные техники оптимизации кода. Например, Control flow analysis — изменяет граф потока управления; Data-flow analysis — оптимизирует аллокаций и распространение данных; Partial evaluation — производит проактивное вычисление значений.

Пример оптимизации кода

Разберем пример того, как теоретически может работать оптимизация кода.

Предупреждение! Пример носит строго образовательный характер. В примере представлен исходный код, но на самом деле в PG изменяется байт-код. Приведенные в примере техники оптимизации могут отличаться в зависимости от выбранной технологии.

Посмотрите на исходное состояние кода. 

Красным будут помечаться удаленные строки кода, зеленым — добавленные.

Перед тем, как читать пояснения, попробуйте самостоятельно оптимизировать данный код. Результат — в комментарии :)

Spoiler
class Test {
    private static final String WILDCARD = "*.";
    public static void main(String... args) {
      String pattern = "*.example.com";
      String host = pattern.startsWith(WILDCARD)
          ? pattern.substring(WILDCARD.length())
          : pattern;
      String canonical = HttpUrl.get("http://" + host).host();
      System.out.println(canonical);
    }

“*.” и “*.example.com” — константы. Эти значения можно перенести в место их непосредственного использования и удалить их объявления в переменных. Более формально: произвести оптимизацию Data-Flow посредством Constant Propagation.

Обратите внимание, что “*.example.com” всегда начинается с “*.”. Результат  данной операции может быть вычислен до запуска программы. Так техника оптимизации Partial Evaluation.

В данном условном выражении всегда используется одна ветвь, поэтому его можно “выпрямить”. Избавляемся от другой при помощи Control Flow оптимизации: Branch Prediction.

Еще один пример применения Partial Evaluation.

И еще один пример применения Partial Evaluation.

Итог. Посмотрите насколько упростился исходный код. Было 12 строк, стало 5 строк. Поразительный результат. 

Особенности optimize в Android:

  • По умолчанию PG оптимизации не применяются для Android (proguard-android.txt:11 -donotoptimize инструкция)

  • Примеры оптимизаций поддерживаемых ProGuard:

    • class/marking/final (помечает классы как final, где это возможно)

    • class/unboxing/enum (заменяет enum на целочисленные константы, где это возможно)

    • class/merging/vertical (сливает классы по вертикали, устраняя ненужное наследование)

    • с остальными можно ознакомиться по ссылке.

Obfuscate

Переводится как “запутывать”. Часто его называют просто “обфускация”. Из руководства к ProGuard: “In the obfuscation step, ProGuard renames classes and class members that are not entry points. In this entire process, keeping the entry points ensures that they can still be accessed by their original names.” (На этапе  обфускации PG переименовывает классы и его члены, которые не являются входными точками. Это сделано для того, чтобы входные точки были доступны извне по их исходным именам (Android OS должна знать реальное название компоненты, чтобы иметь возможность ее запустить))

Это необходимо для усложнения чтения вашего кода после декомпиляции вашего приложения злоумышленниками (reverse engineering). 

Алгоритм обфускации кода

Обфускация производится за счет переименования названий, используемых в исходном коде (и несущих смысл), на неосмысленные.

Опишем общий алгоритм:

  1. Все имена, используемые в приложении: имена пакетов, классов, методов, полей, выносятся в отдельный словарь (словарь обфускации).

  2. Всем именам из словаря даются новые (обфусцированные) имена.

  3. На основе составленного словаря код приложения переименовывается (обфусцируется).

Схематичное представление работы на этапе obfuscate.
Схематичное представление работы на этапе obfuscate.

Почему используется название “short name” читайте в главе «Тайны обфускации кода».

Пример обфускации кода

Разберем пример.

Предупреждение! Пример носит строго образовательный характер. В примере представлен исходный код, но на самом деле в PG изменяется байт-код. Приведенные в примере техники оптимизации могут отличаться в зависимости от выбранной технологии.

На вход подается необфусцированный код. Он сканируется на наличие имен.

Строится словарь обфускации: всем исходным именам приводятся в соответствие новые — обфусцированные.

По словарю обфускации переименовывается код, поданный на вход. Получаем результат.

Этап обфускации подробно рассмотрен в разделе «Тайны обфускации кода».

Preverify

Переводится как “предварительно проверить”. На этом этапе осуществляется проверка кода и ресурсов на соответствие требованиям среды эксплуатации.

Особенности preverify в Android:

  • «Preverification is irrelevant for the dex compiler and the Dalvik VM, so we can switch it off with the -dontpreverify option.» (Предварительная верификация неуместна при использования dex компилятора и Dalvik VM, поэтому мы может выключить этот этап используя флаг -donotpreverify.

  • Флаг -android укажет на необходимость произвести проверку на соответствие target API и совместимости.

Тайны обфускации кода

Мы разобрали как ProGuard реализует этап obfuscate. Но можно ли на этом заканчивать? Давайте разбираться.

Фактическая реализация

На сайте developer.android.com можно найти текст “The purpose of obfuscation is to reduce your app size by shortening the names of your app’s classes, methods, and fields” (Цель обфускации - сократить размер приложения за счет сокращения имена классов, метод, полей приложения).

Вспоминаем пример

До обфускации:

package com.example.proguard.obfuscation;

public class PinCodeBuilder {
    private int pinCodeLength;
    private StringBuilder pinCodeStringBuilder = new StringBuilder(pinCodeLength);

    public PinCodeBuilder(int pinCodeLength) {
        this.pinCodeLength = pinCodeLength;
    }

    public void increment(int pinCodeDigit) {
        if (pinCodeStringBuilder.length() < pinCodeLength) {
            pinCodeStringBuilder.append(pinCodeDigit);
        }
    }
}

После обфускации:

public class a {
    private int a;
    private StringBuilder b = new StringBuilder(pinCodeLength);

    public a(int pinCodeLength) {
        this.a = pinCodeLength;
    }

    public void c(int pinCodeDigit) {
        if (b.length() < a) {
            b.append(pinCodeDigit);
        }
    }
}

Сравним количество символов кода до и после обфускации. Получаем 402/250 — чуть больше чем в 1.5 раза сократился текст исходного кода. Цель обфускации, согласно определению гугла достигнута. Но почему в тексте нет ничего про запутывание?

Реальный смысл

Знатоки латыни скажут: «obfuscare — затенять, затемнять»; знатоки английского скажут «obfuscate — делать неочевидным, запутанным, сбивать с толку». Не совсем одно и тоже, что и сокращение имён, не правда ли?

Определение

Согласно википедии “…obfuscate code to conceal its purpose (security through obscurity) or its logic or implicit values embedded in it, primarily, in order to prevent tampering, deter reverse engineering…” (… обфусцировать код с целью скрытия его исходного смысла  (защита через запутывание) или его логики  или литералов записанных в коде, преимущественно, чтобы предотвратить внедрение в код, усложнить обратный инжиниринг …).

Еще раз

  • GuardSquare, Google: обфускация - сокращение имён.

  • Wikipedia: обфускация - запутывание смысла кода.

Критика

Обфускация в ProGuard и в R8 (о котором поговорим далее) — минификация кода посредством замены имён более короткими и менее осмысленными. Такой подход лишь ЧАСТИЧНО соответствует исходному смыслу обфускации: скрытия исходного смысла, логики, литералов.

Вспомните строчку “minifyEnabled = true” из build.gradle. MINIFY.  OBFUSCATE. Поздравляю, мы купили второй пылесос.

Отрицание

Кто-то возразит:

«Посмотрите на стектрейс обфусцированного кода. Почти ничего не понятно!», И будет прав. Действительно, ПОЧТИ ничего непонятно, но дело в том, что иногда этого ПОЧТИ недостаточно.

Гнев

Другой сделает reverse engineering вашего приложения и приведет пример со словами: “литералы и бизнес логика прослеживаются!”. И будет прав. Действительно, при стандартной настройке обфускации литералы и бизнес логика нетрудно прослеживаются.

Торг

Давайте попробуем разобраться, почему нам не стоит беспокоиться об обфускации через минификацию (ProGuard, R8 подход)? Вот некоторые аргументы:

  • Часть злоумышленников отпадет (со смеху :)) при виде минифицированного байт-кода;

  • Приложение не содержит ценную бизнес логику, то есть нам нечего скрывать.

Часть людей успокоилась. Я рад. Но не торопитесь уходить.

Депрессия

Давайте зададим пессимистичный вопрос: “почему нам стоит беспокоиться?”. Предположительные ответы:

  • Минификация не запутывает байт-код

  • Ценная бизнес логика прослеживается

  • Видны важные литералы

Для более “толстых” и потенциально опасных приложений минификация исходного кода не выдерживает критики. Но обфускация не заканчивается минификацией и тут появляется надежда…

Надежда

До этого момента для нас обфускация начиналась и заканчивалась одной лишь минификацией (переименованием). Благо, это далеко не так. Минификация лишь один из множества техник обфускации. Приведу примеры других возможных приемов обфускации: запутывание названий; шифрование литералов; обфускация control flow; преобразования языковых конструкций; замена высокоуровневых инструкций низкоуровневыми эквивалентами; вставка фиктивного кода; удаление метаданных; сокращение информацию, которой может воспользоваться взломщик; слияние кода; запутывание структуры исходного кода (файлы, папки); anti-tampering; сброс и логирование попыток вмешательства в работу приложения; и другие…

ProGuard

(Достает из грязи) несмотря на то, что PG не поддерживает все техники обфускации, он способен делать больше, чем просто минифицировать код. Для более тонкой настройки можно воспользоваться опциями:

  • -flattenpackagehierarchy […] - складывает весь код в один пакет

  • -dontusemixedcaseclassnames - имена в нижний регистр

  • -adaptclassstrings […] - обфускация имён строковых констант

Стоит вспомнить, что PG лишь один из продуктов GuardSquare. Он бесплатен, но существует и коммерческий продукт — DexGuard, позиционируемый как усовершенствованная версия PG. Вот что пишет о нем GuardSquare:

  • DexGuard полность совместимость с ProGuard

  • Помимо обфускации имён, DexGuard способен обфусцировать арифметические выражения, control flow, нативный код, имена библиотек, ресурсов, вызовов SDK, шифровать классы, ресурсы, нативные библиотеки…

Звучит неплохо. Проверить его в деле доверяю дорогим читателям (обязательно напишите свою оценку этого продукта, если имеете опыт использования).

Open source

Как хорошо, что есть ProGuard и DexGuard, но на них история не заканчивается. Существует ряд open source обфускаторов. Среди них:

  • Обфускаторы java байт-кода: yGuard, JODE, JavaGuard, jarg, Obfuscapk.

Все хотят узнать ваш опыт использования этих технологий. Прошу в комментарии.

  • Не стоит забывать, что Android приложения пишутся не только на Java и Kotlin. На случай, если вы используете C/C++: obfuscator, movfuscator; JavaScript: javascript-obfuscator.

Опять же, ваш опыт — в комментарии.

D8, R8

При изучении вопроса оптимизации сборок Android приложений можно наткнуться на эти две аббревиатуры. Разберёмся, что они значат и какое отношение имеют к оптимизации  сборок, PG и Android в целом.

Небольшой спойлер. До погружения в вопрос оптимизации сборок, я думал, что использую ProGuard.

Вспомним:

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), proguard-rules.pro'
proguard-rules.pro, consumer-rules.pro

ProGuard, ProGuard, ProGuard… Оказывается, что фактически я использовал R8 (начиная с AGP версии 3.4). Также я думал, что использую DX, но фактически использовал D8 (начиная с AGP версии 3.2).

Почему так вышло? Читайте далее.

D8

Определение

Что такое D8? Google (его создатели) позиционирует D8 как dex компилятор нового поколения.

К слову. Он используется по умолчанию с версии 3.2 AGP. Именно поэтому я не заметил его появление в своей жизни: всего лишь обновил AGP, а тут вот что. Урок — читайте release notes продуктов, которые используете.

Мотивация

Зачем он нужен? Вспомним схему встраивания  PG при сборке приложения. .dex файл компилируется DX компилятором.

Вырезка из схемы сборки Android приложения: этапы java (kotlin), dex компиляции
Вырезка из схемы сборки Android приложения: этапы java (kotlin), dex компиляции

Так выглядел процесс компиляции .dex до появления D8. Обратите внимание на количество шагов: 2 - для Kotlin кода, 3 - для Java.

Что стало после появления D8:

Схема этапов сборки при использовании D8
Схема этапов сборки при использовании D8

D8 вобрал себя этап desugar, тем самым сократил общее число шагов компиляции: 2 - для Kotlin кода, 2 - для Java.. К чему это привело?

D8 vs DX

Google приводить сравнительный анализ компиляторов DX (используемый по умолчанию до появления D8) и нового D8. Обратите внимание:

  • D8 ощутимо быстрее компилирует исходный код;

  • Размер выходного .dex немного меньше при использовании D8.

Сравнение размеров выходных .dex файлов при использовании DX и D8. Небольшое преимущество D8
Сравнение размеров выходных .dex файлов при использовании DX и D8. Небольшое преимущество D8
Сравнение скорости компиляции .dex файлов при использовании DX и D8. Победа D8: время сократилось на треть
Сравнение скорости компиляции .dex файлов при использовании DX и D8. Победа D8: время сократилось на треть

R8

Определение

R8, как и D8, разрабатывается Google и также связан со сборкой Android приложений. Но R8 — не .dex компилятор, он — оптимизатор.

R8 используется по умолчанию с версии 3.4 AGP вместо PG. Тут я опять удивился положению вещей. Особенно потому, что раньше я писал правила для ProGuard, а понимает их R8. 10 очков Google за хорошую совместимость c PG правилами. Повторяю урок — читайте release notes продуктов, которые используете.

Мотивация

Посмотрим, чем мотивировал себя Google при разработке этого оптимизатора. Так выглядел процесс сборки до появления R8.

Схема этапов сборки при использовании PG
Схема этапов сборки при использовании PG

Так он стал выглядеть после:

Схема этапов сборки при использовании R8
Схема этапов сборки при использовании R8

История схожа с D8: оптимизировать процесс сборки за счет сокращения промежуточных этапов. Но! Вспомните еще один момент, чей ProGuard?  —  GuardSquare, а чей R8? — Google. Отсюда можно сделать еще один вывод о мотивации Google к созданию R8 — замена сторонней технологии свой собственной. И тут родился конфликт…

R8 vs ProGuard

Появилось две статьи (сначала от Google, потом о GuardSquare) со сравнительным анализом R8 и ProGuard.

Вкратце, о чем пишет Google:

  • R8 (в комбинации с D8) в compact и full режимах почти в 2 раза быстрее “шринкует” и компилирует .dex.

  • Размер выходного .dex немного меньше, чем у PG.

Сравнение скорости выполнения shrink и dex компиляции
Сравнение скорости выполнения shrink и dex компиляции
Сравнение размеров выходных dex файлов
Сравнение размеров выходных dex файлов

Не нужно забывать, что Google, как владелец Android, имеет бОльшее влияние на развитие платформы и сопутствующих технологий. Именно поэтому они могут позволить себе заменить DX компилятор на D8 (один из основных этапов (см. раздел Оптимизация сборки) и встроить сверху R8.

О чем пишет GuardSquare. Основной аргумент GS  в пользу PG заключается в  большем количестве оптимизаций кода: 520 — ProGuard к 6 — R8 (на момент написания статьи — 23 октября 2019). Сравнительная таблица видов оптимизаций из статьи доступна по ссылке. Некоторые из них:

Также GuardSquare утверждает, что ProGuard работает быстрее при малом количестве итераций, но тут стоит сделать одно замечание: чем меньше итераций, тем меньше оптимизаций, как следствие бОльший размер .apk, менее оптимизированный код.

Стоит отдать должное GuardSquare за то, что их продукт разрабатывается более 15 лет, в то время как R8 стал доступен в лишь 2018.

Резюме

Было интересно окунуться в необычные детали такой, казалось бы, обычной задачи как оптимизация сборки. Надеюсь, вы разделили радость познания вместе со мной. 

Подведем итоги:

  • Задача, сторонние сервисы, мобильное приложение — вот компоненты, от которых зависит итоговый размер приложения, скорость его работы и безопасность его использования. Требования, предъявляемые к каждой из этих компонент,  определяют итоговое решение.

  • Процесс сборки Android приложений состоит из основных этапов: java / kotlin компиляция, dex компиляция. Между основными этапами определены строгие контракты, поддерживая которые можно встроить дополнительные этапы. Например, оптимизация кода, ресурсов.

  • Оптимизация сборки в общем случае состоит из этапов: сокращение кода, ресурсов (память); оптимизация кода (скорость); обфускация (безопасность).

  • Обфускация — приведение кода к виду, сохраняющему его функциональность, но затрудняющему анализ. Запутывание имен — одна из многих техник обфускации, часто используемая в популярных оптимизаторах.

  • ProGuard — java байт-код оптимизатор, способный оптимизировать сборки Android приложений. Разработан GuardSquare.

  • D8, R8 — технологии, разработанные Google, призванные улучшить процесс сборки (D8) и ее оптимизации (R8). R8 — позиционируется как замена ProGuard. Что использовать в итоге — решать вам.

Ссылки

Ссылки на источники приложены в месте их непосредственного использования.

Полезные материалы:

  1. Android Developers. Shrink, obfuscate, and optimize your app.

  2. Android Developers, Medium. Practical ProGuard rules examples.

  3. Guardsquare. ProGuard manual.

  4. Android Developers, Medium. Troubleshooting ProGuard issues on Android

  5. IMStudio, Medium. Android Journey: Proguard, D8, R8 what are they?

  6. Otus, YouTube. ProGuard / R8 …

  7. Android Developers, YouTube. Shrinking Your App with R8.

  8. Yonatan V. Levin. Android CPU, Compilers, D8 & R8.

  9. Jake Wharton, R8 Optimizations.

  10. inwady, Habr. 6 способов спрятать данные в Android-приложении.

  11. forceLain, Habr. Как перестать бояться Proguard и начать жить.

Источник: https://habr.com/ru/post/533578/


Интересные статьи

Интересные статьи

Всем привет. Меня зовут Нещадин Иван, и я расскажу про оптимизацию одного из микросервисов Авито на Go. История построена вокруг различных инструментов, которые доступ...
Всем привет, Меня зовут Андрей Рыжкин, я CTO AGIMA. Сегодня я расскажу о том, как мы тестируем приложения на Android, а также поделюсь нашим чек-листом. Чек-лист от команды AGIMA...
Добрый день, уважаемые хабровчане! Несколько лет назад я купился на красочную рекламу zWave и установил себе оконные датчики, базирующиеся на этом протоколе. К домашнему серверу был подключен USB...
Cтатья будет полезна тем, кто думает какую выбрать CMS для интернет-магазина, сравнивает различные движки, ищет в них плюсы и минусы важные для себя.
Гугл любит пасхалки. Любит настолько, что найти их можно практически в каждом продукте компании. Традиция пасхалок в Android тянется с самых первых версий операционной системы (я думаю, все в ку...