ProGuard

Budowanie

  |   6 min czytania

Wstęp

Podczas procesu budowania projektu warto zadbać o optymalizację rozmiaru pliku wyjściowego. W tym celu Android Gradle plugin wykorzystuje kompilator R8 wraz z zasadami ProGuard. Pozwala on na zmniejszanie rozmiaru poprzez wycinanie nieużywanego kodu i zasobów oraz zaciemnianie kodu dodatkowo utrudniając dekompilacje. Użycie kompilatora R8 powinno być nieodłącznym elementem budowania wersji produkcyjnej.

Pliki

Definiowanie zasad w plikach ProGuard pozwala na konfigurację i nadpisanie domyślnego zachowania kompilatora R8. Android Studio dla każdego modułu automatycznie tworzy pusty plik proguard-rules.pro w którym tworzone są zasady dla danego modułu natomiast Android Gradle plugin generuje proguard-android-optimize.txt zawierający zasady optymalizacji użyteczne dla projektów Android takie jak np. zachowanie adnotacji. Jeśli zewnętrzna biblioteka zawiera własne zasady ProGuard opisane w pliku proguard.txt i lokalizacji META-INF/proguard są one również addytywnie aplikowane dla całego projektu i nie mogą zostać usunięte co może w sposób niepożądany znacząco zmienić oczekiwaną konfiguracje. Ponadto po pozytywnym zakończeniu budowania projektu w podkatalogu wariantu budowania może zostać wygenerowany plik aapt_rules.txt zawierający zasady keep dla klas opisanych w manifeście, plikach layout i innych zasobach oznaczonych jako potencjalne punkty wejścia.

Konfiguracja

Podstawowa konfiguracja pliku build.gradle definuje zastosowanie lub pominięcie procesu zmniejszania, zaciemniania i optymalizacji zarówno dla kodu jak i zasobów za pomocą wpisów minifyEnabled, shrinkResources oraz proguardFiles dla ścieżki plików. Domyślnie są one jednak wyłączone ponieważ wydłużają czas kompilacji i mogą powodować występowanie błędów w przypadku niepełnej konfiguracji.

android {

    buildTypes {
        release {
            // Enable code shrinking, obfuscation, optimization
            minifyEnabled true

            // Enable resource shrinking, requires minifyEnabled to be applied
            shrinkResources true

            // Specify ProGuard rules files
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            // don't need to enable minifyEnabled and shrinkResources for debug purpose
        }
    }

    flavorDimensions "version"
    productFlavors {
        free {
            //...
            dimension "version"
            proguardFile 'free-rules.pro' //add extra rules for flavors
        }
        paid {
            //...
            dimension "version"
        }
    }
}

Zmniejszanie kodu

Zmniejszanie kodu (code shrinking) wykrywa i bezpiecznie usuwa nieużywane klasy, pola i metody z aplikacji oraz zewnętrznych zależności. W wyniku tego procesu plik APK zawiera tylko rzeczywiście używany kod co w niektórych sytuacjach może znacznie przyczynić się do redukcji rozmiaru. Cały proces jest jednak dość kosztowny i wydłuża czas kompilacji spowodowany analizą wszystkich punktów wejścia tzn. klas które mogą zostać użyte przez platformę do uruchomienia aktywności czy usługi. Na ich podstawie tworzony jest graf wszystkich metod, zmiennych i innych klas do których aplikacja może uzyskać dostęp w czasie działania. Kod niezawierający się w grafie jest traktowany jako nieosiągalny i może zostać usunięty z aplikacji. Przykładowo aplikacja wykorzystuje tylko kilka metod oraz klas z zewnętrznej biblioteki w związku z czym pozostała jej nieużywana część może zostać zignorowana i niedołączona do plików DEX. Punkty wejścia określane są na podstawie automatycznie generowanego pliku aapt_rules.txt oraz ręcznie opisanych zasad keep w plikach ProGuard. Przeważnie R8 usuwa tylko rzeczywiście nieużywany kod jednakże w przypadku użycia mechanizmu refleksji czy wywołań JNI (Java Native Interface) może nie być w stanie poprawnie zbudować grafu. W takim przypadku należy dodać odpowiednie zasady keep do pliku proguard-rules.pro lub oznaczyć fragmenty kodu adnotacją @Keep.

-keep class CustomClass
-keep class * implements CustomInterface
-keep public class models.** { *; }

Zmniejszanie zasobów

Podczas analizy dokonywanej przez kompilator R8 następuje także identyfikacja i oznaczanie zasobów pod kątem wykorzystania w osiągalnym kodzie. Następnie w procesie zmniejszania zasobów (resource shrinking) te nieużywane mogą zostać usunięte. Zasada ta nie dotyczy jednak zasobów alternatywnych np. inna gęstość czy język. Jeśli zasoby o tej samej nazwie, typie i kwalifikatorze występują w wielu lokalizacjach wówczas tylko jeden zostaje przekazany zgodnie z priorytetem (build type > build flavor > main > library). W przypadku budowania różnych wariantów aplikacji mogą występować różnice w rzeczywistym użyciu zasobów. W takiej sytuacji należy określić własne zasady zachowania zasobów definiowanych w pliku xml w folderze raw.

<resources 
    xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/used1, @drawable/used2"
    tools:discard="@menu/unused1" />

Optymalizacja

W trakcie procesu redukcji kodu kompilator R8 dokonuje także analizy na głębszym poziomie w celu zastosowania optymalizacji. Jeśli kod nigdy nie osiągnie pewnego warunku wówczas cała przypisana mu gałąź może zostać usunięta. Ponadto kod może być także przepisany do krótszej formy, np. metoda wywoływana w jednym miejscu jest usunięta, a jej ciało przeniesione w miejsce wywołania. Domyślne ustawienia optymalizacji zawierają są w pliku proguard-android-optimize.txt.

-optimizations !code/simplification/math
-optimizationpasses 6
-repackageclasses

Zaciemnianie

Zaciemniania (obfuscation) jest częścią procesu zmniejszania kodu. Redukuje rozmiar aplikacji oraz utrudnienia dekompilację. Polega m.in. na skracaniu nazw klas, metod i pól.

-renamesourcefileattribute SourceFile
-keepattributes SourceFile, LineNumberTable
-keeppackagenames package.name.to.keep

Inżynieria wsteczne

Inżynieria wsteczna (reverse engineering) jest procesem badania programu w celu ustalenie sposobu działania. Polega na dekompilacji kodu oraz poddawaniu go statycznej i dynamicznej analizie. W przypadku aplikacji dla systemu Android zalecane jest wykorzystanie różnych narzędzi umożliwiających częściową lub całkowitą dekompilacje. Android Studio posiada wbudowane narzędzie analizy APK, które dostarcza wielu informacji nt pliku wyjściowego w tym także pozwala na przeprowadzenie analizy zdekompilowanych zasobów oraz kodu. Ponadto wykorzystanie dex2jar wraz z jd-gui umożliwia konwersję pliku APK do JAR oraz przeglądanie zawartości plików źródłowych (klas). Pomimo zastosowania zaciemniania kodu wraz z zasadami ProGuard możliwe jest jego przekształcenie do czytelnego zapisu dzięki czemu może on stać się zrozumiały i ujawniać informacje wrażliwe takie jak autorskie algorytmy czy klucze. Dobrze zaprojektowany i zaprogramowany kod nawet w przypadku ujawnienia jego zawartości powinien minimalizować ryzyko utraty bezpieczeństwa i wycieku informacji. Zespół deweloperski jest zobowiązany zidentyfikować wrażliwe obszary aplikacji których znajomość osób trzecich może naruszać bezpieczeństwo i ujawnienie know-how oraz podjąć dodatkowe środki ostrożności. Przykładowo część kodu może być zależna od informacji ze zdalnego serwera. Alternatywnym sposobem na zaciemnienie kodu jest użycie DexGuard, który jest narzędziem zaprojektowanym dla aplikacji Android. Oferuje on znacznie większą ochronę przed statyczną i dynamiczną analize, aplikuje wiele warstw szyfrowania i zaciemniania oraz przetwarza nie tylko kod bajtowy lecz wszystkie komponenty aplikacji w tym zasoby.