Совсем недавно мы включили R8 в нашем приложении для Android. Что это значит? Почему я иногда использую термин proguard и даже добавляю proguard.pro файлов в исходный код? И как мы включили R8 в приложении в несколько шагов? На все эти вопросы можно найти ответы в этом блоге.

Что такое R8

Вообще говоря, R8 — это компилятор, который преобразует байт-код Java в «оптимизированный» код dex.

Если вы хотите запустить код Java (или Kotlin) на устройстве Android, инструмент сборки должен выполнить несколько шагов. Во-первых, он компилирует исходные файлы в байтовый код. Во-вторых, он компилирует байтовый код в код dex. Формат dex (Dalvik executable) может быть понят и выполнен Android.

По умолчанию dex-код (как и байт-код) может быть легко декомпилирован буквально кем угодно в мире. Вам «только» нужен доступ к коду dex и байт-коду Java декомпилятору. После этого вы увидите что-то очень похожее на оригинальный исходный код этих файлов.

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

Вот тут-то и появляется R8. Это маленькое слово «оптимизированный», упомянутое ранее, делает немного больше, чем просто то, что я описал. Он не только компилирует исходные файлы в байтовый код, а затем в код dex. Он также оптимизирует, сжимает и запутывает байтовый код при преобразовании его в код dex.

Подробнее, пожалуйста!

Техника сжатия компилятора R8 означает, что он может определить, какие классы (методы, переменные и т. д.) можно удалить, поскольку они все равно не используются программой. Это довольно распространено, когда вы добавляете сторонние зависимости в свой проект. Например, вы могли добавить в свой проект Гуаву. Guava — довольно большая библиотека с точки зрения функциональности и строк кода. Но, возможно, вы используете только несколько служебных методов из него. Без R8 в вашем приложении будут все строки кода библиотеки. Никакой код не будет удален. При включенном (или выполненном) R8 все неиспользуемые классы (методы, переменные и т. д.) удаляются. Компилятор R8 обнаруживает неиспользуемый исходный код и удаляет его из кода dex.

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

Кроме того, он также оптимизирует исходный код. В качестве простого примера он может найти оператор switch (или Kotlin when), который может быть быстрее с оператором if-elseif-else. Затем он преобразует это switch/when в байтовом коде в оператор if-elseif-else в коде dex.

И последнее, но не менее важное: он запутывает код dex. Это означает, что он преобразует исходные имена классов, которые вы использовали (а также имена пакетов, методы, переменные, что угодно) в тарабарщину. Например, функция, которая выглядит как employee.setName(name), окажется в ab.a(cc). Переменная employee была переименована в ab, метод setName в a, а переменная name в cc.

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

Хорошо, что такое прогард?

Немного истории. Насколько я знаю, сжатие кода и его обфускация всегда были возможны на Android. Но на заре Android компилятора R8 не существовало. (Прямо как Котлин. Тогда его тоже не было 😀).

До того, как Google решил создать свой собственный инструмент сжатия и запутывания и назвал его R8, Android использовал ProGuard. ProGuard, как и компилятор R8, является инструментом сжатия и запутывания.

Вы можете спросить, зачем Google создал свой собственный компилятор, если он уже существует? Как было написано выше, формат, который понимает Android, называется dex code. Но ProGuard не создает код dex. Он производит оптимизированный байт-код. Помните, что я писал выше о двух шагах, которые происходят для создания кода dex. Теперь представьте, что мы добавляем в эту цепочку еще один инструмент (ProGuard). Это будет выглядеть следующим образом:

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

И это именно то, что происходило до того, как они представили R8. R8, с другой стороны, компилирует байтовый код напрямую в оптимизированный код dex. При этом у нас остается всего два шага для создания кода dex.

Файл конфигурации

В лучшем случае вы можете просто запустить компилятор R8, и ваш код будет уменьшен, оптимизирован и запутан. Но в большинстве случаев вам нужно добавить некоторые настройки, чтобы предотвратить запутывание и/или сжатие некоторых частей вашего кода. Почему это? Типичный пример — отражение. Вы (или используемая вами библиотека) можете использовать отражение. При использовании отражения у вас нет прямого доступа к классам. Если это так, R8 не находит прямого узла для этого класса и думает, что он не используется, и удалит (или обфусцирует) его. Если это так, отражение, конечно, не удастся (во время выполнения). Потому что класс, к которому вы хотите получить доступ через отражение, теперь имеет другое имя.

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

Файл конфигурации нуждается в специальном формате, чтобы его мог понять компилятор. ProGuard, старая программа сжатия, которая до сих пор актуальна в мире Java, уже имеет такой формат. Поскольку ProGuard использовался до R8 в мире Android, Google решил, что они реализуют R8 таким образом, чтобы в этом отношении он понимал своего брата ProGuard. Это означает, что ProGuard и R8 могут иметь один и тот же файл конфигурации. Нет необходимости что-либо менять и/или настраивать, если вы переключаетесь с одного на другое. Думаю, это также было одной из главных целей Google при разработке R8. Как уже говорилось, ProGuard раньше использовался на Android. Когда они представили R8, они хотели, чтобы разработчики плавно переключались с ProGuard на R8, буквально избегая каких-либо настроек. Все должно происходить за кулисами. Не было введено нового синтаксиса или формата конфигурации, не были добавлены новые методы Android Gradle Plugin или что-то подобное. Они продолжали использовать ту же формулировку, что и много лет назад. Например, есть еще функция proguardFiles, которая принимает в качестве аргумента путь к конфигурационным файлам proguard (или R8? 😉).

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

И из-за всего этого я также до сих пор использую proguard.pro файлы, содержащие конфигурацию для… R8. Честно говоря, было бы немного странно иметь следующий код в файле build.gradle:

proguardFiles(getDefaultProguardFile(), r8.pro)

Как мы включили R8 в наше приложение

Я занимаюсь Android-разработкой уже довольно давно. Раньше я несколько раз пытался включить ProGuard для существующего приложения. Насколько я правильно помню, он всегда терпел неудачу. Я понятия не имею, почему это было так. Может ProGaurd хуже R8? Может быть, мой исходный код был хуже, чем сейчас? Может быть, документация была не так хороша, как сегодня? Может быть, процесс как добавить его в приложение был не таким уж хорошим? Может быть, у меня не было большого опыта разработки программного обеспечения?

Не имею представления. Я просто предполагаю. Все, что я знаю, это то, что мне всегда было больно включать его. Это закончилось часами тестирования и изменения конфигураций, чтобы в конечном итоге привести к сбою приложения.

Подводя итог, у меня не было хорошего опыта (и отношений) с ProGuard.

Сегодня мир выглядит немного иначе. Документация R8 проста, я научился на своих предыдущих ошибках, и я работаю над хорошо структурированным приложением.

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

С точки зрения птиц, у нас есть несколько независимых модулей Gradle, которые в основном содержат только «несколько файлов». Каждый модуль имеет определенное назначение. У нас есть так называемые «libs», которые в основном используются для предоставления небольших утилит или логики, специфичной для предметной области. Эти библиотеки можно использовать в приложении. Типичным примером этого является наша библиотека lib-logging. Эта библиотека может быть добавлена ​​буквально к любому другому модулю для обеспечения функциональности ведения журнала.

Рядом с нашими «libs» у нас есть так называемые «features». Функции, с другой стороны, предоставляют приложению полный набор функций. Примером такой функции является наш модуль feature-ride-creation. Эта функция предоставляет всю функциональность, позволяющую пользователям создавать поездки. Это включает в себя не только логику, связанную с предметной областью, но и логику, связанную с пользовательским интерфейсом.

И да, прежде чем вы спросите, всегда сложно провести различие между «что такое библиотека и что такое фича» 😁.

Помимо наших хорошо модульных модулей, у нас все еще есть два «устаревших» сегмента. Модуль libraries и модуль lib-core. Эти модули по-прежнему содержат много функций, и их необходимо шаг за шагом извлекать в свои собственные модули.

В любом случае, вернемся к R8 😅

Мы использовали наш многомодульный проект, когда начали активировать R8. Вместо одной большой задачи «Включить R8 в приложении» мы решили разделить ее на несколько (более мелких) задач.

Во-первых, мы включили R8 по умолчанию, но добавили файл конфигурации в каждый модуль, чтобы сохранить все классы (методы, переменные и т. д.) для этого конкретного модуля. В этом контексте сохранять означает не удалять и не запутывать код. После этого мы удалили файл конфигурации для нескольких модулей, чтобы проверить, работает ли наша общая идея. Оказалось, что это сработало. Очень даже хорошо!

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

Мы можем буквально итерировать шаг за шагом от нашей базовой реализации, чтобы улучшить R8 (вау, кажется, мы делаем Scrum правильно 😱).

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

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

Еще один приятный побочный эффект этого подхода заключается в том, что мы можем медленно тестировать R8 в производственной среде. Я имею в виду, что мы можем видеть, какие «изменения» у нас есть, включив его. Например, у нас может быть более медленный конвейер выпуска на нашем сервере непрерывной интеграции. Возможно, мы делаем что-то не так с файлом сопоставления или что-то еще, чего мы еще не видим. Но мы узнаем 😉.