2012/03/09

Android:ログコードをProguardで削除する


Android標準で用意されているandroid.util.Logクラスは便利なロギング機能を提供して
くれますが、ログ出力の度にTAGを指定するのは面倒です。
また、アプリ内でフォーマットを統一したい場合はLogクラスをラップする独自のロガー
を用意することはよくあると思います。

今回下記のようなロガーを作成しました。
特徴は、オプションで出力フォーマットにファイル名と行数を含めることができます。
# 出力される情報はロガーの呼出し元となります。
# 情報はスタックトレースから取り出すので、コールスタックを意識する必要があります

package yuki.proguard;

import android.util.Log;

public abstract class Logger {
    private static final String LOG_TAG = "YourAppName";

    private static boolean DUMP_CALLER_INFO = true;
    // Log format: <log message> @<file name>:<line number>
    private static final String CALLER_INFO_FORMAT = " @%s:%s";

    public static void v(String log) {
        if (!isLoggable(Log.VERBOSE)) {
            return;
        }
        printLog(Log.VERBOSE, log, null);
    }

    public static void d(String log) {
        if (!isLoggable(Log.DEBUG)) {
            return;
        }
        printLog(Log.DEBUG, log, null);
    }

    public static void i(String log) {
        if (!isLoggable(Log.INFO)) {
            return;
        }
        printLog(Log.INFO, log, null);
    }

    public static void w(String log) {
        if (!isLoggable(Log.WARN)) {
            return;
        }
        printLog(Log.WARN, log, null);
       
    }

    public static void w(String log, Throwable err) {
        if (!isLoggable(Log.WARN)) {
            return;
        }
        printLog(Log.WARN, log, err);
       
    }

    public static void e(String log) {
        if (!isLoggable(Log.ERROR)) {
            return;
        }
        printLog(Log.ERROR, log, null);
       
    }

    public static void e(String log, Throwable err) {
        if (!isLoggable(Log.ERROR)) {
            return;
        }
        printLog(Log.ERROR, log, err);
       
    }

    private static boolean isLoggable(int level) {
        return Log.isLoggable(LOG_TAG, level);
    }

    private static void printLog(int level, String log, Throwable err) {
        StringBuilder msg = new StringBuilder(50);
        msg.append(log)
            .append(getErrorStackTrace(err))
            .append(getCallerInfo());

        Log.println(level, LOG_TAG, msg.toString());
    }

    private static String getErrorStackTrace(Throwable err) {
        if (err == null) {
            return "";
        }

        StringBuilder msg = new StringBuilder(400);
        msg.append("\n");
        msg.append(Log.getStackTraceString(err));
        return msg.toString();
    }

    private static String getCallerInfo() {
        if (DUMP_CALLER_INFO) {
            return "";
        }

        StackTraceElement[] stacks = new Throwable().getStackTrace();
        if (stacks == null || stacks.length < 4) {
            // スタックトレースの4番目を呼出し元情報として扱います。
            //   ex) Calling stack is..
            //   stack[0] : Logger.getCallerInfo()
            //   stack[1] : Logger.printLog()
            //   stack[2] : Logger.i()
            //   stack[3] : CallerClass.xxx()
            // stack[3]にあたるクラスが呼出し元情報として出力されます。
            return "";
        }

        StackTraceElement stack = stacks[3];
        return String.format(CALLER_INFO_FORMAT,
                stack.getFileName(), stack.getLineNumber());
    }
}

利用する側は下記のようなコードで実行されます。

public class ProguardTestActivity extends Activity {
// ....
        Logger.d("Debug dump Test");
        Logger.v("Debug dump Test");
// ....
}

これを実行すると、下記のログが出力されます。

Debug dump Test @ProguardTestActivity:3
Debug dump Test @ProguardTestActivity:4


●ログの抱える問題と対応策
ログはデバッグ時には非常に重要な情報ですので、出力の内容は正確かつ価値のあるもの
である必要がありますが、下記のような問題が付きまといます。
  • ログ情報からアプリの秘匿情報が知られる恐れがある
  • ログ出力が原因でパフォーマンスが低下する

開発者に価値のあるログ情報は、悪意のあるユーザにとっても価値ある情報になるケース
があります。

Android初期バージョンでは、電話番号がログ情報に流れる問題があり、悪意のあるアプリ
がログから電話番号を盗むことも可能でした。(現在、この問題は修正されています)
また、Logcatのストリームを監視することでユーザプライバシーに関わる情報も盗めて
しまう可能性があります。
# クレジットカード番号や氏名・体重・年齢等をログ出力するのは避けましょう

また、ログの出力はアプリパフォーマンスにも影響します。
例えば、上記のLoggerクラスでログ出力を抑止(isLoggable==false)しても、ログ情報の
文字列は生成されるため、不要なオーバーヘッドが生まれます。

これら2つの問題を解決するにはProguardを利用するのが便利です。

今回の例ではproguard.cfgに下記定義を追加します。

-assumenosideeffects class yuki.proguard.Logger {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
}

これで、Loggerのv,d,iメソッドはProguard処理で削除されるようになります。
ログ情報を生成する為の文字列も生成されないため、不要なオーバーヘッドが発生する
心配もありません。

実際にProguard適用後のapkを逆コンパイルしてみましょう。

【適用前コード】
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Logger.d("Debug dump Test");
    Logger.v("Debug dump Test  ");
}


【適用後コード】
public void onCreate(Bundle bundle)
{
    super.onCreate(bundle);
    setContentView(0x7f030000);
}

見事にログコードが削除されています。


●Proguardの副作用
Proguardには副作用もあります。
今回の例ですと、難読化によりスタックトレース情報が変化し、正しく呼出し元情報が出
力されなくなります。
これは難読化でメソッドがインライン展開されて、スタックトレースの4番目が呼出し元
クラスであることが保証されなくなるためです。
# そもそもスタック要素が4つ未満になることもあります

難読化後もスタックトレースの順序を保つのは困難です。
# 全てを難読化対象外とすれば可能ですが.....

以上です。