четверг, 19 июля 2012 г.

Удаляем логи из Android-приложения с помощью Proguard

Разбирали мы как-то одно приложение. Было оно обфусцировано, и чёрт бы ногу там сломал. Но помогла нам такая вещь: в коде встречались логи. Просто строчки вида Log.i(TAG, "Do something"). И по ним логику работы приложения можно было худо-бедно проследить.
Да даже если не рассматривать возможность реверс-инжиниринга, писание логов может неплохо затормозить приложение, так что в релизной версии их быть не должно. Этого можно добиться несколькими способами.

Решение в лоб

Первый и очевидный — написать свою обёртку вида:
public class Log {
    private static boolean logEnabled = true;

    public static void i(String tag, String string) {
        if (logEnabled) android.util.Log.i(tag, string);
    }
    public static void e(String tag, String string) {
        if (logEnabled) android.util.Log.e(tag, string);
    }
    public static void d(String tag, String string) {
        if (logEnabled) android.util.Log.d(tag, string);
    }
    public static void v(String tag, String string) {
        if (logEnabled) android.util.Log.v(tag, string);
    }
    public static void w(String tag, String string) {
        if (logEnabled) android.util.Log.w(tag, string);
    }
}
После этого надо не забывать перед релизом выставлять logEnabled=false, и все будет хорошо.
Способ рабочий, но есть пара минусов: во-первых, реверс-инжиниринг мы так не затрудним, а во-вторых, при каждом вызове лога будет выполняться какая-то лишняя проверка.

Решение получше

Можно дописать несколько строчек в конфиге Proguard:
-assumenosideeffects class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
}
Соответственно, если есть обертка над стандартным логом, нужно забанить и её.
И еще должна быть такая строчка, иначе ничего не выйдет:
-optimizationpasses 1
Для корректной обработки правила тут должно быть любое число не меньше 1.

P.S.

И ещё небольшой оффтопик. Когда собирается внутренняя сборка для тестеров, логи из неё вырезать не нужно. Для таких целей бывает полезно держать два конфига Proguard (скажем, proguard-test.cfg и proguard-release.cfg). А чтобы избежать полного дублирования, можно использовать такую конструкцию:
-include proguard-test.cfg

// правила, необходимые только для релиза

4 комментария:

bear комментирует...

Дарья, тут, похоже, должна использоваться одна и та же переменная:

ENABLE_LOCATION = true;
...
if (logEnabled)

Ещё вопрос: допустим, мы логируем так:
logger.d(someString+someString);

Как я понимаю, даже если logEnabled==false, конкатенация строк всё равно выполнится, т.е. это ещё небольшой overhead.

darja комментирует...

ENABLE_LOCATION случайно затесалась, она тут совершенно лишняя. Спасибо за замечание.

simplemath комментирует...

"Способ рабочий, но есть пара минусов: во-первых, реверс-инжиниринг мы так не затрудним, а во-вторых, при каждом вызове лога будет выполняться какая-то лишняя проверка."

А разве javac не удалит такие участки кода? Насколько мне известно он весьма неплохо справляется с dead-code elimination, т.о. в итоговый байткод эти if'ы просто напросто не попадут если logEnabled константно выставлен в false.

darja комментирует...

Да, может и удалит. Хоть я бы и не стала на него надеяться.