Androidアプリの圧縮、難読化、最適化【R8】
つい後回しにしてきたので、正しく理解するために
公式ユーザーガイドを元に要点をまとめたいと思います。
R8の有効化
R8はAndroid Gradleプラグイン3.4.0以降を利用していれば自動で適用される。
Android Studio 3.3 ベータ版, Android Studio 3.3.1 では
gradle.propertiesに以下の設定を加えると適用される。
android.enableR8=true
要は何もしなくても勝手に適用してくれるようですね。
ほとんどの場合gradle.propertiesを変更する必要はなさそうです。
ちなみにR8が有効の状態でuseProguardを有効のままにしている場合はビルド時に以下のような警告が表示されます。
DSL element 'useProguard' is obsolete and will be removed soon. Use 'android.enableR8' in gradle.properties to switch between R8 and Proguard..
これを回避するにはbuild.gradleでuseProguardの設定を削除します。
なにができるのか
コードの圧縮
コード圧縮を有効化するにはApplicationレベルのbuild.gradleで以下のように設定します。
buildTypes {
release {
minifyEnabled true
...
}
}
圧縮されるソースコードやその方法について
要約するとこのように記されています。
エントリポイントからアプリのコードを探索しグラフを作成し、到達不能と判断したソースコードを削除する
つまり、永遠に実行されることのない次のようなソースコードは削除されます。
val test = "" // 使用していない変数は削除される
if(true) {
hoge()
} else {
//このelse文は削除される
fuga()
}
//このメソッドは削除される
fun fuga() {
test = "fugafuga"
}
使用しているライブラリにも適用されるようです。
また、次のような実装については誤って削除されることがあるようです。
- アプリが Java Native Interface(JNI)からメソッドを呼び出す場合
- アプリが実行時にコードを検索する場合(リフレクションの使用時など)
JNIを利用しているプロジェクトでは注意が必要です。
リフレクションについてはあまり使用する機会はなさそうですがこちらの投稿がわかりやすかったです。
さらに削除したソースコードの出力もできるようです。
proguard-rules.proに以下を追加します。
-printconfiguration /tmp/full-r8-config.txt
-printusage /tmp/usage.txt
この通りに設定を追加するとビルドしたマシンのルート
/tmpにレポートが出力されます。
リソースファイルの圧縮
リソースファイルの圧縮を有効にするにはbuild.gradleで以下のように設定します。
buildTypes {
release {
shrinkResources true
}
}
こうすることで参照されることのないリソースを削除します。
ただし、安全第一設計となっており、リソースへのパスなどが近似するファイルは削除しないことがあるようです。
この辺りはどういう仕組みか記載されていませんでしたが、あまり気にすることはないかなと思います。
圧縮したくないコードをカスタマイズする
これは従来のproguard-rules.proを使ってフィルターを設定する方法と保持するコードに @Keepアノテーションを追加する方法があります。
フィルターを設定するにはproguard-rules.proに以下のように設定します。
-keep public class MyClass
-keep class androidx.** { *; }
#このようにパッケージごとまとめてフィルターすることもできる
@Keepを使う方法ですが、可読性やデバッグのしやすさを考えて個人的にははこちらをお勧めします。
クラスごと全部フィルターの対象にする場合以下のようにします。
@Keep
class HogeFuga { //class名などが保持される
val test = "" //削除されない
if(true) {
hoge()
} else {
//削除されない
fuga()
}
//削除されない
fun fuga() {
test = "fugafuga"
}
//削除されない
fun hige() {
}
}
また、メソッドごとに指定も可能です。
class HogeFuga { //class名などが保持される
val test = "" //削除されない
if(true) {
hoge()
} else {
//削除されない
fuga()
}
@Keep
fun fuga() { //削除されない
test = "fugafuga"
}
//削除される
fun hige() {
}
}
難読化について
クラス名やメソッド名を短縮して、クラッシュログなどからの実装の予測されることを防いだり、容量の削減を目的としています。
ただし公式ユーザーガイドに目的は「容量の削減」としか記載されていないので、ハッキングやデコンパイルを防ぐ手段にはなり得ないのかもしれません。
どのように短縮されるかというと下記のようになります。
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
ただし難読化をするとクラッシュレポートなどからのデバッグが困難になるので
<module- name>/build/outputs/mapping/<build-type>/mapping.txt に
次のように変更前のクラス名と変更後のクラス名を出力してくれます。
retrofit2.http.QueryMap -> retrofit2.x.t:
retrofit2.http.QueryName -> retrofit2.x.u:
retrofit2.http.Streaming -> retrofit2.x.v:
難読化の注意点
難読化では実際に実行するコードの名前を変更するので、Gsonなどを利用してクラス名やフィールドの名前をそのまま利用している場合は、ビルドに成功するが、APIとの通信に失敗するといった不具合が起こりがちです。
そういった不具合は利用するモデルクラスに前述した@Keepアノテーションを使って難読化をやめるか
@SerializedName("name")アノテーションを使うことで対応が可能です。
コードの最適化
これは実はコードの圧縮とほぼ同義のようです。
前述したelse分の削除などはこちらの領域のようですが、あまり違いがわかりません。
gradle.propertiesに以下の設定を追加することでより高度な最適化を行ってくれるようです。
android.enableR8.fullMode=true
ただこれがデフォルトで有効になっていない理由を考えるとこの機能の使用は慎重になった方が良いかと思います。