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

Я планирую написать две статьи о ProGuard. В этой я расскажу о добавлении ProGuard в проект и дам несколько советов, которые с вероятностью 95% решат вашу задачу. В следующей статье мы подробнее рассмотрим работу инструмента и разберем, что делать, если советов из первой статьи недостаточно.

Что такое ProGuard

Разработчики утверждают, что ProGuard — это shrinker, optimizer, obfuscator и preverifier. Ни одно из этих слов не переводится на русский без оговорок. Эти четыре слова обозначают четыре последовательных этапа работы ProGuard.

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

shrink [ʃrɪŋk] уменьшать, сокращать

Optimization step ProGuard может классы и методы, которые не являются точками входа, сделать private, final, static, удалить неиспользуемые параметры, “встроить” (inline) методы, и т.д.

Оптимизация силами ProGuard’а приложений под Android часто вреда приносит больше, чем пользы, поэтому по умолчанию этот шаг пропускают. Если появится желание поэкспериментировать, посмотрите сначала, что написано в родном файле конфигурации.

Obfuscation step ProGuard переименовывает классы и члены классов, которые не являются точками входа. Точки входа сохраняют свое оригинальное название. Это затрудняет декомпиляцию и исследование работы приложения (reverse engineering).

obfuscate [‘ɔbfəskeɪt] затемнять, напускать туману, делать непонятным, запутанным; озадачивать, сбивать с толку

Preverification step Виртуальная машина Java при загрузке классов проводит проверку, что код не может случайно или намеренно вырваться из песочницы виртуальной машины. Для того, чтобы эта проверка проходила быстрее, компилятор добавляет к файлам классов дополнительную информацию. Соответственно, ProGuard должен в конце своей работы сформировать новую информацию для этой проверки. Для приложений под Android это не требуется.

Историческая справка

ProGuard - open source проект под руководством Эрика Лафортуне (Eric Lafortune), первый релиз был в 2002 году, с конца 2010 года входит в состав Android SDK.

ProGuard и Android

Точками входа в приложение являются Activity и другие компоненты, указанные в AndroidManifest.xml. Сам ProGuard про AndroidManifest.xml ничего не знает, для него в процессе сборки формируется файл конфигурации, в котором перечислены классы всех компонент. Чуть позже мы заглянем внутрь этого файла.

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

В приложении под Android пользовательский интерфейс чаще всего описывается с помощью XML. Часть классов, использующихся при описании интерфейса, нигде в коде не используется, и, если не сообщить об этих классах ProGuard’у, они будут удалены. Поэтому XML с описаниями интерфейса в процессе сборки анализируются аналогично AndroidManifest.xml, и все перечисленные там классы добавляются в файл конфигурации для ProGuard.

Текущая версия build tools помещает сформированный файл в app/build/intermediates/proguard-rules/debug/aapt_rules.txt

Вот фрагмент этого файла, в нем видно, что после анализа XML был автоматически сформирован список классов, которые необходимо сохранить:

# view res/layout/design_layout_snackbar_include.xml #generated:18
-keep class android.support.design.internal.SnackbarContentLayout { <init>(...); }
 
# view res/layout/activity_main.xml #generated:11
-keep class android.support.design.widget.AppBarLayout { <init>(...); }
 
# view AndroidManifest.xml #generated:19
-keep class ru.jollydroid.athdemo.MainActivity { <init>(...); }

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

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

Сторонние библиотеки

Самое интересное присходит, когда в приложение требуется добавить стороннюю библиотеку, которая в работе использует рефлексию. Яркие примеры — это Retrofit и ButterKnife.

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

Если любопытно, поищите файлы proguard.txt в каталоге app/build/intermediates/exploded-aar/. Вот, например:

$ find . -name proguard.txt
./app/build/intermediates/exploded-aar/com.android.support/animated-vector-drawable/25.1.0/proguard.txt
./app/build/intermediates/exploded-aar/com.android.support/design/25.1.0/proguard.txt
./app/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/25.1.0/proguard.txt
./app/build/intermediates/exploded-aar/com.android.support/support-core-ui/25.1.0/proguard.txt
./app/build/intermediates/exploded-aar/com.crashlytics.sdk.android/answers-shim/0.0.4/proguard.txt
./app/build/intermediates/exploded-aar/com.digits.sdk.android/digits/2.0.0/proguard.txt
./app/build/intermediates/exploded-aar/com.facebook.android/facebook-android-sdk/4.13.2/proguard.txt
./app/build/intermediates/exploded-aar/com.twitter.sdk.android/twitter-core/2.0.0/proguard.txt
./app/build/intermediates/exploded-aar/io.fabric.sdk.android/fabric/1.3.14/proguard.txt

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

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

Android и ProGuard

По умолчанию build.gradle приложения содержит следующий раздел:

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

proguard-android.txt — это дефолтный файл конфигурации, поставляемый с SDK, я про него уже писал. Смелые экспериментаторы, желающие поиграть с оптимизацией, могут заменить его на proguard-android-optimize.txt.

proguard-rules.pro — файл, в который предлагается писать свои директивы для ProGuard. Изначально пустой, находится в каталоге приложения.

Рецепт будет таким:

Первое — включить ProGuard не только для release, но и для debug версий. Это очень полезно, потому что поведение приложения с включенным ProGuard может отличаться от приложения с выключенным ProGuard совершенно неожиданным образом. Чем раньше все эти неожиданности будут обнаружены и исправлены, тем лучше. Не откладывайте включение ProGuard на день сборки релиза, это может стоить вам нескольких бессонных ночей.

За включение ProGuard отвечает директива minifyEnabled. Включенному ProGuard соответствует

minifyEnabled true

Второе — до тех пор, пока нет уверенности в правильности работы ProGuard, имеет смысл выключить обфускацию. Для этого добавьте в proguard-rules.pro следующую директиву:

-dontobfuscate

Третье — конфигурации для всех сторонних библиотек, которые вы взяли в android-proguard-snippets, храните в каталоге proguard внутри каталога вашего приложения. Тогда подключить сразу весь каталог можно с помощью следующей директивы в build.gradle:

proguardFiles fileTree('proguard').asList().toArray()

В приложении, над которым я сейчас работаю, эта папка выглядит так:

Screenshot 1

В итоге build.gradle получается вот такой:

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        proguardFiles fileTree('proguard').asList().toArray()
    }
    debug {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        proguardFiles fileTree('proguard').asList().toArray()
   }
}

Как увидеть результат работы ProGuard

Можно воспользоваться APK Analyzer (Build -> Analyze APK… в Android Studio). Соберите два apk с включенным и с выключенным ProGuard и сравните их. Должна быть заметна разница (не обязательно такая большая):

Screenshot 2

Если вы знаете в своем коде какие-то неиспользуемые методы или классы, то откройте apk с помощью APK Analyzer и попробуйте внутри classes.dex их найти. Если ProGuard отработал корректно, то их там не должно быть.

Другой способ. ProGuard формирует 4 текстовых файла с отчетами о своей работе. Один из них — usage.txt — содержит список классов и членов, которые были удалены. Найдите этот файл и загляните внутрь, при правильной работе он всегда не пустой. Лежат эти файлы, как правило, в каталоге app/build/outputs/mapping

Заключение

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

Ссылки