如何配置Proguard以删除非常规的日志消息?

5

Proguard使用assumenosideeffects关键字可以很好地删除一些不重要的Log调用。但是,它对于一些较为复杂的Log调用处理得并不好。

所谓“较为复杂”,就是指不止包含一个简单字符串的Log调用。比如:Log.i(TAG,"velocity="+velocity)
Proguard会保留new StringBuilder("velocity=")以及连接该值的变量,而仅仅只是移除这行代码中的Log调用。
这些字符串将会占用内存和CPU周期,并且还有助于黑客理解代码。


为了解决这个问题,我会把每个非常规的调试Log调用用if(BuildConfig.DEBUG){...}语句包裹起来。 但是这样做每次都需要打这样的包裹语句,非常繁琐且容易出错,也不符合DRY(Don't repeat yourself)原则。

那么,是否有一种方法可以完全删除某个方法(或通过其他方式),包括所有调用该方法的方法?

例如:

@proguard_purge
public static void vanishingDebug(String whatever) {
  Log.i(TAG,whatever);
}

那么混淆器会使该方法消失,同时也会递归地消除所有对此方法的调用吗?

详细说明

混淆将优化代码并删除未使用或被排除的方法。
但是代码编译会在方法调用之前生成额外的字节码,这些先行代码在混淆后将会保留,留下类似以下的代码:

new StringBuilder("velocity=").append(a)

(假设在编译时无法确定a。要进行测试,请使用velocity=Math.random();)

这使得混淆代码变得非常容易理解。
要重现此问题,您需要安装dex2jar将apk转换为jar,并JAD将jar转换为java代码。 您将看到实际留下的内容,会感到恐惧。

示例

@Override
protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    Log.i("TAG", "Simple Comment"); // << Disappears well!

    double index = Math.random();
    index++;

    Log.i("TAG2", "log_index=" + index); // << hmm... (!)

    // class with only log calls inside
    new ReferencedClass();

    // simple method call
    MyLogger.notLog("no_log" + index); // << stays, as expected

    // simple method call with only Log call inside (aka "Log Wrapper")
    MyLogger.log("log" + index);  // << stays, as expected

    Log.i("TAG2", "This is random:" + Math.random()); // << stays, same as above

    setContentView(R.layout.activity_main);
}

使用此混淆配置:

-assumenosideeffects class android.util.Log {
    public static *** isLoggable(java.lang.String, int);
    public static *** d(...);
    public static *** v(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
}

它将被混淆为一个可以反编译和反混淆为以下内容的状态:

  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    double d = 1.0D + Math.random();
    new StringBuilder("log_index=").append(d).toString();
    new b();
    a.a("no_log" + d);
    new StringBuilder("log").append(d).toString();
    a.a();
    new StringBuilder("This is random:").append(Math.random()).toString();
    setContentView(2130903064);
  }

1
你有使用过assumenosideeffects关键字吗? - NickT
1
@NickT 当然,这就是我所说的琐碎用法,我编辑了我的问题以澄清这一点。 - Amir Uval
3个回答

2
我得出结论,ProGuard无法知道StringBuilder与要删除的日志相关。 因此,不能有ProGuard规则来删除以下类型的复杂日志:
Log.i("TAG2", "This is random:" + Math.random());

它将始终生成一段混淆的代码和剩余部分,可以解码为:

new StringBuilder("This is random:").append(Math.random()).toString();

消除剩余的唯一方法是用以下方式包装每个非平凡的日志调用:

if (BuildConfig.DEBUG) Log.i(...)

2

根据我的研究,Proguard无法完成这个任务。不过,你可以通过设置Ant构建脚本来实现。

<target name="-commentoutlogs">
    <replaceregexp match="(Log\..*?;\s*\n)" replace="/*\1*/" flags="gs" byline="false">
        <fileset dir="src">
            <include name="**/*.java"/>
        </fileset>
    </replaceregexp>
</target>


<target name="-uncommentlogs">
    <replaceregexp match="\/\*(Log\..*?;\s*\n)\*\/" replace="\1" flags="gs" byline="false">
        <fileset dir="src">
            <include name="**/*.java"/>
        </fileset>
    </replaceregexp>
</target>

这是一个基于正则表达式的简单脚本,您可以将其添加到Ant发布目标的build.xml文件中,如下所示:

<target name="release"
            depends="-uncommentlogsbefore, -commentoutlogs, -set-release-mode, -release-obfuscation-check, -package, -post-package, -release-prompt-for-password, -release-nosign, -release-sign, -uncommentlogsafter, -post-build"
            description="Builds the application in release mode.">
</target>

当然,您还需要创建一个名为“uncommentlogsbefore”的目标,与“uncommentlogs”具有相同的内容。
这基本上是在任何日志之前放置/*,并在最近的);之后放置*/。

谢谢!我的问题是我正在使用第三方构建工具(Crashlytics),它调用proguard,所以我不确定是否可以在他们的脚本中插入Ant脚本。但这可能会解决其他开发人员的问题,希望有人在这里评论是否有效。 - Amir Uval

0

您可以在proguard文件中添加以下规则,以便删除所有对于调试日志打印的调用:

-assumenosideeffects class android.util.Log {
  public static *** d(...);  }

例如,以下代码中的所有行都将从您混淆的jar文件中消失:
String logevent = "log event";
android.util.Log.d("Tag", "This is my ");
android.util.Log.d("Tag", "This is my " + logevent);

附言:
如果您有这行代码,请不要忘记将其删除或注释掉:-dontoptimize


谢谢你的回答。不过我有一个问题,你是否检查了(去)混淆代码,确保在使用MyLog("text"+var)时调用方没有StringBuilder?我不明白包装器会有什么区别。 - Amir Uval
当你使用Log.d("tag", "x=" + var)时,在哪里找到了StringBuilder的内容?在混淆后的类文件中吗?如果是这样,我在我的混淆后的类文件中找不到任何东西,并且使用了答案中上述三行代码进行了重新检查。我错过了什么吗? - MikeL
我已经对问题进行了详细说明,附加了这个问题的额外解释。 - Amir Uval
我必须承认,你一开始吓到了我,所以我重新检查了我的答案,我必须说我在我的反混淆代码中没有找到任何printouts、members或concats。我在我的jar项目和主apk项目中有调试printouts。对于两者,我都使用了assumenosideeffects来删除调试printouts,并且运行dex2jar并在jargui中查看反混淆的jar文件也没有显示任何调试printout。 - MikeL
添加了一个示例,希望它有所帮助。 - Amir Uval
显示剩余4条评论

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