如何在构建 Android 应用程序的发布版本之前删除所有调试日志调用?

447

根据谷歌的要求,在将我的 Android 应用程序发布到 Google Play 之前,我必须 "在源代码中停用任何对Log方法的调用"。这是来自发布检查清单第3节的摘录:

在构建应用程序以发布时,请确保停用日志记录并禁用调试选项。您可以通过从源文件中删除对 Log 方法的调用来停用日志记录。

我的开源项目很大,每次发布都手动完成这个任务很麻烦。此外,删除日志行可能有潜在的麻烦,例如:

if(condition)
  Log.d(LOG_TAG, "Something");
data.load();
data.show();
如果我注释掉Log行,则条件将应用于下一行,load()可能不会被调用。这种情况很少见吗,我可以决定它不应存在吗?
那么,有没有更好的源代码级别的方法来做到这一点?或者也许有一些聪明的ProGuard语法可以有效但安全地删除所有Log行吗?

2
+1 是因为我没有记得这个在出版清单中。 - rds
54
为了将一行未被阻止的代码注释掉,我使用";//"而不是"//"。 - yingted
2
Dimitar添加的链接已经失效了。我找到了这个链接代替:http://source.android.com/source/code-style.html#log-sparingly。 - JosephL
这就是为什么不建议在没有{}的情况下使用if语句,特别是当您将表达式移到下一行时;请使用Sonar Luke。 - Marian Paździoch
1
@mboy:现在主要是为了性能,但在旧版Android上也有安全方面的好处。 - Nicolas Raoul
显示剩余3条评论
31个回答

533

我发现一种更简单的解决方法是忘记所有的if检查,并只需在调用Ant的release目标时,使用ProGuard来删除任何Log.d()Log.v()方法调用。

这样,我们始终可以在常规构建中输出调试信息,而不必为发布版本进行任何代码更改。ProGuard还可以对字节码进行多次传递,以删除其他不需要的语句、空块,并在适当的情况下自动内联短方法。

例如,这是一个非常基本的Android ProGuard配置:

-dontskipnonpubliclibraryclasses
-dontobfuscate
-forceprocessing
-optimizationpasses 5

-keep class * extends android.app.Activity
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
}

你需要将其保存到一个文件中,然后从Ant中调用ProGuard,传入刚编译的JAR和你正在使用的Android平台JAR。

另请参见ProGuard手册中的示例


更新(4.5年后):现在我使用Timber进行Android日志记录。

它不仅比默认的Log实现更好——日志标签会自动设置,并且很容易记录格式化字符串和异常,而且还可以在运行时指定不同的日志记录行为。

在此示例中,日志记录语句仅在我的应用程序的调试版本中写入logcat:

Timber在我的ApplicationonCreate()方法中设置:

if (BuildConfig.DEBUG) {
  Timber.plant(new Timber.DebugTree());
}

那么在我的代码的任何其他地方,我都可以轻松记录日志:

Timber.d("Downloading URL: %s", url);
try {
  // ...
} catch (IOException ioe) {
  Timber.e(ioe, "Bad things happened!");
}

查看Timber示例应用,以获取更高级的示例。在开发过程中将所有日志语句发送到日志记录器,在生产环境中不记录调试语句,但静默报告错误至Crashlytics。


74
为什么默认的Proguard文件中没有包含这个? - rds
13
由于一些行会被移除,因此使用+rds将导致生产堆栈跟踪的行号与代码中的行号不同。请注意,在翻译过程中不要改变原文的含义。 - Guy
6
我可以确认,删除Log调用将导致堆栈跟踪中的行号变化。它不总是不同步的(我进行了几次快速测试,但无法确定原因,可能是如果在Log调用中连接一个字符串),但有时会有几行偏差。为了能够轻松地删除Log调用,这很值得费些功夫。 - Tony Chan
5
从ADT工具中的proguard-android.txt文件中可以看到,如果您希望启用优化功能,就不能仅在自己的项目配置文件中包含优化标志。相反,您需要在项目属性文件中指向"proguard-android-optimize.txt"文件,而不是这个文件。 - Raanan
4
正如espinchi在下面的回答中所说:“这种方法唯一的问题是,即使在发布版本中未显示此日志消息,如果执行Log.d("tag", "Processed: " + new ItemCounter(blabla) + " items "),仍会使用StringBuilder来创建消息,这可能会很昂贵。” 在Timber中也是如此吗? - Chitrang
显示剩余30条评论

133

所有答案都不错,但当我完成开发后,我不想在所有Log调用周围使用if语句,也不想使用外部工具。

因此,我使用的解决方案是,将android.util.Log类替换为我的自定义Log类:

public class Log {
    static final boolean LOG = BuildConfig.DEBUG;

    public static void i(String tag, String string) {
        if (LOG) android.util.Log.i(tag, string);
    }
    public static void e(String tag, String string) {
        if (LOG) android.util.Log.e(tag, string);
    }
    public static void d(String tag, String string) {
        if (LOG) android.util.Log.d(tag, string);
    }
    public static void v(String tag, String string) {
        if (LOG) android.util.Log.v(tag, string);
    }
    public static void w(String tag, String string) {
        if (LOG) android.util.Log.w(tag, string);
    }
}

我需要在所有源文件中替换android.util.Log的导入为我的自定义类。


150
这种方法的唯一问题在于,即使在您发布的版本中没有显示此日志消息,如果您执行Log.d("tag", "Processed: " + new ItemCounter(blabla) + " items "),仍会使用StringBuilder创建消息,这可能很耗费资源。 - espinchi
10
这个解决方案存在一个大问题。espinchi只是提到了冰山一角。问题在于当你调用Log.d("tag", someValue.toString());时很容易忘记检查someValue是否为null,这意味着它可能会在生产环境中抛出NullPointerException。它建议一个安全的解决方案,但这会欺骗你。我们使用一个private static boolean DEBUG,然后if(DEBUG)Log.d(TAG, msg); - philipp
2
@espinchi,您的关注似乎适用于所有日志记录库,如此答案所述https://dev59.com/vXI-5IYBdhLWcg3wF0Qc#15452492(Slf4j,backlog,...)。不建议使用它们吗? - OneWorld
1
唯一减少@espinchi在第一条评论中提到的开销的方法是更改日志记录方法以接受varargs而不是String。完整的解决方案在这里中描述。显然,这还有另一个缺点:每个调用都应该被编辑(不仅仅是一个导入行)。 - Stan
22
提醒一下,如果你使用的是Android Studio和gradle构建系统,你可以使用static final boolean LOG = BuildConfig.DEBUG,而无需修改这个文件。 - ashishduh
显示剩余6条评论

62
我建议在某个地方设置一个静态布尔值来指示是否记录日志:
class MyDebug {
  static final boolean LOG = true;
}
然后,在你的代码中想要记录日志的地方,只需这样做:
if (MyDebug.LOG) {
  if (condition) Log.i(...);
}
现在,当你将 MyDebug.LOG 设置为 false 时,编译器将删除所有此类检查内部的代码(由于它是静态和最终的,所以编译器在编译时知道该代码不会被使用)。
对于较大的项目,您可能希望在各个文件中具有布尔值,以便根据需要轻松启用或禁用日志记录。例如,这些是窗口管理器中各种记录常量:
static final String TAG = "WindowManager";
static final boolean DEBUG = false;
static final boolean DEBUG_FOCUS = false;
static final boolean DEBUG_ANIM = false;
static final boolean DEBUG_LAYOUT = false;
static final boolean DEBUG_RESIZE = false;
static final boolean DEBUG_LAYERS = false;
static final boolean DEBUG_INPUT = false;
static final boolean DEBUG_INPUT_METHOD = false;
static final boolean DEBUG_VISIBILITY = false;
static final boolean DEBUG_WINDOW_MOVEMENT = false;
static final boolean DEBUG_ORIENTATION = false;
static final boolean DEBUG_APP_TRANSITIONS = false;
static final boolean DEBUG_STARTING_WINDOW = false;
static final boolean DEBUG_REORDER = false;
static final boolean DEBUG_WALLPAPER = false;
static final boolean SHOW_TRANSACTIONS = false;
static final boolean HIDE_STACK_CRAWLS = true;
static final boolean MEASURE_LATENCY = false;

使用相应的代码:

    if (DEBUG_FOCUS || DEBUG_WINDOW_MOVEMENT) Log.v(
        TAG, "Adding window " + window + " at "
        + (i+1) + " of " + mWindows.size() + " (after " + pos + ")");

4
把条件作为第一个参数传递,这样不是更简洁吗? - Snicolas
1
尽管每个日志语句都需要额外的代码,但这似乎是最佳解决方案:行号得以保留(ProGuard方法的弱点),不执行创建日志消息的代码(包装类方法的弱点和日志记录库方法的显然缺陷)。根据@LA_的说法,Google在应用内计费示例中使用了这种方法,这也支持了我的想法。 - OneWorld
2
@Snicolas 如何在不实现包装器的情况下将条件作为第一个参数传递?此外,如果将其添加为参数,则在进入方法之前,必须评估所有参数,包括消息字符串。必须在构建参数之前测试条件。所提出的解决方案可能是在没有外部工具的情况下最好的解决方案。 - type-a1pha
1
@type-a1pha 你说得对。我现在意识到这更加优化。尽管如此,我从未遇到过一个完全符合我在Android上记录日志需求的库。 - Snicolas
2
从二进制代码的角度来看,这是最好的。但是像这样编码只是为了简单的调试日志输出而付出了很多努力。代码可读性显著降低。我想赢得一些,失去一些吧... - Richard Le Mesurier
显示剩余2条评论

31

Christopher的Proguard解决方案是最好的,但如果出于任何原因你不喜欢Proguard,这里有一个非常简单的解决方案:

注释日志:

find . -name "*\.java" | xargs grep -l 'Log\.' | xargs sed -i 's/Log\./;\/\/ Log\./g'

取消注释日志:

find . -name "*\.java" | xargs grep -l 'Log\.' | xargs sed -i 's/;\/\/ Log\./Log\./g'

一个限制条件是你的日志记录指令不能跨越多行。

(在项目的根目录中,在UNIX shell中执行这些命令。如果使用Windows,请获取UNIX层或使用相当于Windows的命令)


1
如果在 Mac 上运行 Sed,则需要在 -i 后面加上 ""(参见此处)。谢谢。 - Vishal
如果您在非括号 while 分支之后有一个日志(正如您在第一篇帖子中建议的那样),该怎么办? - type-a1pha
如果您采用这种解决方案,那么您必须将括号块视为强制性。 - Nicolas Raoul
2
@NicolasRaoul 分号可以解决这个问题(// vs. ;//)。 - Alex Gittemeier
@GopalSinghSirvi:在我的解决方案之上的所有解决方案都可以在Android Studio中实现,请尝试它们。 - Nicolas Raoul
显示剩余4条评论

20

我想为在Android Studio和Gradle中使用Proguard添加一些细节,因为我曾经遇到过许多从最终二进制文件中删除日志行的问题。

为了让Proguard中的assumenosideeffects起作用,有一个前提条件。

在您的gradle文件中,您必须指定使用proguard-android-optimize.txt作为默认文件。

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

        // With the file below, it does not work!
        //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

实际上,在默认的proguard-android.txt文件中,使用两个标志禁用了优化:

-dontoptimize
-dontpreverify

proguard-android-optimize.txt文件没有添加这些行,因此现在assumenosideeffects可以使用。

然后,就个人而言,我使用SLF4J,尤其是当我开发一些分发给他人的库时。优点是默认情况下没有输出。如果集成者想要一些日志输出,他可以使用Logback for Android并激活日志,因此日志可以重定向到文件或LogCat。

如果我真的需要从最终库中去除日志,那么我会在我的Proguard文件中添加以下内容(当然是在启用了proguard-android-optimize.txt文件后):

-assumenosideeffects class * implements org.slf4j.Logger {
    public *** trace(...);
    public *** debug(...);
    public *** info(...);
    public *** warn(...);
    public *** error(...);
}

这在新的Jack编译器中不起作用--https://dev59.com/2Zjga4cB1Zd3GeqPGzOI?rq=1 - fattire
这对我很有帮助;需要使用proguard-android-optimize.txt作为默认的Proguard文件,并在自定义的Proguard文件中使用-assumenosideeffects!我正在使用R8收缩器(现在是默认值)和默认的Android日志记录。 - Jonik

12

我强烈建议使用Jake Wharton的Timber。

https://github.com/JakeWharton/timber

它可以解决您启用/禁用问题,并自动添加标记类。

只需要...

public class MyApp extends Application {

  public void onCreate() {
    super.onCreate();
    //Timber
    if (BuildConfig.DEBUG) {
      Timber.plant(new DebugTree());
    }
    ...

日志仅在您的调试版本中使用,然后使用。

Timber.d("lol");
或者
Timber.i("lol says %s","lol");

打印

“您的类/消息”,不指定标签


2
Timber非常好用,但如果您已有现有项目,可以尝试https://github.com/zserge/log。它是android.util.Log的替代品,拥有大部分Timber的功能甚至更多。 - zserge
zserge,你的日志解决方案看起来很不错。有很多功能。你考虑过像Timber一样添加Lint规则吗? - jk7

8

我使用了一个类似于Google IO示例应用程序中使用的LogUtils类。我修改了它,使用应用程序特定的DEBUG常量而不是BuildConfig.DEBUG,因为BuildConfig.DEBUG不可靠。然后在我的类中,我有以下内容。

import static my.app.util.LogUtils.makeLogTag;
import static my.app.util.LogUtils.LOGV;

public class MyActivity extends FragmentActivity {
  private static final String TAG = makeLogTag(MyActivity.class);

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    LOGV(TAG, "my message");
  }
}

我曾经使用过Build.DEBUG的bug报告,给了它一个加1。我也放弃了各种“正确”的解决方法,采用了与你类似的风格解决方案。 - Richard Le Mesurier

7
我是一名有用的助手,可以为您进行文本翻译。以下是您需要翻译的内容:

我发布了这个解决方案,特别适用于Android Studio用户。我最近也发现了Timber,并通过以下方式成功地将其导入到我的应用程序中:

将库的最新版本放入您的build.gradle文件中:

compile 'com.jakewharton.timber:timber:4.1.1'

在Android Studio中,转到“编辑”->“查找”->“替换路径...”。在“要查找的文本”文本框中输入Log.e(TAG,或者你已经定义好的Log消息。然后用Timber.e(替换它。保留HTML标签。

enter image description here

点击查找,然后替换所有。

Android Studio将遍历您项目中的所有文件,并用Timber替换所有日志。

我使用这种方法唯一的问题是,gradle在此之后会出现数百万个错误消息,因为它无法在每个Java文件的导入中找到“Timber”。只需单击错误,Android Studio就会自动将“Timber”导入到您的Java文件中。完成所有错误文件的操作后,gradle将再次编译。

您还需要在 Application 类的 onCreate 方法中放置此代码片段:

    if (BuildConfig.DEBUG) {
        Timber.plant(new Timber.DebugTree());
    }

这将使应用仅在开发模式下记录日志,而不是生产模式。您还可以使用BuildConfig.RELEASE在发布模式下记录日志。

3
尝试对您的导入执行相同的操作,并确保勾选了“正则表达式”框。要查找的文本:import android\.util\.Log\;替换为:import android\.util\.Log\;\nimport timber\.log\.Timber\; - Clark Wilson
或者您可以像Chike Mgbemena在他的帖子中展示的那样使用结构化搜索和替换。 - Maksim Turaev
@MaksimTuraev,您的链接已经失效了。现在它是一个关于发型的博客。 - Vadim Kotov
看起来帖子被删除了 =( 在任何地方都找不到它。 - Maksim Turaev
@MaksimTuraev 这是从Wayback机器上的一份副本,但图片已经损坏 - https://web.archive.org/web/20161004161318/http://chikemgbemena.com/2016/10/01/android-studio-structural-search-and-replace/ - Vadim Kotov
替换为:Timber.e(TAG + ": " +将是更好的实践。 - DeveloperKurt

7
我会考虑使用roboguice的日志功能,而不是内置的android.util.Log。
他们的功能会自动禁用发布版本中的调试和详细日志。 此外,您还可以免费获得一些很棒的功能(例如可定制的日志记录行为、每个日志的附加数据等)。
使用proguard可能会很麻烦,除非您有充分的理由(禁用日志记录不是一个好理由),否则我不会费力去配置和使其与您的应用程序配合工作

当你无法使用混淆时,一种非常好的方法...特别是因为Roboguice由于Proguard而崩溃,哈哈。 - Snicolas
1
更新了Robojuice的日志记录工具链接:https://github.com/roboguice/roboguice/wiki/Logging-via-Ln - RenniePet

7
如果你能进行全局替换(一次),并且在此之后保留一些编码约定,你可以遵循 Android 框架 中经常使用的模式。

而不是写

Log.d(TAG, string1 + string2 + arg3.toString());

将其作为
if (BuildConfig.DEBUG) Log.d(TAG, string1 + String.format("%.2f", arg2) + arg3.toString());

现在,Proguard可以从优化的发行版DEX中删除StringBuilder及其使用的所有字符串和方法。使用proguard-android-optimize.txt,您不需要担心proguard-rules.pro中的android.util.Log
android {
  …
  buildTypes {
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
}

使用Android Studio gradle插件,BuildConfig.DEBUG非常可靠,因此您不需要额外的常量来控制剥离。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接