proguard / R8 を使って、デバッグ用の処理をリリースビルド時に削除する

アプリを作っていると、デバッグログを始め、デバッグのためだけに必要な処理を書くことはよくあります。
ただ、リリースビルド版にはできるだけ影響を与えないように、可能であればそれらの処理をまるっと削除したいところです。例えばログメッセージのStringが残っているだけでもアプリ全体で見ると無視でできないサイズになっている、というのも良くあることです。
release版とdebgu版でsourceSetを分けて〜という方法もなくはないですが、異なるBuildVariantに同一のコードを置いて、それぞれをメンテナンスしていくのは非常に大変です。

proguard /　R8 （以下proguard）には不要な処理を削除してくれる機能もあるので、これを使ってどんな方法があるのかを考えてみたいと思います。

# BuildConfig.DEBUGを使う

BuildConfig.DEBUGについて、デバッグビルド時には以下の様に定義されていますが

```BuildConfig.java
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  ...
}
```

リリースビルドでは以下のように定義されます。

```BuildConfig.java
public final class BuildConfig {
  public static final boolean DEBUG = false;
  ...
}
```

`BuildConfig.DEBUG` はリリースビルド時には定数であり、以下のようなif文はデッドコードとして判定されます。

```kotlin
if (BuildConfig.DEBUG) {
    ...
}
```

デッドコードは　proguard　を使わずとも、コンパイラーが削除してしまいます。
試してみましょう。

コンパイラレベルで削除されることを確認するため、 `isMinifyEnabled = false` にしておきます。

```build.gradle.kts
buildTypes {
    release {
        isMinifyEnabled = false
    }
}
```

以下のようなコードを書いて、リリースビルドします

```MainActivity.kt
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (BuildConfig.DEBUG) {
            Log.e("XXXX", "Hello, world!", Throwable())
        }
    }
}
```

これをデコンパイルすると以下のようになり、if文がまるごと削除されていますね。

```java
public final class MainActivity extends Activity {
    @Override
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
    }
}
```

`if (BuildConfig.DEBUG)` で囲んで、ここに処理を書くのが最も簡単で確実な方法ですね。

ただし、いくつか課題はあります。
マルチモジュールの場合、モジュールごとにBuildConfigを作る必要があること。（現在は　`buildFeatures.buildConfig` のデフォルト値は `false` ） モジュールの数だけ BuildConfig が作られるので、モジュールが多いと、どの BuildConfig を参照しているのか混乱してしまう可能性もありますね。
また、コンパイル時に削除されるため、ライブラリプロジェクトの場合、組み込んだアプリ側で、ライブラリのデバッグ機能を使いたい場合は、デバッグビルドしたライブラリを組み込む必要がある。ライブラリ提供側としてはデバッグ用とリリース用2種類のライブラリを提供する必要があるということになります。

# assumenosideeffects を使用する

proguard の指定の中に assumenosideeffects というものがあります。
かつてはprogaurd-rule.proのテンプレートの中でコメントアウトされていたような気がしますが、以下のように記述することでLog出力を削除するように指定することができます。

```proguard-rule.pro
-assumenosideeffects public class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
    public static *** wtf(...);
}
```

assumenosideeffects は、その名の通り「副作用が無いと見なす」という指定です。
すなわち、「戻り値を返す以外何もしないメソッド」であると見なし、戻り値が使われていない場合、そのメソッドは呼び出す必要は無いと判断され、削除されます。

試してみましょう。

```MainActivity.kt
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.e("XXXX", "Hello, world!", Throwable())
    }
}
```

これをリリースビルドし、デコンパイルしてみます。

```java
public final class MainActivity extends Activity {
    @Override
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        new Throwable();
    }
}
```

はい、`Log.e`メソッドが削除されています。
副作用が無いのは`Log.e`のみで、その引数で呼び出しているThrowableのコンストラクタは副作用があるため、それだけが残されていますね。

## assumenosideeffectsでラムダを実行するメソッドを削除する

Logをassumenosideeffectsで指定すると、確かに出力処理は削除されますが、それに渡す引数を作る処理は残ってしまいます。それも含めて削除できるようにしてみましょう。

以下のようなメソッドを考えましょう。引数でラムダを受け取り、それを実行するだけのメソッドです。
ついでなので、これをアプリとは別のモジュールで定義します。

```Debug.kt
fun debug(action: () -> Unit) {
    action()
}
```

そして、consumer-rules.proに以下のように書いてみます。
グローバルメソッドではありますが、Java風の指定をするので以下のようになります。

```consumer-rules.pro
-assumenosideeffects public class com.example.log.DebugKt {
    public static void debug(...);
}
```

MainAcitivityで以下のように使います。

```MainActivity.kt
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        debug {
            Log.e("XXXX", "Hello, world!", Throwable())
        }
    }
}
```

これをリリースビルド、デコンパイルしてみます。

```java
public final class MainActivity extends Activity {
    @Override
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
    }
}
```

ということで、ラムダの中身もろとも削除されました。`debug`メソッドが何もしないなら、どんなラムダを渡していても実行されないのだから、ラムダ自体作る必要が無いという判断ですね。

こうしておけば、ライブラリプロジェクトの場合も、コンパイル時点ではデバッグの処理を残しておき、
proguardを有効にしたリリースビルド時にデバッグ処理を削除する。ということを実現できます。
consumerProguardFilesにこういったルールを書いておけば、ライブラリ利用側が削除処理を意識する必要も無いですね。

似たようなデバッグ用のメソッド群を一ファイルでまとめている場合で、全部を削除できるようにしたい場合、以下のようにワイルドカードでまとめて指定してしまうのもありですね。

```consumer-rules.pro
-assumenosideeffects public class com.example.log.DebugKt { *; }
```

以上です。