アプリを作っていると、デバッグログを始め、デバッグのためだけに必要な処理を書くことはよくあります。
ただ、リリースビルド版にはできるだけ影響を与えないように、可能であればそれらの処理をまるっと削除したいところです。例えばログメッセージのStringが残っているだけでもアプリ全体で見ると無視でできないサイズになっている、というのも良くあることです。
release版とdebgu版でsourceSetを分けて〜という方法もなくはないですが、異なるBuildVariantに同一のコードを置いて、それぞれをメンテナンスしていくのは非常に大変です。
proguard / R8 (以下proguard)には不要な処理を削除してくれる機能もあるので、これを使ってどんな方法があるのかを考えてみたいと思います。
BuildConfig.DEBUGを使う
BuildConfig.DEBUGについて、デバッグビルド時には以下の様に定義されていますが
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
...
}
リリースビルドでは以下のように定義されます。
public final class BuildConfig {
public static final boolean DEBUG = false;
...
}
BuildConfig.DEBUG
はリリースビルド時には定数であり、以下のようなif文はデッドコードとして判定されます。
if (BuildConfig.DEBUG) {
...
}
デッドコードは proguard を使わずとも、コンパイラーが削除してしまいます。
試してみましょう。
コンパイラレベルで削除されることを確認するため、 isMinifyEnabled = false
にしておきます。
buildTypes {
release {
isMinifyEnabled = false
}
}
以下のようなコードを書いて、リリースビルドします
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.DEBUG) {
Log.e("XXXX", "Hello, world!", Throwable())
}
}
}
これをデコンパイルすると以下のようになり、if文がまるごと削除されていますね。
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出力を削除するように指定することができます。
-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 は、その名の通り「副作用が無いと見なす」という指定です。
すなわち、「戻り値を返す以外何もしないメソッド」であると見なし、戻り値が使われていない場合、そのメソッドは呼び出す必要は無いと判断され、削除されます。
試してみましょう。
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e("XXXX", "Hello, world!", Throwable())
}
}
これをリリースビルドし、デコンパイルしてみます。
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で指定すると、確かに出力処理は削除されますが、それに渡す引数を作る処理は残ってしまいます。それも含めて削除できるようにしてみましょう。
以下のようなメソッドを考えましょう。引数でラムダを受け取り、それを実行するだけのメソッドです。
ついでなので、これをアプリとは別のモジュールで定義します。
fun debug(action: () -> Unit) {
action()
}
そして、consumer-rules.proに以下のように書いてみます。
グローバルメソッドではありますが、Java風の指定をするので以下のようになります。
-assumenosideeffects public class com.example.log.DebugKt {
public static void debug(...);
}
MainAcitivityで以下のように使います。
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
debug {
Log.e("XXXX", "Hello, world!", Throwable())
}
}
}
これをリリースビルド、デコンパイルしてみます。
public final class MainActivity extends Activity {
@Override
public final void onCreate(Bundle bundle) {
super.onCreate(bundle);
}
}
ということで、ラムダの中身もろとも削除されました。debug
メソッドが何もしないなら、どんなラムダを渡していても実行されないのだから、ラムダ自体作る必要が無いという判断ですね。
こうしておけば、ライブラリプロジェクトの場合も、コンパイル時点ではデバッグの処理を残しておき、
proguardを有効にしたリリースビルド時にデバッグ処理を削除する。ということを実現できます。
consumerProguardFilesにこういったルールを書いておけば、ライブラリ利用側が削除処理を意識する必要も無いですね。
似たようなデバッグ用のメソッド群を一ファイルでまとめている場合で、全部を削除できるようにしたい場合、以下のようにワイルドカードでまとめて指定してしまうのもありですね。
-assumenosideeffects public class com.example.log.DebugKt { *; }
以上です。