Pull to refresh

Как перестать бояться Proguard и начать жить

Reading time12 min
Views58K


Здравствуйте, я Android разработчик и я больше не боюсь ProGuard...


Обычно, об этой утилите вспоминают, когда сталкиваются с dalvik dex-limit issue или с требованием улучшить безопасность приложения. К сожалению, правильно настроить Proguard удается далеко не с первого раза. Я часто наблюдал, как многие, сломав проект, выключают Proguard и включают поддержку Mulditex и каждый раз немножечко грустил по этому поводу, ведь Proguard помогает как сократить размер приложения, так и повысить его производительность.


В итоге, я решил написать статью, в которую смогу поместить всю полезную информацию, которую я узнал за несколько лет работы с Proguard и которая могла бы помочь как совсем новичкам, так и тем, кто уже что-то знает.


О чем это


Proguard — это open-source утилита для оптимизации и обфускации Java кода. Этот инструмент обрабатывает уже скомпилированный Java код, так что он должен работать с любым JVM языком. Точнее сказать, сам язык для Proguard безразличен, важен только байткод. Все манипуляции Proguard-а с байткодом можно разделить на 3 основных категории: Code shrinking, Optimisation и Obfuscation.


Code shrinking


Да, довольно странная затея писать код, а потом его удалять, но это реальность Android-разработки. Речь, конечно, не о собственноручно написанном коде (хотя и такое бывает), а о тоннах мертвого груза, который приносят библиотеки всех сортов. Guava, Apache Commons, Google Play Services и другие ребята могут раздуть размер apk файла с 500кб до пары десятков мегабайт. Порой это заходит так далеко, что программа не может скомпилироваться из-за превышения Dalvik methods limit. Proguard поможет удалить весь неиспользуемый код и сократить размер приложения обратно до нескольких мегабайт.


Optimisation


Помимо удаления ненужного кода, Proguard может оптимизировать код оставшийся. В его арсенале имеется control flow analysis, data-flow analysis, partial evaluation, static single assignment, global value numbering, liveness analysis. Proguard умеет выполнять peephole-оптимизации, уменьшать количество аллокаций переменных, упрощать хвостовые рекурсии и многое другое (wiki). Помимо таких общих операций, у Proguard есть оптимизации, полезные именно для Android-платформы, например, замена enum классов int-ами, удаление логгирующих инструкций.


Obfuscation


Напоследок, Proguard может превратить весь ваш код в нечитабельное месиво переименовав все классы, методы и поля в наборы случайных (на самом деле не совсем случайных) букв. Это очень полезная опция, так как декомпилировать ваш apk-файл может любой желающий, а разбираться в обфусцированном коде хватит терпения не у каждого.


Принцип работы


Proguard работает в 3 шага в той последовательности, которая была описана выше: code shrinkingoptimizationobfuscation. Каждый из шагов опциональный.


Шаг Optimization в случае Android SDK по умолчанию выключен.


Для работы Proguard нужно предоставить 3 компонента:


  • Ваш скомпилированный код — архив с class-файлами вашей программы и всех библиотек, которые вы используете (jar, aar, apk, war, zip и т.п.). Proguard модифицирует только уже скомпилированный код и не имеет никакого отношения к исходному.
  • Конфигурационный файл(ы) — файл(ы), содержащие все правила, опции и настройки, с которыми вы хотите запустить обработку.
  • Library jars (aar, apks, ...) — классы платформы, на которой работает ваша программа. В случае с Android это android.jar. Эти архивы нужны только для правильного анализа вашего кода, они не будут модифицированы (в этом нет смысла, т.к. android.jar находится "в телефоне", у нас нет к нему доступа).

Картинка из презентации Jeb Ware, ссылка в конце статьи
(Картинка из презентации Jeb Ware, ссылка в конце статьи)


Используя library classes и ваши конфиг-файлы Proguard определяет все точки входа в вашу программу (seeds). Другими словами, определяет те классы и методы, которые могут быть вызваны извне и которые трогать нельзя. Затем, начиная с обнаруженных seeds, Proguard рекурсивно обходит весь ваш код, помечая флажком "используемо" все, до чего смог дотянуться. Весь остальной код будет удален. В конфиге требуется указать хотя бы одну точку входа. Для стандартной java-программы это функция main. В Android нет единой точки входа в программу, вместо этого у нас есть стандартные компоненты (Activity, Service и т.п.), которые создаются и вызываются системой. К счастью, нам здесь ничего самостоятельно указывать не надо, Android SDK создаст нужный конфиг за нас.


Сопутствующие файлы


После обнаружения всех входных точек Proguard запишет их в файл seeds.txt.


Весь тот код, который Proguard посчитал ненужным, записывается в файл usage.txt. Это довольно странное название для файла, содержащего удаленный код, было бы правильнее назвать его unusage.txt, но мы имеем то, что имеем, просто помните об этом.


На шаге обфускации будет создан файл mapping.txt, содержащий пары <оригинальное имя класса|метода|поля> -> <обфусцированное имя класса|метода|поля>. Этот файл пригодится тогда, когда потребуется деобфусцировать программу, например, прочитать stacktrace. Вручную маппить файлы обратно не требуется, в Android SDK есть утилиты retrace и proguardui, которые помогут. Более того, если вы используете Fabric Crashlytics, то их gradle plugin умеет самостоятельно находить и загружать этот файл в консоль, так что вам не надо беспокоится об этом.


В случае с Android, эти файлы обычно находятся в app/build/output/mapping/<product-flavor-name>/.


Еще Proguard создает файл dump.txt, который содержит все то, что Proguard положил в итоговый архив. Мне он никогда не пригождался, но, возможно, кому-то он будет полезен.


Как дела обстоят в Android


Android Gradle Plugin умеет запускать Proguard самостоятельно. Все, что вам нужно сделать, это включить эту опцию и указать конфиг-файлы.


buildTypes {

    <...>

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

minifyEnabled true — включаем Proguard на этапе сборки


proguardFiles — список конфиг-файлов. Правила из всех конфиг-файлов будут добавлены в общий список в порядке их появления.


proguard-rules.pro — это наш конфиг-файл со специфичными для конкретного проекта правилами


getDefaultProguardFile('proguard-android.txt') — функция, возвращающая стандартный конфиг-файл для Android-приложений. Он лежит в AndroidSDK/tools/proguard


На самом деле, в Android SDK есть два конфига: proguard-android.txt и proguard-android-optimize.txt. В первом из них есть опция -dontoptimize, которая выключает все оптимизации. Если хотите включить оптимизацию — используйте второй конфиг.


Помимо этих стандартных конфигов Android SDK (aapt) автоматически генерирует набор правил для ресурсов: aapt проверяет все xml файлы (включая манифест), чтобы найти все активити, сервисы, вьюшки и т.п. и сгенерировать для них нужные правила. Сгенерированные правила можно найти в app/build/intermediates/proguard-rules/<flavor>/aapt_rules.txt. Вам не нужно его указывать самостоятельно, Android Gradle Plugin добавит эти правила автоматически.


Картинка из презентации Jeb Ware, ссылка в конце статьи
(Картинка из презентации Jeb Ware, ссылка в конце статьи)


Конфиги


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


Среди всех опций я бы выделил 3 наиболее важных группы:


  • keep rules — Все возможные точки входа в программу. Правила говорящие Proguard-у, какие классы или части классов нужно сохранить без изменения или какие из модификаций допустимы для конкретных классов.
  • optimisation tuning — указывают какие оптимизации допустимы, сколько циклов оптимизации нужно сделать.
  • работа с предупреждениями, ошибками и debugging

Keep rules


Это набор опций, предназначенных для того, чтобы защитить ваш код от беспощадного Proguard. В самом общем виде такое правило выглядит так:


-keep [,modifier,...] class_specification

keep — самая общая из таких опций (есть и другие), говорящая Proguard сохранить сам класс и все его составляющие(class members): поля и методы.


class_specification — шаблон, указывающий на класс(ы) или его части (class members). Общий вид шаблона очень большой, его можно посмотреть в офф. документации. Можно к нему обращаться, однако в целом, можно просто помнить, что у нас есть возможность составить шаблон из таких составляющих:


  • выбрать все классы с определенным именем, пакетом
  • выбрать все классы, наследующие/реализующие определенные классы/интерфейсы
  • выбрать все классы с определенными модификаторами и/или определенными аннотациями
  • выбрать все методы с определенным именем, модификаторами, аргументами и возвращаемым значением
  • выбрать все поля определенным именем, модификаторами, определенного типа.
  • есть возможность использовать wildcards


    Еще раз, это не строгое описание шаблона, это скорее список возможностей, которые у нас есть. А вот несколько примеров:



-keep public class com.example.MyActivity
сохранить класс com.example.MyActivity


-keep public class * extends android.app.Activity
сохранить все публичные классы, наследующие android.app.Activity


-keep public class * extends android.view.View { 
      public <init>(android.content.Context); 
      public <init>(android.content.Context, android.util.AttributeSet); 
      public <init>(android.content.Context, android.util.AttributeSet, int); 
      public void set*(...); 
} 

найти все публичные классы, наследующие android.view.View и сохранить в них 3 конструктора с определенными параметрами + все публичные методы, с модификатором void, любыми аргументами и именем, начинающимся на set. Все остальные части класса могут быть модифицированы.


-keep class com.habr.** { *; }
сохранить все классы и все их содержимое в пакете com.habr


modifiers — дополнение к keep-правилу:


  • includedescriptorclasses — помимо указанного класса/метода/поля нужно сохранить все классы, встречающиеся в их дескрипторах.
  • includecode — содержимое метода, на который указывает это конкретное правило, тоже трогать нельзя.
  • allowshrinking — классы, на которые указывает это правило, не являются входными точками (seeds) и их можно удалять, но только если они не используются в самой программе. Однако, если после codeshrinking этот код остался (по причине того, что его кто-то использует), оптимизировать/обфусцировать этот код нельзя.
  • allowoptimization — классы, на которые указывает это правило, можно только оптимизировать, но нельзя удалять или обфусцировать.
  • allowobfuscation — классы, на которые указывает это правило, можно только обфусцировать, но нельзя удалять или оптимизировать.

Помимо keep, есть еще несколько опций:


-keepclassmembers — указывает, что нужно сохранить class members, если сам класс сохранился после code shrinking.


-keepclasseswithmembers — указывает, что нужно сохранит классы, содержимое которых попадает под указанный шаблон. Например, -keepclasseswithmembers class * { public <init>(android.content.Context); } — сохранит все классы, у которых есть публичный конструктор с одним аргументом типа Context.


-keepnames — сокращение для -keep,allowshrinking.


-keepclassmembernames — сокращение для -keepclassmembers,allowshrinking.


keepclasseswithmembernames — сокращение для -keepclasseswithmembers,allowshrinking.


Optimisation tuning


Самой главной опцией здесь является флаг -dontoptimize. Если он присутствует, ни одна оптимизация не будет выполнена и все остальные опции оптимизации будут проигнорированны.


Опций оптимизаций много, но самыми полезными мне кажутся следующие:


-optimizations optimization_filter — перечисление всех способов, которые вы хотите использовать. Лучше использовать тот набор, который указан в proguard-android-optimize.txt или его подмножество. Список всех оптимизаций можно найти тут.


-optimizationpasses n — количество циклов оптимизации. Несколько циклов могут улучшить результат. При этом Proguard достаточно умный, чтобы прекратить циклы, если увидит, что результат не улучшился с прошлого раза.


-assumenosideeffects class_specification — указывает, что данный метод не имеет сайд-эффектов и только возвращает какое-то значение. Proguard удалит вызовы этого метода, если обнаружит, что возвращаемый им результат не используется. Пожалуй, самое распространенное применение этой опции — удаление всех отладочных логов: -assumenosideeffects class android.util.Log { public static int d(...); }


-allowaccessmodification — показать все, что скрыто :) Отличная опция, позволяющая избаиться от кучи искусственных accessor-методов для вложенных классов. Работает только в паре с -repackageclasses


-repackageclasses — разрешает переместить все классы в один указанный пакет. Это больше относится к обфускации, но в то же время, дает хорошие результаты в оптимизации.


Прочие полезные опции


-dontwarn и -dontnote


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


Например, бывает, что какая-то java-бибилиотека использует platform-классы, которых нет в android.jar и Proguard предупредит об этом. Если вы уверены, что эта библиотека работает нормально в Android-окружении, вы можете отключить это предупреждение -dontwarn java.lang.management.**


-whyareyoukeeping class_specification — полезная опция, которая напечатает причину, по которой Proguard решил не трогать этот класс/метод.


-verbose — печатать более подробные логи и исключения


-printconfiguration — напечатать полный список опций из всех конфиг-файлов, которые были использованны, включая правила из библиотек и сгенерированные через aapt.


-keepattributes SourceFile, LineNumberTable — сохраняет мета-информацию (имена файлов, нумерацию строк), что бы иметь возможность отлаживать код в IDE и получать осмысленный stacktrace. Обязательно добавляйте эту опцию.


Практика


Обычно бывает так: включаешь Proguard и он ломает тебе весь проект выдавая тонну ошибок. Многие на этом шаге выключают Proguard и стараются к нему не возвращаться. Я попробую дать несколько советов, что бы этот процесс перехода был попроще.


Определиться с начальными входными точками


Если вы Android-разработчик, тут все предельно элементарно — просто выберите один из двух стандартных конфигов из Android SDK: proguard-android.txt или proguard-android-optimize.txt, они позаботятся о всем, что должно остаться нетронутым.


Проверить все библиотеки


В последнее время все больше и больше библиотек распространяются уже с готовыми proguard-конфигами. Proguard умеет заглядывать внутрь архива, находить конфиг библиотеки и добавлять его к остальным опциям. Проверьте каждую библиотеку, которую вы используете на наличие такого конфига.


(содержимое aar-файла одной из библиотек)
(содержимое aar-файла одной из библиотек)


Если вы используете Google Play Services, то плагин com.google.gms.google-services подберет нужный вам конфиг самостоятельно.


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


Если готовых правил нигде не удалось найти, придется читать логи и решать проблему индивидуально. Скорее всего, потребуется добавил keep правил для того кода библиотеки, который сломался. Или проигнорировать ошибки, если они не мешают работе программы.


Провести инспекцию своего кода


Вам виднее, какой код можно отправить под нож, но стоит внимательно взглянуть на все места, где так или иначе используется рефлексия:


  • Class.forName(...) (документация обещает, что Proguard умеет определять такой код, однако, бывали случаи, стоит проверить)
  • Модельки/entity-классы, которые используются в серелизации, маппинге. Все классы, имена полей (иногда и самих классов) которых важно сохранить (Gson, RealmIO, т.п.)
  • вызовы нативных библиотек через JNI

Тесты


Если какой-то класс/метод используется только в тестах и нигде больше, Proguard удалит этот код. Это обычная ситуация, если у вас TDD :) На этот случай у меня есть отдельный конфиг, куда я добавляю классы, которые еще не интегрированы в проект, нигде не используются, но которые нужно протестировать.


В Android Gradle Plugin помимо proguardFiles инструкции еще есть testProguardFiles. Эта инструкция нужна для того, чтобы указать конфиги, которые будут применяться к тестовому приложению, которое генерируется для того, чтобы тестировать ваше приложение, когда вы запускаете instrumentation тесты. Обычно это применяется для того, чтобы добиться одинаковой оптимизации/обфускации в обоих apk-файлах, чтобы между ними не было рассинхронизации. Ссылка.


APK Analyzer


В Android Studio есть такой отличный инструмент. Открыть его можно или через Find Action -> Analyze APK, или открыв сам apk файл в Android Studio. Analyzer показывает много полезной информации, но сейчас нас интересует код. Что бы посмотреть что в итоге упаковалось в APK файл, нужно выбрать файл classes.dex



По умолчанию, вам будет показан именно результирующий код, прошедший шаги shrinking и optimisation. Однако, вы можете нажать на кнопку Load Proguard mappings..., добавить seeds.txt и usage.txt, чтобы увидеть код, который был удален.



Если Proguard по какой-то причине модифицировал нужный вам код, найдите его в Analyzer и через ПКМ выберите Generate Proguard Keep Rule. Analyzer сгенерирует вам на выбор несколько вариантов правил, от самого общего до самого специфичного, выберите ОДИН из них.




Для авторов библиотек


Если вы делаете Android библиотеку, вы можете добавить proguard-конфиг для своих клиентов следующим образом:


buildTypes {
    release {
        consumerProguardFiles 'proguard-rules.pro'
    }
}

На мой взгляд, лучше не усердствовать с оптимизацией и обфускацией своей библиотеки, а предоставить эту возможность своим клиентам. Хороший тон — добавить в конфиг то, что клиентам все равно придется добавить, если они включают Proguard. Однако, если вы все-таки хотите добавить безопасности, очевидно, что нужно защитить от Proguard-а весь pulic API своей библиотеки, включая дескрипторы и сигнатуры.


R8, DexGuard и Redex


R8 — это новый инструмент от Google взамену нынешнему Proguard. Подождите, не пытайтесь забыть все, что только что прочитали в статье, просто рассматривайте это как новый Proguard. Google обещает сохранить весь public api, так что все конфиги будут работать как прежде. Проект пока в стадии beta, но вы можете попробовать его самостоятельно.


DexGuard — это платная утилита от разработчиков Proguard. Ее можно использовать вместе или вместо Proguard. Утверждается, что DexGuard умеет все, что умеет Proguard, но лучше. К сожалению, у меня не было шанса его попробовать, если у кого-то есть опыт, пожалуйста, поделитесь.


Redex — еще один dex оптимизатор от Facebook. Сообщается, что с помощью него можно добиться до 25% увеличения производительности и сокращения размера приложения, применив тулзу на уже обработанный Proguard-ом код.


Вместо заключения


Не бойтесь использовать Proguard, не поленитесь и потратьте какое-то время на настройку. Этим вы уменьшите его размер, увеличите скорость работы, чем добавите лояльности ваших юзеров. При этом старайтесь создавать эффективные Proguard-конфиги, не пишите "ковровые"" правила, иначе к вам придет разгневанный Jake Wharton и будет вас ругать.



Ресурсы


Сайт Proguard. Там же есть информация про DexGuard.
Различные примеры правил
R8
Запись презентации How Proguard Works с DroidCon
Запись презентации Effective ProGuard keep rules for smaller applications (Google I/O '18)
Инструкция по включению и настройке Proguard для Android
Страничка на Wiki
Redex

Tags:
Hubs:
Total votes 18: ↑18 and ↓0+18
Comments12

Articles