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つ未満になることもあります
難読化後もスタックトレースの順序を保つのは困難です。
# 全てを難読化対象外とすれば可能ですが.....
以上です。